Skip to main content

frink_models/
decoder.rs

1//! Generic decoder-only transformer forward pass, assembled from a
2//! ModelConfig. Each layer is: RMSNorm -> GQA attention (+RoPE) ->
3//! residual -> RMSNorm -> MoE FFN (router + routed experts + shared
4//! experts) -> residual. This is the standard decoder block shape
5//! shared by the LLaMA/DeepSeek/GLM/Kimi family of open-weight models.
6//!
7//! Weight loading from a real GGUF checkpoint lives in `loader`
8//! (`Decoder::from_gguf`); `Decoder::new_random` builds
9//! correctly-shaped, randomly initialized weights so the full pipeline
10//! -- embedding lookup, N decoder layers, output head -- can be
11//! exercised end to end with real assertions about shapes, finiteness,
12//! and determinism, without requiring a multi-hundred-gigabyte
13//! checkpoint to be present.
14
15mod attn_block;
16#[cfg(feature = "cuda")]
17mod cuda_prefill;
18#[cfg(feature = "metal")]
19mod device_attention;
20mod entry;
21mod ffn_block;
22#[cfg(feature = "metal")]
23mod fused_attention;
24#[cfg(feature = "metal")]
25mod fused_recurrent;
26#[cfg(any(feature = "metal", feature = "cuda"))]
27mod fused_view;
28pub mod kv_window;
29mod lm_head;
30mod pre_norm;
31mod qk_norm;
32mod qkv_bias;
33mod recurrent_block;
34mod rope;
35
36use std::sync::atomic::{AtomicU64, Ordering};
37
38use crate::skip_stream::SkipStream;
39pub(crate) use attn_block::KvStep;
40use frink_core::attention::{
41    causal_gqa_attention_prefill_shared_kv_split, causal_gqa_attention_row,
42    causal_gqa_attention_softcap,
43};
44use frink_core::cache::{KvCache, PagedKvCache, PagedStoreExhausted, SharedPagedKv};
45use frink_core::matmul::rms_norm;
46pub use kv_window::{KvWindowPolicy, KV_WINDOW_ENV};
47#[cfg(feature = "metal")]
48use lm_head::FoldedLmHead;
49use lm_head::Logits;
50use rayon::prelude::*;
51
52/// `Decoder::alibi_slopes` from the config, the ONE derivation, so a
53/// constructor cannot carry a bias without its slopes.
54pub(crate) fn config_alibi_slopes(config: &ModelConfig) -> Option<Vec<f32>> {
55    config
56        .alibi_max_bias
57        .and_then(|b| frink_core::alibi::slopes(config.n_heads, b))
58}
59
60/// Whether the CUDA `gqa_decode` kernel should serve the per-token GQA
61/// reduction (`FRINK_CUDA_GQA=1`). Off by default and only compiled with
62/// `--features cuda`; the host path is byte-identical when unset.
63#[cfg(feature = "cuda")]
64fn cuda_gqa_enabled() -> bool {
65    use std::sync::OnceLock;
66    static ENABLED: OnceLock<bool> = OnceLock::new();
67    *ENABLED.get_or_init(|| {
68        matches!(
69            std::env::var("FRINK_CUDA_GQA").ok().as_deref(),
70            Some("1") | Some("true") | Some("on")
71        )
72    })
73}
74use frink_core::tensor::Tensor;
75use frink_core::weight_matrix::WeightMatrix;
76use frink_moe::{
77    combine_expert_outputs, route_top_k, run_expert, run_expert_placed, ExpertPlacement,
78    ExpertWeights, GluAct, PlacementPlan,
79};
80
81use crate::config::ModelConfig;
82use crate::norm::NormOp;
83use crate::scalar_multipliers::residual_add;
84
85pub struct AttnWeights {
86    pub q_proj: WeightMatrix, // [n_heads*head_dim, hidden_dim]
87    pub k_proj: WeightMatrix, // [n_kv_heads*head_dim, hidden_dim]
88    pub v_proj: WeightMatrix, // [n_kv_heads*head_dim, hidden_dim]
89    pub o_proj: WeightMatrix, // [hidden_dim, n_heads*head_dim]
90    /// The PRE-attention norm, or [`NormOp::None`] for the
91    /// post-norm-only topology (`olmo2` / `exaone4`), which projects
92    /// Q/K/V straight off the raw residual. See [`crate::norm`].
93    pub norm_weight: NormOp,
94    /// OLMoE-style QK-RMSNorm (`attn_q_norm`/`attn_k_norm` GGUF tensors),
95    /// applied to the *whole* q_proj/k_proj output (width `n_heads*head_dim`
96    /// / `n_kv_heads*head_dim`) before RoPE -- confirmed against
97    /// `OlmoeAttention.forward` in `transformers/models/olmoe/modeling_olmoe.py`
98    /// (`q_norm(q_proj(x))`, `k_norm(k_proj(x))`, both plain whole-vector
99    /// RMSNorm, not per-head). `None` for every model that doesn't ship
100    /// these tensors -- absent, not zero/identity-weighted, so existing
101    /// presets/fixtures are byte-for-byte unaffected.
102    ///
103    /// Qwen3 / Gemma3 ship the same tensor names with length `head_dim`
104    /// (per-head). Which style is used is selected by
105    /// [`ModelConfig::qk_norm_style`] (refined at load from weight length).
106    pub q_norm: Option<Vec<f32>>,
107    pub k_norm: Option<Vec<f32>>,
108    /// Qwen2/Qwen2-MoE-family QKV attention bias (`attn_{q,k,v}.bias`
109    /// GGUF tensors, real `config.qkv_bias`), added elementwise to the
110    /// corresponding projection's output before QK-norm/RoPE -- confirmed
111    /// against the real `transformers` source
112    /// (`Qwen2MoeAttention.__init__`: `q_proj = nn.Linear(..., bias=
113    /// config.qkv_bias)`, same for `k_proj`/`v_proj`; `o_proj` has no
114    /// bias). Found as a real, previously-unhandled architecture gap:
115    /// frink's generic GGUF loader silently ignored these real tensors
116    /// entirely, producing fluent-but-wrong output on a real downloaded
117    /// Qwen1.5-MoE checkpoint (same failure class as OLMoE's missing
118    /// QK-norm). `None` for every model that doesn't ship these tensors.
119    pub q_bias: Option<Vec<f32>>,
120    pub k_bias: Option<Vec<f32>>,
121    pub v_bias: Option<Vec<f32>>,
122    /// Gemma 2+/3 post-attention RMSNorm (`blk.N.post_attention_norm.weight`
123    /// / llama.cpp `attn_post_norm`). Applied to attention output before
124    /// the residual add. `None` for Llama/Qwen/OLMoE.
125    pub post_attn_norm: Option<Vec<f32>>,
126    /// Gemma 2+/3 post-FFN RMSNorm (`blk.N.post_ffw_norm.weight`).
127    pub post_ffn_norm: Option<Vec<f32>>,
128    /// The learned output gate, `blk.N.attn_gate.weight`, applied to the
129    /// attention output before `o_proj` (`afmoe`, `laguna`, `step35`).
130    /// See [`crate::attn_gate`] for the two axes it varies on and the
131    /// one input it always reads. `None` for every architecture whose
132    /// graph has no such op; a file carrying the tensor on one of those
133    /// is refused as unconsumed rather than gated.
134    pub output_gate: Option<crate::attn_gate::AttnGate>,
135    /// `blk.N.attn_sinks.weight`, one learned logit per query head that
136    /// joins every softmax and contributes nothing to the output
137    /// (`ggml_soft_max_add_sinks`, `llama-graph.cpp:2600`).
138    ///
139    /// Used to live on the gpt-oss side table alone, which spelled the
140    /// rule as "arch is gpt-oss". Four llama.cpp graphs pass this
141    /// tensor into `build_attn` -- `openai-moe.cpp:115`,
142    /// `mimo2.cpp:177`, `dflash.cpp`, `deepseek4.cpp` -- through the
143    /// SAME `build_attn_mha` path, so the rule is "the tensor is
144    /// present". gpt-oss requires it (`openai-moe.cpp:44`) and
145    /// `loader.rs` still refuses a gpt-oss file without one; the fused
146    /// Metal launches refuse any layer that has one, by the exhaustive
147    /// destructure in `Decoder::metal_attn_view`.
148    pub sinks: Option<Vec<f32>>,
149    /// BitNet's `blk.N.attn_sub_norm.weight`, `[hidden_dim]`: an RMSNorm
150    /// on the attention output BEFORE `o_proj` (`bitnet.cpp:101-106`),
151    /// the other side of that matmul from `post_attn_norm`. Loaded
152    /// only for a model whose `ModelConfig::block_sub_norms` says so
153    /// (`crate::sub_norms`); the fused Metal launches refuse the model
154    /// through `metal_can_serve_model` and the layer through the
155    /// exhaustive destructure in `Decoder::metal_attn_view`.
156    pub attn_sub_norm: Option<Vec<f32>>,
157    /// `blk.N.attn_output.scale`, the `{1}` companion `build_lora_mm`
158    /// multiplies the attention branch by right after `wo`
159    /// (`llama-graph.cpp:1492-1494`; `talkie` writes it on every export,
160    /// `crate::weight_scales`). The fused Metal launches refuse a layer
161    /// that has one through the destructure in `metal_attn_view`.
162    pub o_scale: Option<f32>,
163    /// `blk.N.attn_output.bias`, added right after `wo` (and after
164    /// `o_scale`, the order `build_attn` has). Loaded for the
165    /// architectures whose graph creates the tensor
166    /// (`crate::proj_bias::ATTN_OUT_BIAS_CREATORS`), which includes
167    /// gpt-oss, whose bias used to live on its side table; `None`
168    /// everywhere else, where a present tensor is refused as unread.
169    /// The fused Metal launches refuse a layer that has one through
170    /// the destructure in `metal_attn_view`.
171    pub o_bias: Option<Vec<f32>>,
172    /// LFM2's short convolution, `Some` on exactly the layers whose
173    /// shape is `AttnShape::ShortConv` (`crate::shortconv`); the four
174    /// projections above are empty on such a layer. The host bodies
175    /// branch on the SHAPE and reach this field through it; the fused
176    /// Metal launches refuse the model (a short-conv model is never
177    /// uniform) and the layer (the destructure in `metal_attn_view`).
178    pub shortconv: Option<crate::shortconv::ShortConv>,
179    /// The state-space block (`crate::ssm_block`): `Some` on exactly the
180    /// layers whose shape is `AttnShape::Mamba1` / `Mamba2`, and on
181    /// every GQA layer of a `ModelConfig::parallel_ssm` model
182    /// (`crate::mamba2::PARALLEL_WITH_ATTENTION`); the same rules as
183    /// `shortconv` otherwise.
184    pub ssm: Option<crate::ssm_block::SsmBlock>,
185    /// `attn_q` is `2 * n_heads * head_dim` wide, each head's `[q, gate]`
186    /// interleaved, and `sigmoid(gate)` multiplies the attention output
187    /// before `wo` (`crate::attn_gate::Q_INTERLEAVED_GATE_ARCHS`). The
188    /// projection stays one matrix; the three host bodies split its
189    /// output. The fused Metal launches refuse the layer.
190    pub q_gate_interleaved: bool,
191}
192
193/// How a layer's routed experts are held. `Resident` is the original
194/// always-in-memory form (owned f32 or zero-copy mmap views).
195/// `Stored` holds only byte-range layouts; each use acquires the
196/// expert's bytes from a bounded, lease-protected
197/// `frink_core::expert_store::ExpertStore` shared by every layer
198/// (one global byte budget), builds temporary `WeightMatrix` views
199/// over the leased buffer (`WeightBytes::Shared`, which pins the
200/// cache entry for the views' lifetime), and drops them after the
201/// expert runs. Dequantized math over identical bytes is identical,
202/// so the two backings are bit-equivalent by construction -- pinned
203/// by an integration test against the MoE fixture.
204pub enum ExpertBacking {
205    Resident(Vec<ExpertWeights>),
206    Stored {
207        store:
208            std::sync::Arc<frink_core::expert_store::ExpertStore<crate::loader::GgufExpertSource>>,
209        layouts: Vec<crate::loader::StoredExpertLayout>,
210        layer: u32,
211    },
212}
213
214impl ExpertBacking {
215    pub fn n_experts(&self) -> usize {
216        match self {
217            ExpertBacking::Resident(v) => v.len(),
218            ExpertBacking::Stored { layouts, .. } => layouts.len(),
219        }
220    }
221}
222
223pub struct MoeWeights {
224    pub router: WeightMatrix, // [n_experts, hidden_dim]
225    pub experts: ExpertBacking,
226    pub shared_experts: Vec<ExpertWeights>,
227    /// Qwen2-MoE-specific: when present, the shared experts' combined
228    /// output is scaled by `sigmoid(shared_expert_gate . x)` before
229    /// being added to the routed output, instead of added unconditionally
230    /// -- confirmed against the real `transformers` source
231    /// (`Qwen2MoeSparseMoeBlock.forward`: `shared_expert_output =
232    /// F.sigmoid(self.shared_expert_gate(hidden_states)) *
233    /// shared_expert_output`) and llama.cpp's real `qwen2moe.cpp`
234    /// (`ffn_gate_inp_shexp` dotted against the hidden state, sigmoid,
235    /// multiplied into the shared-expert branch before the final add).
236    /// Real on-disk shape is `[hidden_dim]` (a `Linear(hidden_dim, 1,
237    /// bias=false)`'s weight, flattened -- ggml's real `create_tensor`
238    /// call declares it as `{n_embd}`, not a 2D matrix), so this is a
239    /// plain owned vector dotted with the normed hidden state directly,
240    /// not a `WeightMatrix`. `None` for every other architecture
241    /// (DeepSeek-V3's shared experts, for one real confirmed contrast,
242    /// add unconditionally with no gate at all).
243    pub shared_expert_gate: Option<Vec<f32>>,
244    /// The PRE-FFN norm, or [`NormOp::None`] for the post-norm-only
245    /// topology (`olmo2` / `exaone4`), which runs the FFN on the raw
246    /// post-attention residual. See [`crate::norm`].
247    pub norm_weight: NormOp,
248    /// DeepSeek-V3's aux-loss-free expert-selection bias, on disk as
249    /// `blk.{N}.exp_probs_b.bias` (llama.cpp's `LLM_TENSOR_FFN_EXP_PROBS_B`
250    /// -- note the on-disk name has no `ffn_` prefix, `llama-arch.cpp:416`).
251    /// It is added to the *selection* score only: the top-k is taken over
252    /// `gating(logit) + bias[expert]`, while each winner's combine weight
253    /// comes from the unbiased `gating(logit)`
254    /// (`build_moe_ffn`: "leave probs unbiased as it's later used to get
255    /// expert weights"). Biasing the weight too would silently skew every
256    /// routed contribution away from what the router learned.
257    ///
258    /// `None` for every checkpoint that does not ship the tensor. When it
259    /// *is* present, the GPU MoE fast paths refuse the layer rather than
260    /// route without it -- their kernels have no bias input.
261    pub exp_probs_bias: Option<Vec<f32>>,
262    /// BitNet's `blk.N.ffn_sub_norm.weight`, `[ffn_dim]`: an RMSNorm on
263    /// `silu(gate) * up` BEFORE `down` (`bitnet.cpp:135-140`), inside
264    /// the dense FFN. `Some` only for a model whose
265    /// `ModelConfig::block_sub_norms` says so (`crate::sub_norms`), and
266    /// only on a dense layer: no MoE graph has this site. Read by
267    /// `Decoder::run_dense_expert` and `Decoder::dense_ffn_batch`, the
268    /// two dense FFN bodies; the fused kernels never see it because
269    /// `metal_can_serve_model` refuses the model.
270    pub ffn_sub_norm: Option<Vec<f32>>,
271    /// `blk.N.ffn_down.scale`, the `{1}` companion multiplied onto the
272    /// dense FFN's output right after `down` (`crate::weight_scales`);
273    /// refused on a routed layer, whose experts carry their own.
274    pub down_scale: Option<f32>,
275    /// The dense FFN's `blk.N.ffn_{up,gate,down}.bias`
276    /// (`crate::proj_bias`), on a dense layer whose architecture's
277    /// graph creates them and whose file carries at least one; `None`
278    /// otherwise. Applied by `run_dense_expert` and `dense_ffn_batch`,
279    /// whose fused Metal launch has no bias site and is fenced on it.
280    pub dense_bias: Option<frink_moe::DenseBias>,
281    /// Arctic's `blk.N.ffn_norm_exps.weight`, `[hidden_dim]`: the SECOND
282    /// per-layer norm, applied to the layer INPUT to make the routed
283    /// branch's operand (`arctic.cpp:45,136-139`;
284    /// `RouterInput::NormedLayerInput`). REQUIRED on a routed layer of
285    /// such a model, `None` everywhere else. Read by
286    /// `Decoder::router_operand`, the one constructor of the operand.
287    pub exps_norm: Option<Vec<f32>>,
288    /// The factor on `ffn_out + moe_out` for a layer whose dense FFN is
289    /// summed with its experts (`crate::parallel_dense_ffn`): `Some`
290    /// only when the loader filled `shared_experts` from the dense
291    /// names AND the row scales the sum (`grok.cpp:180`, `sqrt(2)/2`).
292    /// Applied by the FFN bodies to the whole branch output before the
293    /// post-FFN norm.
294    pub parallel_sum_scale: Option<f32>,
295    /// `Some` when this layer is a PARALLEL residual, `x + attn(norm(x))
296    /// + ffn(norm(x))`, and under which norm the FFN reads the layer
297    /// input (`crate::parallel_residual`): the vector attention read
298    /// (`SharedNorm`, and then `norm_weight` is `NormOp::None` because
299    /// there is no tensor) or its own `ffn_norm` of the layer input
300    /// (`TwoNorms`). `None` is the sequential layer, `ffn_norm(h)` over
301    /// the post-attention residual. Read by ONE constructor,
302    /// `Decoder::branch_inputs`, before attention runs.
303    pub parallel: Option<crate::parallel_residual::ParallelNorm>,
304    /// How many times each routed expert (index into `experts`) has been
305    /// selected by `route_top_k` across every `forward_token`/
306    /// `forward_batch` call so far. Real observed hotness, not a
307    /// placeholder -- feeds `placement_plan` below, which is what
308    /// `PlacementPlan::from_budget` needs to prioritize actually-hot
309    /// experts for GPU residency instead of guessing by index.
310    pub activation_counts: Vec<AtomicU64>,
311    /// Verified-at-load contiguous expert planes for Metal MoE
312    /// (`mul_mm_sg` gather/id). Built in `loader` when every routed expert
313    /// is mmap-backed with a simdgroup-GEMM quant (Q4_0 / Q4_K / Q8_0 / …)
314    /// and back-to-back gate/up/down slices. Gate/up/down kinds may differ
315    /// (Qwen1.5-MoE: Q4_K gate/up + Q8_0 down). `None` for store-backed,
316    /// F32, or non-contiguous layouts.
317    #[cfg(feature = "metal")]
318    pub packed_q4: Option<MoePackedQ4Planes>,
319}
320
321/// Load-time validated contiguous expert tensor planes (any `mul_mm_sg` quant).
322#[cfg(feature = "metal")]
323pub struct MoePackedQ4Planes {
324    gate: frink_core::weight_matrix::WeightBytes,
325    up: frink_core::weight_matrix::WeightBytes,
326    down: frink_core::weight_matrix::WeightBytes,
327    gate_stride: usize,
328    up_stride: usize,
329    down_stride: usize,
330    n_experts: usize,
331    ffn_rows: usize,
332    hidden_rows: usize,
333    gate_row_bytes: usize,
334    down_row_bytes: usize,
335    gate_kind: &'static str,
336    up_kind: &'static str,
337    down_kind: &'static str,
338}
339
340#[cfg(feature = "metal")]
341impl MoePackedQ4Planes {
342    #[allow(clippy::too_many_arguments)]
343    pub(crate) fn new(
344        gate: frink_core::weight_matrix::WeightBytes,
345        up: frink_core::weight_matrix::WeightBytes,
346        down: frink_core::weight_matrix::WeightBytes,
347        gate_stride: usize,
348        up_stride: usize,
349        down_stride: usize,
350        n_experts: usize,
351        ffn_rows: usize,
352        hidden_rows: usize,
353        gate_kind: &'static str,
354        up_kind: &'static str,
355        down_kind: &'static str,
356    ) -> Self {
357        Self {
358            gate,
359            up,
360            down,
361            gate_stride,
362            up_stride,
363            down_stride,
364            n_experts,
365            ffn_rows,
366            hidden_rows,
367            gate_row_bytes: gate_stride / ffn_rows,
368            down_row_bytes: down_stride / hidden_rows,
369            gate_kind,
370            up_kind,
371            down_kind,
372        }
373    }
374
375    pub fn view(&self) -> frink_metal::gpu::MoePackedQ4<'_> {
376        frink_metal::gpu::MoePackedQ4 {
377            gate: self.gate.as_slice(),
378            up: self.up.as_slice(),
379            down: self.down.as_slice(),
380            gate_stride: self.gate_stride,
381            up_stride: self.up_stride,
382            down_stride: self.down_stride,
383            n_experts: self.n_experts,
384            ffn_rows: self.ffn_rows,
385            hidden_rows: self.hidden_rows,
386            gate_row_bytes: self.gate_row_bytes,
387            down_row_bytes: self.down_row_bytes,
388            gate_kind: self.gate_kind,
389            up_kind: self.up_kind,
390            down_kind: self.down_kind,
391        }
392    }
393}
394
395impl MoeWeights {
396    pub fn n_experts(&self) -> usize {
397        self.experts.n_experts()
398    }
399
400    /// This routed expert's weight byte footprint, from resident
401    /// matrices or the stored layout -- identical numbers either way,
402    /// so residency planning is backing-independent.
403    pub fn expert_bytes(&self, e: usize) -> usize {
404        match &self.experts {
405            ExpertBacking::Resident(v) => {
406                let ex = &v[e];
407                ex.gate.resident_bytes() + ex.up.resident_bytes() + ex.down.resident_bytes()
408            }
409            ExpertBacking::Stored { layouts, .. } => layouts[e].total_bytes(),
410        }
411    }
412
413    /// Runs `f` against expert `e`'s weights, materializing them from
414    /// the store first when this layer is store-backed. The lease (and
415    /// therefore the cache entry's pin) lives exactly as long as `f`'s
416    /// borrow.
417    pub fn with_expert<R>(&self, e: usize, f: impl FnOnce(&ExpertWeights) -> R) -> R {
418        match &self.experts {
419            ExpertBacking::Resident(v) => f(&v[e]),
420            ExpertBacking::Stored {
421                store,
422                layouts,
423                layer,
424            } => {
425                let lease = store
426                    .acquire(frink_core::expert_store::ExpertKey {
427                        layer: *layer,
428                        expert: e as u32,
429                    })
430                    .unwrap_or_else(|err| {
431                        panic!(
432                            "expert store read failed for layer {layer} expert {e}: {err} \
433                             (checkpoint file unreadable mid-decode)"
434                        )
435                    });
436                let tmp = layouts[e].materialize(&lease);
437                f(&tmp)
438            }
439        }
440    }
441
442    /// The one expert a DENSE layer runs, recorded as the host body
443    /// records it, so a fused layer's hotness counters do not depend on
444    /// which backend ran it (`crate::fused_layer`).
445    #[cfg(feature = "metal")]
446    pub(crate) fn record_activations_dense(&self) {
447        self.record_activations(&[0]);
448    }
449
450    fn record_activations(&self, expert_ids: &[usize]) {
451        for &eid in expert_ids {
452            if let Some(counter) = self.activation_counts.get(eid) {
453                counter.fetch_add(1, Ordering::Relaxed);
454            }
455        }
456    }
457
458    /// A real VRAM-budget-and-hotness-driven placement plan for this
459    /// layer's routed experts, built from each expert's actual resident
460    /// byte size (`WeightMatrix::resident_bytes()` summed across its
461    /// gate/up/down matrices, so it reflects the real quantization
462    /// format in use, not an estimate) and the activation counts
463    /// observed so far. See `frink_moe::PlacementPlan::from_budget`.
464    pub fn placement_plan(&self, vram_budget_bytes: u64) -> PlacementPlan {
465        let sizes: Vec<usize> = (0..self.n_experts())
466            .map(|e| self.expert_bytes(e))
467            .collect();
468        let counts: Vec<u64> = self
469            .activation_counts
470            .iter()
471            .map(|c| c.load(Ordering::Relaxed))
472            .collect();
473        let has_observations = counts.iter().any(|&c| c > 0);
474        PlacementPlan::from_budget(
475            &sizes,
476            has_observations.then_some(counts.as_slice()),
477            vram_budget_bytes,
478        )
479    }
480}
481
482pub struct LayerWeights {
483    pub attn: AttnWeights,
484    pub moe: MoeWeights,
485    /// Talkie's `blk.N.layer_output_scale.weight`, the scalar its
486    /// embedding skip stream is multiplied by before joining this
487    /// layer's output (`talkie.cpp:123-126`, `crate::skip_stream`).
488    /// `Some` only on a `ModelConfig::skip_stream` model.
489    pub out_scale: Option<f32>,
490}
491
492/// The per-layer weights the gpt-oss graph carries and the generic GQA
493/// layer structs do not.
494///
495/// Held as a side table on [`Decoder`] rather than as new `Option`
496/// fields on [`AttnWeights`]/[`MoeWeights`] for two reasons. The first
497/// is mechanical: those two structs have thirty construction sites
498/// across seven loaders and every dedicated engine, and none of them
499/// will ever set these. The second is the point of the exercise — a
500/// checkpoint either has the whole gpt-oss graph or none of it, so
501/// `Decoder::gpt_oss.is_some()` is a single, checkable predicate for
502/// "this model needs the gpt-oss path", which is what the CPU-only and
503/// paged-attention refusals below key off. Scattering four independent
504/// `Option`s would make "half the graph is wired" representable, and
505/// that state is precisely the silent-wrong-answer bug this work exists
506/// to remove.
507///
508/// The attention sinks used to be the fifth field here and are
509/// [`AttnWeights::sinks`] now, because they are NOT gpt-oss-only:
510/// `mimo2.cpp:58,177` passes the same tensor into the same
511/// `build_attn_mha`. What keeps the whole-graph rule intact is that
512/// the loader refuses a gpt-oss file whose sinks are absent.
513pub struct GptOssLayer {
514    /// `blk.N.ffn_gate_inp.bias`, added to the router logits.
515    /// (`attn_output.bias` used to be here too and is
516    /// [`AttnWeights::o_bias`] now: `crate::proj_bias` fills that slot
517    /// for every graph that creates the tensor, gpt-oss among them.)
518    pub router_bias: Vec<f32>,
519    /// `blk.N.ffn_{gate,up,down}_exps.bias`, one entry per expert.
520    pub expert_bias: Vec<frink_moe::ExpertBias>,
521}
522
523/// gpt-oss side table: one entry per layer, in layer order.
524pub struct GptOssWeights {
525    pub layers: Vec<GptOssLayer>,
526}
527
528pub struct Decoder {
529    pub config: ModelConfig,
530    /// `[vocab_size, hidden_dim]`. A `WeightMatrix` rather than an
531    /// eagerly-widened f32 `Tensor`, so a quantized `token_embd.weight`
532    /// stays quantized on disk/mmap and token lookup dequantizes one
533    /// row at a time (`WeightMatrix::dequant_row`) -- a large-vocab
534    /// model's embedding table is multi-GB in f32 and only ever read
535    /// row-wise.
536    pub embedding: WeightMatrix,
537    /// The learned position table, `[context_length, hidden_dim]`, for
538    /// a graph that adds one to the embeddings (`crate::position_embd`;
539    /// `gpt2`, `starcoder`), read by [`Self::embed_token`] and nothing
540    /// else. `None` for every architecture that encodes position by
541    /// rotation or not at all.
542    pub position_embd: Option<WeightMatrix>,
543    /// The norm on the token embeddings before layer 0
544    /// (`norm_sites::EMBEDDING_NORM_ARCHITECTURES`: `bloom`'s
545    /// `token_embd_norm`, a biased LayerNorm), or [`NormOp::None`] for
546    /// every other architecture. Applied in [`Self::embed_token`], the
547    /// one embedding site; the GPU embedding gather has no norm and is
548    /// not taken for a model that has one.
549    pub embedding_norm: NormOp,
550    /// HRM-Text's learned LOW stream (`hrm.z_l_init`,
551    /// `hrm-text.cpp:46`, one `[n_embd]` row), `None` for every other
552    /// architecture. `crate::hrm` is the state the bodies carry.
553    pub hrm_z_l_init: Option<Vec<f32>>,
554    pub layers: Vec<LayerWeights>,
555    /// The norm before the LM head.
556    ///
557    /// A [`NormOp`] rather than a `Vec<f32>` because `olmo` is the first
558    /// architecture whose final norm is not an RMSNorm: `olmo.cpp:128-130`
559    /// is `build_norm(cur, NULL, NULL, LLM_NORM, -1)`, and `olmo.cpp:15-36`
560    /// creates no `output_norm` tensor for it to weight. The fused Metal
561    /// stacks that fold `final_norm + lm_head + argmax` had
562    /// `Some(&self.final_norm)` written into them unconditionally; now
563    /// they ask [`NormOp::rms_weights`] and fall back to the host body
564    /// when there is nothing to hand over.
565    pub final_norm: NormOp,
566    pub output_head: WeightMatrix, // [vocab_size, hidden_dim]
567    /// `output.bias`, `[vocab_size]`, added to the logits right after
568    /// the head (`crate::proj_bias::OUTPUT_BIAS_CREATORS`: `phi2` and
569    /// `phimoe` REQUIRE it, `qwen2` creates it optional). Applied in
570    /// `decoder::lm_head::Logits`, the one place the head's
571    /// post-projection transforms run; a head with a bias is never
572    /// folded into a fused Metal decode stack (`FoldedLmHead::permit`).
573    pub output_bias: Option<Vec<f32>>,
574    /// ALiBi's per-head slopes, `[n_heads]`, `Some` exactly when
575    /// `ModelConfig::alibi_max_bias` is (`frink_core::alibi::slopes`),
576    /// handed to every host attention kernel as its additive per-key
577    /// bias. Derived from the config at construction so the two cannot
578    /// disagree; no fused GPU path serves a model that has them.
579    pub alibi_slopes: Option<Vec<f32>>,
580    /// Real VRAM budget for GPU-resident routed experts.
581    /// `None` (both constructors below
582    /// set it) means every expert always runs on CPU -- the exact
583    /// behavior this field's absence had before it existed. `Some(bytes)`
584    /// makes each forward call build ONE global `ResidencyPlan`
585    /// (`Decoder::residency_plan`) across every layer's actual
586    /// resident expert sizes and observed activation counts against
587    /// this single budget -- the budget is never re-spent per layer --
588    /// dispatching device-placed routed experts through
589    /// `frink_moe::run_expert_placed` (a real CUDA kernel when the
590    /// `cuda` feature is compiled in and the expert's quant kind has
591    /// one; a correct CPU fallback otherwise, so setting this on a
592    /// non-`cuda` build is harmless, just never GPU-accelerated).
593    /// Shared experts and a dense layer's sole expert always run on
594    /// CPU regardless -- every token activates them, so there's no
595    /// routing decision to offload the way routed-expert placement is.
596    /// Rebuilding the plan on every forward call is real but not yet
597    /// performance-tuned; a real, disclosed limit, not a correctness
598    /// gap.
599    pub gpu_vram_budget_bytes: Option<u64>,
600    /// `Some` only for the gpt-oss family. See [`GptOssWeights`]. When
601    /// set, every layer runs the gpt-oss CPU graph (attention sinks,
602    /// alternating SWA, biased router + experts, `swiglu_oai`), GPU
603    /// offload is refused at load time, and the paged-KV decode path is
604    /// refused at call time — neither implements sinks, and answering
605    /// with a different distribution is the failure this replaces.
606    pub gpt_oss: Option<GptOssWeights>,
607    /// Does this architecture norm Q and K AFTER RoPE rather than
608    /// before? `maincoder` and `hunyuan-moe` do; see [`qk_norm`] for
609    /// the llama.cpp lines and for why no GGUF key can answer this.
610    /// Set by the loader from the architecture string; refused by
611    /// `layer_supports_metal_attn`, because no fused kernel can express
612    /// the order.
613    pub qk_norm_after_rope: bool,
614    /// Per-layer Metal-resident KV for fused decode/prefill attention
615    /// (`FRINK_METAL_ATTN`). Lazily allocated. After
616    /// [`frink_metal::attn::launch_decode_dense_stack`], Metal KV is
617    /// authoritative for the next decode step; host [`KvCache`] may lag
618    /// until [`Self::sync_metal_attn_kv_to_host`] or a CPU fallback.
619    /// Prefill / prefix restore still upload host → Metal when lengths
620    /// diverge for other reasons.
621    #[cfg(feature = "metal")]
622    pub(crate) metal_attn_kv: std::sync::Mutex<Option<Vec<frink_metal::attn::MetalKvBuffers>>>,
623    /// Load-time execution plan (family, fused-op caps, SWA/RoPE
624    /// policy). Built once; hot path must not re-resolve architecture
625    /// strings. See [`crate::execution_plan`].
626    pub execution_plan: crate::execution_plan::ExecutionPlan,
627    /// May a windowed layer's host [`KvCache`] drop rows that have
628    /// fallen behind its window? Off unless `FRINK_KV_WINDOW` says
629    /// otherwise; see [`kv_window`] for what else turns it off. A field
630    /// rather than a cached global so a test can run both arms in one
631    /// process and compare tokens.
632    pub kv_window: KvWindowPolicy,
633    /// LoRA adapters attached after load, by id. Each one's deltas
634    /// live INSIDE the `WeightMatrix` values above
635    /// (`WeightMatrix::Adapted`); this list is what a server lists and
636    /// rescales. See [`crate::lora_attach`].
637    pub lora_adapters: Vec<crate::lora_attach::LoraAttached>,
638    /// Cache key hit → fused caps last used for that geometry (enables
639    /// decode/prefill plan reuse without rebuilding residency).
640    pub plan_cache: std::sync::Mutex<
641        std::collections::HashMap<
642            crate::execution_plan::PlanGeometry,
643            crate::execution_plan::FusedOpCaps,
644        >,
645    >,
646}
647
648/// Simple deterministic pseudo-random generator so tests are
649/// reproducible without pulling in an external `rand` dependency.
650struct Lcg(u64);
651impl Lcg {
652    fn new(seed: u64) -> Self {
653        Lcg(seed)
654    }
655    fn next_f32(&mut self) -> f32 {
656        // xorshift64*
657        self.0 ^= self.0 << 13;
658        self.0 ^= self.0 >> 7;
659        self.0 ^= self.0 << 17;
660        ((self.0 >> 40) as f32 / (1u64 << 24) as f32) - 0.5
661    }
662    fn vec(&mut self, n: usize) -> Vec<f32> {
663        (0..n).map(|_| self.next_f32() * 0.1).collect()
664    }
665}
666
667/// Where a batch of independent sequences keeps its KV.
668///
669/// `forward_multi_seq` batches the projections across sequences but
670/// must attend per sequence, because each has its own length and its
671/// own history. That per-sequence step is the ONLY place the batched
672/// path touches a cache, which is why paging it is a parameter here
673/// rather than a second copy of a 300-line function -- the lesson the
674/// paged decode path taught by losing five model features one at a
675/// time to exactly that kind of copy.
676pub enum MultiSeqKv<'a> {
677    Contiguous(&'a mut [Vec<KvCache>]),
678    Paged {
679        caches: &'a mut [Vec<PagedKvCache>],
680        stores: &'a SharedPagedKv,
681    },
682}
683
684impl MultiSeqKv<'_> {
685    /// Sequences in the batch.
686    pub fn len(&self) -> usize {
687        match self {
688            MultiSeqKv::Contiguous(c) => c.len(),
689            MultiSeqKv::Paged { caches, .. } => caches.len(),
690        }
691    }
692
693    pub fn is_empty(&self) -> bool {
694        self.len() == 0
695    }
696
697    /// Layers each sequence carries, for the shape assertion.
698    fn layers_per_seq(&self, seq: usize) -> usize {
699        match self {
700            MultiSeqKv::Contiguous(c) => c[seq].len(),
701            MultiSeqKv::Paged { caches, .. } => caches[seq].len(),
702        }
703    }
704
705    /// Sequence `b`'s layer-`l` cache as the one-row step the attention
706    /// and short-conv bodies take. The only place this enum's arms are
707    /// matched for a cache, so paging changes where rows live and
708    /// nothing else.
709    fn step(&mut self, b: usize, l: usize) -> KvStep<'_> {
710        match self {
711            // `Batched`, not `Decode`: the CUDA resident per-layer KV
712            // holds ONE sequence's history, and this path never seeds
713            // it. See `KvStep::Batched`.
714            MultiSeqKv::Contiguous(caches) => KvStep::Batched(&mut caches[b][l]),
715            MultiSeqKv::Paged { caches, stores } => KvStep::Paged {
716                cache: &mut caches[b][l],
717                stores,
718            },
719        }
720    }
721}
722
723impl Decoder {
724    /// Eagerly resolve every kernel lookup this model's dispatch paths
725    /// will make, and record it in
726    /// [`frink_core::kernel_registry`] before anything runs.
727    ///
728    /// Call once, at the end of loading, immediately before
729    /// [`frink_core::kernel_registry::seal`]. Nothing here dispatches
730    /// or decides anything: it asks the same predicates the hot path
731    /// asks and writes the answers down, so a kernel that is missing
732    /// becomes a startup line instead of an unexplained benchmark row.
733    ///
734    /// Routed experts held in an [`ExpertBacking::Stored`] layer are not
735    /// probed -- they exist only as byte ranges until a token routes to
736    /// them, and materialising every expert here would defeat the
737    /// bounded expert store. Their kinds are the same as the resident
738    /// case, and a dispatch-site miss still trips the sealed registry.
739    pub fn probe_kernels(&self) {
740        use frink_core::kernel_registry as reg;
741
742        if !reg::enabled() {
743            return;
744        }
745        self.embedding.probe_kernels("token_embd");
746        self.output_head.probe_kernels("output_head");
747        for layer in &self.layers {
748            layer.attn.q_proj.probe_kernels("attn_q");
749            layer.attn.k_proj.probe_kernels("attn_k");
750            layer.attn.v_proj.probe_kernels("attn_v");
751            layer.attn.o_proj.probe_kernels("attn_o");
752            layer.moe.router.probe_kernels("moe_router");
753            for e in &layer.moe.shared_experts {
754                e.gate.probe_kernels("shexp_gate");
755                e.up.probe_kernels("shexp_up");
756                e.down.probe_kernels("shexp_down");
757            }
758            if let ExpertBacking::Resident(experts) = &layer.moe.experts {
759                for e in experts {
760                    e.gate.probe_kernels("ffn_gate");
761                    e.up.probe_kernels("ffn_up");
762                    e.down.probe_kernels("ffn_down");
763                }
764            }
765        }
766        // The generic decoder has a real batched prefill
767        // (`forward_hidden_batch`), so a `pp512` here is one GEMM per
768        // projection, not 512 matvecs. Recorded as a hit so that an
769        // engine which lacks it stands out as a miss rather than as an
770        // absence.
771        reg::record_build(
772            reg::Lookup::new(
773                frink_core::weight_matrix::active_backend(),
774                reg::op::ENGINE_PREFILL_BATCH,
775                None,
776            )
777            .with_role("generic_decoder"),
778            reg::Outcome::Hit,
779        );
780    }
781
782    /// Builds a decoder with correctly-shaped, randomly initialized
783    /// weights for `config`, but overrides `n_layers` and `vocab_size`
784    /// with small test-scale numbers so it can actually be allocated and
785    /// run inside a CI sandbox. Use this to validate the forward-pass
786    /// plumbing only, never to draw conclusions about real model
787    /// quality.
788    pub fn new_random_small(config: ModelConfig, n_layers: usize, vocab_size: usize) -> Self {
789        let mut rng = Lcg::new(42);
790        let mut config = config;
791        config.n_layers = n_layers;
792        config.vocab_size = vocab_size;
793        let hidden = config.hidden_dim;
794        let head_dim = config.head_dim;
795        let n_heads = config.n_heads;
796        let n_kv_heads = config.n_kv_heads;
797
798        let embedding = WeightMatrix::F32(Tensor::new(
799            rng.vec(vocab_size * hidden),
800            vec![vocab_size, hidden],
801        ));
802
803        let wm = |data: Vec<f32>, shape: Vec<usize>| WeightMatrix::F32(Tensor::new(data, shape));
804
805        let mut layers = Vec::with_capacity(n_layers);
806        for layer_idx in 0..n_layers {
807            let attn = AttnWeights {
808                q_proj: wm(
809                    rng.vec(n_heads * head_dim * hidden),
810                    vec![n_heads * head_dim, hidden],
811                ),
812                k_proj: wm(
813                    rng.vec(n_kv_heads * head_dim * hidden),
814                    vec![n_kv_heads * head_dim, hidden],
815                ),
816                v_proj: wm(
817                    rng.vec(n_kv_heads * head_dim * hidden),
818                    vec![n_kv_heads * head_dim, hidden],
819                ),
820                o_proj: wm(
821                    rng.vec(hidden * n_heads * head_dim),
822                    vec![hidden, n_heads * head_dim],
823                ),
824                norm_weight: NormOp::Rms(vec![1.0; hidden]),
825                q_norm: None,
826                k_norm: None,
827                q_bias: None,
828                k_bias: None,
829                v_bias: None,
830                post_attn_norm: None,
831                post_ffn_norm: None,
832                output_gate: None,
833                sinks: None,
834                attn_sub_norm: None,
835                o_scale: None,
836                o_bias: None,
837                shortconv: None,
838                ssm: None,
839                q_gate_interleaved: false,
840            };
841
842            // Leading dense layers (see ModelConfig::layer_is_dense's
843            // doc comment) get a single-expert, no-shared-expert
844            // dense-equivalent FFN regardless of this model's global
845            // MoE topology, matching the DeepSeek-2/3-family
846            // convention found in ik_llama.cpp's source.
847            let is_dense_layer = config.layer_is_dense(layer_idx);
848            let n_experts = if is_dense_layer {
849                1
850            } else {
851                config.moe.n_experts
852            };
853            let n_shared = if is_dense_layer {
854                0
855            } else {
856                config.moe.n_shared_experts
857            };
858            let ffn_dim = config.moe.expert_ffn_dim;
859            let make_expert = |rng: &mut Lcg| ExpertWeights {
860                gate: WeightMatrix::F32(Tensor::new(
861                    rng.vec(ffn_dim * hidden),
862                    vec![ffn_dim, hidden],
863                )),
864                up: WeightMatrix::F32(Tensor::new(
865                    rng.vec(ffn_dim * hidden),
866                    vec![ffn_dim, hidden],
867                )),
868                down: WeightMatrix::F32(Tensor::new(
869                    rng.vec(hidden * ffn_dim),
870                    vec![hidden, ffn_dim],
871                )),
872            };
873            let experts: Vec<ExpertWeights> =
874                (0..n_experts).map(|_| make_expert(&mut rng)).collect();
875            let shared_experts = (0..n_shared).map(|_| make_expert(&mut rng)).collect();
876            let activation_counts = (0..experts.len()).map(|_| AtomicU64::new(0)).collect();
877
878            let moe = MoeWeights {
879                exp_probs_bias: None,
880                ffn_sub_norm: None,
881                down_scale: None,
882                exps_norm: None,
883                parallel_sum_scale: None,
884                parallel: None,
885                dense_bias: None,
886                router: wm(rng.vec(n_experts * hidden), vec![n_experts, hidden]),
887                experts: ExpertBacking::Resident(experts),
888                shared_experts,
889                shared_expert_gate: None,
890                norm_weight: NormOp::Rms(vec![1.0; hidden]),
891                activation_counts,
892                #[cfg(feature = "metal")]
893                packed_q4: None,
894            };
895
896            layers.push(LayerWeights {
897                attn,
898                moe,
899                out_scale: None,
900            });
901        }
902
903        let final_norm = NormOp::Rms(vec![1.0; hidden]);
904        let output_head = wm(rng.vec(vocab_size * hidden), vec![vocab_size, hidden]);
905        let execution_plan = crate::execution_plan::ExecutionPlan::from_config(
906            &config,
907            crate::capability::DecoderFamily::StandardGqa,
908            crate::capability::MemoryKind::KvGqa,
909            crate::execution_plan::ExecutionPlan::probe_metal_caps(),
910        );
911
912        let alibi_slopes = config_alibi_slopes(&config);
913        Decoder {
914            config,
915            embedding,
916            position_embd: None,
917            embedding_norm: NormOp::None,
918            hrm_z_l_init: None,
919            layers,
920            final_norm,
921            output_head,
922            output_bias: None,
923            alibi_slopes,
924            gpu_vram_budget_bytes: None,
925            // Synthetic-weights constructor: no checkpoint, no gpt-oss.
926            gpt_oss: None,
927            // Synthetic-weights constructor: the preset families it
928            // serves all norm before RoPE.
929            qk_norm_after_rope: false,
930            #[cfg(feature = "metal")]
931            metal_attn_kv: std::sync::Mutex::new(None),
932            execution_plan,
933            kv_window: KvWindowPolicy::from_env(),
934            plan_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
935            lora_adapters: Vec::new(),
936        }
937    }
938
939    /// Metal's launch description for `m`, or `None` when no kernel
940    /// serves its storage. Delegates to `crate::metal_launch`, which is
941    /// where both this file and `crate::gdn` read it from.
942    #[cfg(feature = "metal")]
943    fn metal_matvec_launch<'a>(m: &'a WeightMatrix) -> Option<frink_metal::gpu::MatvecLaunch<'a>> {
944        crate::metal_launch::matvec(m)
945    }
946    /// HRM-Text's two streams for a decode of `rows` rows, or `None`
947    /// for every other architecture (`crate::hrm`).
948    ///
949    /// Called by every body that walks layers, so the state is built
950    /// in one place from one rule; a body that forgot it would run the
951    /// stacks on a single stream and answer fluently.
952    pub(crate) fn hrm_streams(&self, embedded: &[f32]) -> Option<crate::hrm::HrmStreams> {
953        let init = self.hrm_z_l_init.as_ref()?;
954        crate::hrm::hrm_schedule(self.config.layer_loops)?;
955        Some(crate::hrm::HrmStreams::new(embedded, init))
956    }
957
958    /// The residual layer `l` reads: `zH + zL` at a stack boundary,
959    /// and whatever the previous layer produced everywhere else.
960    pub(crate) fn hrm_stack_input(
961        &self,
962        streams: Option<&crate::hrm::HrmStreams>,
963        l: usize,
964        hidden: &mut Vec<f32>,
965    ) {
966        let Some(streams) = streams else { return };
967        let Some(loops) = self.config.layer_loops else {
968            return;
969        };
970        if loops.stack_starts_at(l) {
971            *hidden = streams.stack_input();
972        }
973    }
974
975    /// Store a finished stack's output in the stream it writes.
976    ///
977    /// Runs AFTER the FFN body's weightless pass norm
978    /// (`Decoder::apply_loop_norm`), because `hrm-text.cpp:162` norms
979    /// inside `build_stack` and `:186,193` store what it returned.
980    pub(crate) fn hrm_store(
981        &self,
982        streams: Option<&mut crate::hrm::HrmStreams>,
983        l: usize,
984        hidden: &[f32],
985    ) {
986        let Some(streams) = streams else { return };
987        let Some(loops) = self.config.layer_loops else {
988            return;
989        };
990        if let Some(stream) = loops.stream_after(l) {
991            streams.store(stream, hidden);
992        }
993    }
994
995    /// The per-model facts no fused Metal kernel implements, as ONE
996    /// predicate the four Metal eligibility checks share.
997    ///
998    /// Five today. `residual_scale`: every fused launch that folds a
999    /// residual add in -- the dense decode stack, the resident MoE
1000    /// decode stack, and both prefill stacks -- adds the branch output
1001    /// to the stream on device, with no uniform for a multiplier, so a
1002    /// Granite layer served by any of them would be scaled by the host
1003    /// bodies and not by the GPU. `clamp_kqv`: the fused launches apply
1004    /// the QKV bias inside their kernels (`AttnExtras`) and clamp
1005    /// nothing, so a DBRX or clamped-OLMo layer served by any of them
1006    /// would run unclamped projections while the host bodies clamp
1007    /// (`decoder/qkv_bias.rs`). An FFN activation the kernels cannot
1008    /// spell: every fused dense launch takes a `gelu: bool`, so the
1009    /// ungated ReLU-squared FFN (`GluAct::Reglu`, arcee) has no value to
1010    /// pass and every site that asks `fused_kernel_gelu_flag` gets
1011    /// `None` -- this predicate keeps the whole model off the stacks so
1012    /// that a layer is never half-served. And per-layer shapes: the
1013    /// stacks size Q/K/V and the Metal KV plane from ONE head count
1014    /// (`n_heads` is a launch argument, `MetalKvBuffers` one geometry),
1015    /// so a model whose layers disagree (`crate::layer_shapes`, deci /
1016    /// openelm) stays on the host bodies, which read each layer's own.
1017    /// And the per-position attention temperature
1018    /// (`crate::attn_temperature`): no fused launch takes a per-token Q
1019    /// scale, so a Ministral-3 layer served by any of them would attend
1020    /// at temperature 1 while the host bodies step it with position.
1021    /// Either way it is the same weights answering differently
1022    /// depending on which backend took the token, the exact failure
1023    /// `attention_scale` is fenced off for next door.
1024    ///
1025    /// It is one function rather than four spellings because the GPU
1026    /// router's eligibility check has already drifted four ways in this
1027    /// file -- prefill tested three conditions, fused decode two, and
1028    /// the whole-stack decode NONE.
1029    ///
1030    /// `lora_attached` is the fifth fact, and the first that is not a
1031    /// property of the file: a LoRA delta lives inside a
1032    /// `WeightMatrix::Adapted` and is served by that type's methods,
1033    /// while every fused stack takes its weights as raw bytes through
1034    /// `metal_matvec_launch` / `mul_mm_sg_launch` (both answer `None`
1035    /// for an adapted matrix). Fencing the whole model here, too, is
1036    /// what keeps a model with an adapter on one layer from being served
1037    /// half by a stack and half by the host.
1038    ///
1039    /// Named for the backend that had fused stacks first. The CUDA
1040    /// resident prefill layer (`decoder/cuda_prefill.rs`, #259) asks the
1041    /// same question through `fused_view`, because the facts are
1042    /// properties of the model and not of the kernel language.
1043    #[cfg(any(feature = "metal", feature = "cuda"))]
1044    fn metal_can_serve_model(config: &ModelConfig, lora_attached: bool) -> bool {
1045        !lora_attached
1046            && config.residual_scale.is_none()
1047            // `minimax-01.cpp:249,428`: the residual a fused kernel
1048            // adds its branch to is the PRE-NORM's output, which never
1049            // leaves the device in any of these stacks
1050            // (`crate::normed_residual`).
1051            && config.normed_residual_scale.is_none()
1052            && config.clamp_kqv.is_none()
1053            && config.attn_temperature.is_none()
1054            // BitNet's two inner norms (`crate::sub_norms`): no fused
1055            // kernel norms between attention and `wo`, or between the
1056            // activation and `down`.
1057            && !config.block_sub_norms
1058            // Every fused launch bakes the pre-FFN norm over the
1059            // POST-ATTENTION residual into its kernel; a parallel layer
1060            // norms the layer INPUT (`crate::parallel_residual`).
1061            && !config.parallel_residual
1062            // The GPU embedding gather has no add and no stack sees
1063            // `pos` for it (`crate::position_embd`).
1064            && !config.learned_positions
1065            // Every fused launch runs attention alone on its layer; a
1066            // parallel Mamba-2 block (`crate::mamba2`) has no kernel.
1067            && !config.parallel_ssm
1068            // Every fused launch takes ONE sliding window per layer; a
1069            // chunked layer's window is per query
1070            // (`crate::chunked_swa`), and no fused kernel norms Q and K
1071            // without a weight after RoPE (`crate::weightless_qk_norm`).
1072            && !config.swa_chunked
1073            && !config.weightless_qk_norm
1074            // No fused kernel adds a per-key bias (`crate::alibi`).
1075            && config.alibi_max_bias.is_none()
1076            // Every fused launch takes ONE epsilon for the whole layer;
1077            // `muse-glimmer.cpp:63` runs its post-norms at a literal
1078            // 1e-8 and its pre-norms at the model's
1079            // (`crate::norm::POST_NORM_EPS_LITERAL`), so a model whose
1080            // two epsilons differ stays on the host rather than having
1081            // its post-norms silently run at the wrong one.
1082            && config.post_norm_eps() == config.rms_norm_eps
1083            // Every fused launch takes ONE head width for K and V (the
1084            // KV buffers, the attention tile, the `wo` fold); MiMo-V2's
1085            // split widths stay on the host (`crate::kv_head_dims`).
1086            && !config.kv_head_dims_split()
1087            // No fused kernel scales the branch after its `wo` fold
1088            // (`crate::attn_value_scale`).
1089            && config.attn_value_scale.is_none()
1090            // Every fused launch indexes `layers[l]` and `metal_kvs[l]`
1091            // with ONE `l`; a looped model's logical layers outnumber
1092            // its weights (`crate::layer_loops`).
1093            && config.layer_loops.is_none()
1094            // Talkie: an embedding norm no gather kernel applies, a
1095            // second residual no stack carries, a per-head SCALAR Q gain
1096            // and a weightless K norm no `AttnExtras` spells
1097            // (`crate::skip_stream`, `QkNormStyle::PerHeadScalar`).
1098            && !config.skip_stream
1099            && config.qk_norm_style != crate::capability::QkNormStyle::PerHeadScalar
1100            // `model_ffn_act` is `None` for an activation that varies
1101            // by layer (xIELU's parameters), which no fused kernel
1102            // takes; `fused_kernel_gelu_flag` is `None` for one no
1103            // kernel spells.
1104            && config
1105                .model_ffn_act()
1106                .and_then(GluAct::fused_kernel_gelu_flag)
1107                .is_some()
1108            && config.layer_shapes.is_uniform()
1109            // Each fused launch takes ONE rotary width (`MetalRope::
1110            // rot_dim`); a model whose sliding layers rotate a
1111            // different width from its full ones (`rope_dim_swa`,
1112            // Step-3.5 and Laguna-XS.2) stays on the host bodies, which
1113            // read each layer's own through `layer_rope`.
1114            && !config.rope_dim_varies_by_layer()
1115    }
1116
1117    /// The blocked prefill attention, on the GPU when this shape is one
1118    /// the Metal flash kernel serves and on the Rayon host kernel
1119    /// otherwise, which is the ONE place that choice is made.
1120    ///
1121    /// It is reached only by layers the FUSED Metal attention block
1122    /// cannot take -- a gated softmax attention, a V width that differs
1123    /// from K's, a projection with no Metal launch -- so the layer's
1124    /// Q, K and V are on the host either way and there is nothing to
1125    /// fuse; what is left is whether the `n_q x n_kv` score matrix is
1126    /// built by six cores or by the GPU. On Bonsai's 16 attention
1127    /// layers that matrix is the largest single host cost of a prefill
1128    /// (`dot_f32`, `pv_tile` and `qk_tile` are 11178 of a sampled
1129    /// prefill's top-of-stack against 1504 for the next thing).
1130    ///
1131    /// The refusals are the kernel's, not a guess: it takes no sliding
1132    /// window, no ALiBi slopes, no attention sink and one head width
1133    /// for K and V, and a shape it does not serve falls through to the
1134    /// host body below rather than being approximated.
1135    #[allow(clippy::too_many_arguments)]
1136    fn prefill_attention_blocked(
1137        &self,
1138        q_batch: &[f32],
1139        cache_k: &[f32],
1140        cache_v: &[f32],
1141        n_heads: usize,
1142        n_kv_heads: usize,
1143        head_dim: usize,
1144        v_head_dim: usize,
1145        batch_size: usize,
1146        base_seq_len: usize,
1147        softcap: Option<f32>,
1148        window: Option<usize>,
1149    ) -> Vec<f32> {
1150        #[cfg(feature = "metal")]
1151        if window.is_none()
1152            && self.alibi_slopes.is_none()
1153            && v_head_dim == head_dim
1154            && frink_core::weight_matrix::metal_dense_enabled()
1155        {
1156            if let Ok(out) = frink_metal::attn::launch_gqa_prefill_host_ex(
1157                q_batch,
1158                cache_k,
1159                cache_v,
1160                n_heads,
1161                n_kv_heads,
1162                head_dim,
1163                batch_size,
1164                base_seq_len,
1165                softcap,
1166            ) {
1167                return out;
1168            }
1169        }
1170        causal_gqa_attention_prefill_shared_kv_split(
1171            q_batch,
1172            cache_k,
1173            cache_v,
1174            n_heads,
1175            n_kv_heads,
1176            head_dim,
1177            v_head_dim,
1178            batch_size,
1179            base_seq_len,
1180            softcap,
1181            window,
1182            self.alibi_slopes.as_deref(),
1183        )
1184    }
1185
1186    /// True when this layer can use the fused Metal attention block
1187    /// (Norm or NeoX RoPE, quantized projections; QKV bias + QK-norm
1188    /// via [`frink_metal::attn::AttnExtras`]).
1189    #[cfg(feature = "metal")]
1190    fn layer_supports_metal_attn(&self, layer: &LayerWeights) -> bool {
1191        // The backend-neutral questions are `fused_view`'s; what is
1192        // left is whether Metal has a launch for every projection.
1193        self.layer_supports_fused_attn(layer)
1194            && Self::metal_matvec_launch(&layer.attn.q_proj).is_some()
1195            && Self::metal_matvec_launch(&layer.attn.k_proj).is_some()
1196            && Self::metal_matvec_launch(&layer.attn.v_proj).is_some()
1197            && Self::metal_matvec_launch(&layer.attn.o_proj).is_some()
1198    }
1199
1200    /// Layer features only the fused dense stack implements — the
1201    /// per-layer Metal launches would silently skip them (wrong output).
1202    #[cfg(feature = "metal")]
1203    fn layer_needs_metal_stack(&self, layer: &LayerWeights, layer_idx: usize) -> bool {
1204        layer.attn.post_attn_norm.is_some()
1205            || layer.attn.post_ffn_norm.is_some()
1206            || self.config.layer_sliding_window(layer_idx).is_some()
1207            || !self.config.layer_ffn_acts(layer_idx).all_swiglu()
1208            // `Some(base)` when this layer rotates at the model's own
1209            // base, so this ONE comparison covers both stack-only RoPE
1210            // facts: a per-layer base (Gemma-3's sliding layers) and a
1211            // layer that does not rotate at all (`crate::rope_layers`,
1212            // EXAONE-4 32B's full-attention layers). A second predicate
1213            // beside it is exactly the drift this file keeps paying for.
1214            || self.config.layer_rope_theta(layer_idx) != Some(self.config.rope_theta)
1215    }
1216
1217    /// BOTH halves of layer `il`'s RoPE, in the shape the Metal
1218    /// launches take.
1219    ///
1220    /// One conversion, every Metal call site, because
1221    /// `ModelConfig::layer_rope` returning a pair and the launches
1222    /// taking a pair is only worth anything while nothing in between
1223    /// gets to take one half and default the other. That is exactly
1224    /// what the fused stacks used to do: a per-layer `rope_theta` beside
1225    /// ONE `freq_factors` slice for the whole run, which refused
1226    /// Gemma-3 4B/12B/27B off the fused path entirely rather than rope
1227    /// five layers in six at the wrong scale.
1228    #[cfg(feature = "metal")]
1229    fn metal_layer_rope(&self, layer_idx: usize) -> Option<frink_metal::attn::LayerRope<'_>> {
1230        // Exhaustive on purpose: the third field is the rotary width,
1231        // which the Metal kernels take as ONE uniform for every layer
1232        // (`MetalRope::rot_dim`, from `metal_rope`). A layer whose width
1233        // differs from the model's must never reach a launch, and
1234        // `metal_can_serve_model` refuses such a model up front; this
1235        // is the check that the fence held, not a second fence.
1236        let crate::config::LayerRopeParams {
1237            theta,
1238            freq_factors,
1239            rot_dim,
1240        } = self.config.layer_rope(layer_idx)?;
1241        assert_eq!(
1242            rot_dim,
1243            self.config.rope_dim.filter(|w| *w < self.config.head_dim),
1244            "layer {layer_idx} rotates a different width from the model's; \
1245             `metal_can_serve_model` must have refused this model"
1246        );
1247        Some(frink_metal::attn::LayerRope {
1248            theta,
1249            freq_factors,
1250        })
1251    }
1252
1253    /// [`Self::fused_attn_extras`] -- the ONE exhaustive destructure of
1254    /// `AttnWeights`, in `fused_view` -- in Metal's spelling.
1255    #[cfg(feature = "metal")]
1256    fn metal_attn_view<'a>(
1257        &self,
1258        layer: &'a LayerWeights,
1259    ) -> Option<frink_metal::attn::AttnExtras<'a>> {
1260        let fused_view::FusedAttnExtras {
1261            q_bias,
1262            k_bias,
1263            v_bias,
1264            q_norm,
1265            k_norm,
1266        } = Self::fused_attn_extras(layer)?;
1267        Some(frink_metal::attn::AttnExtras {
1268            q_bias,
1269            k_bias,
1270            v_bias,
1271            q_norm,
1272            k_norm,
1273            attn_logit_softcap: self.config.attn_logit_softcap,
1274        })
1275    }
1276
1277    /// Optional QKV bias / QK-norm ops for the Metal attn paths, for a
1278    /// layer [`Self::layer_supports_metal_attn`] has admitted. The
1279    /// `expect` can only fire when a launch site runs without asking
1280    /// that predicate, which is the drift this file's eligibility
1281    /// checks exist to stop, and a panic there beats a silent answer
1282    /// without the gate or the sinks.
1283    #[cfg(feature = "metal")]
1284    fn metal_attn_extras<'a>(&self, layer: &'a LayerWeights) -> frink_metal::attn::AttnExtras<'a> {
1285        self.metal_attn_view(layer)
1286            .expect("layer_supports_metal_attn admits this layer, so its weights have a Metal view")
1287    }
1288
1289    /// GPU expert residency only when Metal attention stays on-device
1290    /// (when the Metal dense+attn path is active). Avoids CPU-attention
1291    /// ↔ GPU-expert activation ping-pong on Metal MoE.
1292    #[cfg(feature = "metal")]
1293    fn expert_residency_plan(&self, use_metal_attn: bool) -> Option<frink_moe::ResidencyPlan> {
1294        if frink_core::metal_dense_enabled()
1295            && frink_metal::attn::metal_attn_enabled()
1296            && !use_metal_attn
1297        {
1298            return None;
1299        }
1300        self.gpu_vram_budget_bytes.map(|b| self.residency_plan(b))
1301    }
1302
1303    #[cfg(not(feature = "metal"))]
1304    fn expert_residency_plan(&self, _use_metal_attn: bool) -> Option<frink_moe::ResidencyPlan> {
1305        self.gpu_vram_budget_bytes.map(|b| self.residency_plan(b))
1306    }
1307
1308    /// Map this checkpoint's RoPE onto the Metal kernel uniforms:
1309    /// pairing convention, rotary width (`n_rot`), and ggml `rope_yarn`'s
1310    /// `mscale`. The last two are what
1311    /// [`Decoder::apply_rope_head_theta`] and
1312    /// [`Decoder::apply_rope_attn_factor`] do on the CPU side, so the
1313    /// two backends stay one graph.
1314    #[cfg(feature = "metal")]
1315    fn metal_rope(&self) -> frink_metal::attn::MetalRope {
1316        use crate::config::RopeLayout;
1317        let layout = match self.config.rope_layout {
1318            RopeLayout::Norm => frink_metal::attn::MetalRopeLayout::Norm,
1319            RopeLayout::Neox => frink_metal::attn::MetalRopeLayout::Neox,
1320        };
1321        frink_metal::attn::MetalRope {
1322            layout,
1323            rot_dim: self
1324                .config
1325                .rope_dim
1326                .filter(|rot| *rot < self.config.head_dim),
1327            attn_factor: self.config.rope_attn_factor,
1328        }
1329    }
1330
1331    /// Dense FFN (single expert) with Metal-capable gate/up/down.
1332    #[cfg(feature = "metal")]
1333    fn layer_supports_metal_dense_ffn(layer: &LayerWeights) -> bool {
1334        Self::is_dense_layer(layer)
1335            // No fused FFN kernel scales after `down` (`crate::weight_scales`)
1336            // or adds a bias anywhere (`crate::proj_bias`).
1337            && layer.moe.down_scale.is_none()
1338            && layer.moe.dense_bias.is_none()
1339            && layer.moe.with_expert(0, |ex| {
1340                Self::metal_matvec_launch(&ex.gate).is_some()
1341                    && Self::metal_matvec_launch(&ex.up).is_some()
1342                    && Self::metal_matvec_launch(&ex.down).is_some()
1343            })
1344    }
1345
1346    /// Dense layer eligible for the one-CB `mul_mm_sg` prefill stack.
1347    /// QKV bias / QK-norm are applied on-GPU via [`AttnExtras`] (same as
1348    /// decode); SWA fit is checked separately.
1349    #[cfg(feature = "metal")]
1350    fn metal_prefill_dense_layer_eligible(
1351        layer: &LayerWeights,
1352        config: &ModelConfig,
1353        lora_attached: bool,
1354    ) -> bool {
1355        Self::fused_prefill_dense_layer_eligible(layer, config, lora_attached)
1356    }
1357
1358    #[cfg(feature = "metal")]
1359    fn metal_prefill_dense_swa_fits(
1360        &self,
1361        layer_idx: usize,
1362        start_pos: usize,
1363        batch_size: usize,
1364    ) -> bool {
1365        match self.config.layer_sliding_window(layer_idx) {
1366            Some(window) => start_pos + batch_size <= window,
1367            None => true,
1368        }
1369    }
1370
1371    /// Routed-expert FFN for the fused prefill stack, or `None` when this
1372    /// layer must keep the host-routed path (`launch_moe_prefill_q4_0`).
1373    ///
1374    /// Note: routing happens on the GPU here, so prefill no longer feeds
1375    /// `record_activations`. Expert hotness for `inspect-plan` comes from
1376    /// decode, which still routes on the host.
1377    #[cfg(feature = "metal")]
1378    fn metal_prefill_moe<'a>(
1379        layer: &'a LayerWeights,
1380        config: &ModelConfig,
1381        lora_attached: bool,
1382    ) -> Option<frink_metal::gpu::PrefillMoeMetal<'a>> {
1383        if !Self::metal_can_serve_model(config, lora_attached)
1384            || Self::is_dense_layer(layer)
1385            || !layer.moe.shared_experts.is_empty()
1386            || !config.model_ffn_act().is_some_and(GluAct::is_swiglu)
1387            // See `gpu_router_matches_host_routing`: the GPU router
1388            // takes router weights and nothing else.
1389            || !Self::gpu_router_matches_host_routing(layer, config)
1390        {
1391            return None;
1392        }
1393        let frink_core::weight_matrix::WeightMatrix::F32(router) = &layer.moe.router else {
1394            return None;
1395        };
1396        let packed = Self::moe_packed_q4(&layer.moe)?;
1397        let moe = frink_metal::gpu::PrefillMoeMetal {
1398            router_w: &router.data,
1399            top_k: config.moe.n_experts_active,
1400            renormalize: config.moe.norm_topk_prob,
1401            packed,
1402        };
1403        moe.is_supported().then_some(moe)
1404    }
1405
1406    /// FFN half of a fused-prefill-stack layer: dense `mul_mm_sg` launches
1407    /// or (MoE) the routed-expert description.
1408    #[cfg(feature = "metal")]
1409    fn metal_prefill_ffn<'a>(
1410        layer: &'a LayerWeights,
1411        config: &ModelConfig,
1412        lora_attached: bool,
1413    ) -> Option<frink_metal::attn::PrefillFfnMetal<'a>> {
1414        if let Some(moe) = Self::metal_prefill_moe(layer, config, lora_attached) {
1415            return Some(frink_metal::attn::PrefillFfnMetal::Moe(moe));
1416        }
1417        if !Self::is_dense_layer(layer) {
1418            return None;
1419        }
1420        let ExpertBacking::Resident(experts) = &layer.moe.experts else {
1421            return None;
1422        };
1423        let ex = experts.first()?;
1424        Some(frink_metal::attn::PrefillFfnMetal::Dense {
1425            gate: ex.gate.mul_mm_sg_launch()?,
1426            up: ex.up.mul_mm_sg_launch()?,
1427            down: ex.down.mul_mm_sg_launch()?,
1428        })
1429    }
1430
1431    /// Length of a consecutive run of Metal prefill-stack layers from
1432    /// `start`, or `None` when fewer than two layers qualify.
1433    #[cfg(feature = "metal")]
1434    fn metal_prefill_dense_stack_run_len(
1435        &self,
1436        start: usize,
1437        start_pos: usize,
1438        batch_size: usize,
1439        kv_caches: &[KvCache],
1440        metal_kvs: Option<&[frink_metal::attn::MetalKvBuffers]>,
1441    ) -> Option<usize> {
1442        // See `layer_supports_metal_attn`: gpt-oss stays on CPU.
1443        if self.gpt_oss.is_some() {
1444            return None;
1445        }
1446        let metal_kvs = metal_kvs?;
1447        let mut run = 0usize;
1448        for li in start..self.layers.len() {
1449            let layer = &self.layers[li];
1450            let cache = &kv_caches[li];
1451            if !self.metal_prefill_dense_swa_fits(li, start_pos, batch_size) {
1452                break;
1453            }
1454            // POSITIONS: compared against `start_pos`, and against
1455            // Metal's own count of the same sequence.
1456            if metal_kvs[li].seq_len != cache.positions() || start_pos != cache.positions() {
1457                break;
1458            }
1459            let ok = layer.attn.q_proj.mul_mm_sg_launch().is_some()
1460                && layer.attn.k_proj.mul_mm_sg_launch().is_some()
1461                && layer.attn.v_proj.mul_mm_sg_launch().is_some()
1462                && layer.attn.o_proj.mul_mm_sg_launch().is_some()
1463                && Self::metal_prefill_ffn(layer, &self.config, self.lora_attached()).is_some();
1464            if !ok {
1465                break;
1466            }
1467            run += 1;
1468        }
1469        (run >= 2).then_some(run)
1470    }
1471
1472    /// Try [`frink_metal::attn::launch_prefill_dense_stack`] for
1473    /// `run_len` layers starting at `start`. Advances host + Metal KV
1474    /// on success.
1475    #[cfg(feature = "metal")]
1476    #[allow(clippy::too_many_arguments)]
1477    fn try_metal_prefill_dense_stack(
1478        &self,
1479        start: usize,
1480        run_len: usize,
1481        hidden_batch: &[f32],
1482        start_pos: usize,
1483        batch_size: usize,
1484        n_heads: usize,
1485        metal_kvs: &mut [frink_metal::attn::MetalKvBuffers],
1486        kv_caches: &mut [KvCache],
1487        host_kv_authoritative: bool,
1488    ) -> Option<Vec<f32>> {
1489        // `None` for an activation no fused kernel implements: the
1490        // stack does not launch, rather than running it as GELU.
1491        let gelu = self
1492            .config
1493            .model_ffn_act()
1494            .and_then(GluAct::fused_kernel_gelu_flag)?;
1495        let mut prefill_layers = Vec::with_capacity(run_len);
1496        for li in start..start + run_len {
1497            let layer = &self.layers[li];
1498            let ffn = Self::metal_prefill_ffn(layer, &self.config, self.lora_attached())?;
1499            if matches!(ffn, frink_metal::attn::PrefillFfnMetal::Dense { .. }) {
1500                layer.moe.record_activations(&[0]);
1501            }
1502            let (q, k, v, o) = (
1503                layer.attn.q_proj.mul_mm_sg_launch()?,
1504                layer.attn.k_proj.mul_mm_sg_launch()?,
1505                layer.attn.v_proj.mul_mm_sg_launch()?,
1506                layer.attn.o_proj.mul_mm_sg_launch()?,
1507            );
1508            prefill_layers.push(frink_metal::attn::PrefillDenseLayerMetal {
1509                // `?`, not a `&`: the kernel applies the RMSNorm itself
1510                // and the post-norm-only topology has no weight to give
1511                // it, so the stack declines and the host body runs. See
1512                // `crate::norm`.
1513                attn_norm_w: layer.attn.norm_weight.rms_weights()?,
1514                ffn_norm_w: layer.moe.norm_weight.rms_weights()?,
1515                q,
1516                k,
1517                v,
1518                o,
1519                ffn,
1520                post_attn_norm: layer.attn.post_attn_norm.as_deref(),
1521                post_ffn_norm: layer.attn.post_ffn_norm.as_deref(),
1522                extras: self.metal_attn_extras(layer),
1523                rope: self.metal_layer_rope(li),
1524                layer_idx: li as u32,
1525            });
1526        }
1527        let kvs = &mut metal_kvs[start..start + run_len];
1528        let h_out = frink_metal::attn::launch_prefill_dense_stack(
1529            hidden_batch,
1530            &prefill_layers,
1531            kvs,
1532            n_heads,
1533            batch_size,
1534            self.metal_rope(),
1535            start_pos,
1536            self.config.rms_norm_eps,
1537            gelu,
1538            self.config.attn_logit_softcap,
1539        )
1540        .ok()?;
1541        for (mkv, cache) in kvs.iter().zip(&mut kv_caches[start..start + run_len]) {
1542            Self::advance_host_kv_after_metal_prefill(
1543                mkv,
1544                cache,
1545                batch_size,
1546                host_kv_authoritative,
1547            );
1548        }
1549        Some(h_out)
1550    }
1551
1552    /// True when a plain top-k softmax over the raw router logits picks
1553    /// the SAME experts with the SAME weights that
1554    /// [`Self::route_for_layer`] would.
1555    ///
1556    /// Every Metal MoE path either routes on the GPU (which implements
1557    /// exactly that plain top-k and takes no other input) or, in
1558    /// `launch_moe_decode_pre`'s case, used to re-implement it on the
1559    /// host. `route_for_layer` has three arms this does not: grouped
1560    /// routing, a per-expert router bias (`exp_probs_bias`), and
1561    /// `expert_weights_scale`. A checkpoint carrying any of them routes
1562    /// to DIFFERENT experts with DIFFERENT weights depending on which
1563    /// backend served the token -- not an error, a different model.
1564    ///
1565    /// One predicate rather than the four hand-copied `.is_none()` lists
1566    /// this used to be, because those lists had already drifted three
1567    /// ways: the prefill sites checked all three conditions, the fused
1568    /// decode layer checked two of them, and the whole-stack decode
1569    /// checked none. Mirror `route_for_layer` arm for arm when either
1570    /// changes.
1571    // Read by the three Metal MoE eligibility predicates, and by
1572    // `the_gpu_router_predicate_admits_only_routing_it_reproduces`. A
1573    // CPU-only build has no Metal path to gate, so it is dead there.
1574    #[cfg_attr(not(feature = "metal"), allow(dead_code))]
1575    fn gpu_router_matches_host_routing(layer: &LayerWeights, config: &ModelConfig) -> bool {
1576        matches!(config.moe.gating, frink_moe::GatingFunction::Softmax)
1577            // Every GPU router reads `normed2`; a model whose router
1578            // reads the raw layer input (`crate::router_input`) would
1579            // route on the wrong tensor at full speed.
1580            && config.router_input == crate::router_input::RouterInput::NormedFfnInput
1581            // Conservative on purpose: `route_for_layer` only takes its
1582            // grouped arm for `n_groups > 1`, but a checkpoint that
1583            // declares the key at all is one this kernel was never
1584            // checked against.
1585            && config.moe.expert_group_count.is_none()
1586            && layer.moe.exp_probs_bias.is_none()
1587            && config.moe.expert_weights_scale == 1.0
1588    }
1589
1590    /// MoE layer eligible for resident Metal decode (attn+router+experts
1591    /// without host residual ping-pong). Requires SwiGLU, no shared
1592    /// experts, Resident expert backing, Metal router/QKV/O, and a
1593    /// routing decision the GPU router reproduces exactly.
1594    #[cfg(feature = "metal")]
1595    fn layer_supports_metal_moe_resident(
1596        layer: &LayerWeights,
1597        config: &ModelConfig,
1598        lora_attached: bool,
1599    ) -> bool {
1600        Self::metal_can_serve_model(config, lora_attached)
1601            && !Self::is_dense_layer(layer)
1602            && layer.moe.shared_experts.is_empty()
1603            && Self::gpu_router_matches_host_routing(layer, config)
1604            && config.model_ffn_act().is_some_and(GluAct::is_swiglu)
1605            // Streamed experts are eligible too. They were excluded
1606            // while the fused launch could not hold all of top-k at
1607            // once; it can now, by materialising each expert into an
1608            // owned view that carries its own pin on the store entry.
1609            && matches!(
1610                layer.moe.experts,
1611                ExpertBacking::Resident(_) | ExpertBacking::Stored { .. }
1612            )
1613            && Self::metal_matvec_launch(&layer.moe.router).is_some()
1614            && Self::metal_matvec_launch(&layer.attn.q_proj).is_some()
1615            && Self::metal_matvec_launch(&layer.attn.k_proj).is_some()
1616            && Self::metal_matvec_launch(&layer.attn.v_proj).is_some()
1617            && Self::metal_matvec_launch(&layer.attn.o_proj).is_some()
1618    }
1619
1620    /// One Metal CB for all top-k routed experts (weighted sum). Returns
1621    /// `None` if any expert lacks a Metal launch (caller falls back).
1622    #[cfg(feature = "metal")]
1623    fn try_metal_moe_topk(
1624        layer: &LayerWeights,
1625        normed2: &[f32],
1626        decision: &frink_moe::RoutingDecision,
1627    ) -> Option<Vec<f32>> {
1628        if decision.expert_ids.is_empty() {
1629            return Some(vec![0f32; normed2.len()]);
1630        }
1631        // Build launches while holding each expert briefly; collect owned
1632        // weight refs via with_expert into temporary MatvecLaunch list.
1633        let mut launches: Vec<frink_metal::gpu::MoeExpertLaunch<'_>> =
1634            Vec::with_capacity(decision.expert_ids.len());
1635        // Lifetime: MatvecLaunch borrows WeightMatrix bytes that live in
1636        // layer.moe for the duration of this call. Collect via a scoped
1637        // approach — we need all launches alive together.
1638        // Use indices + rebuild inside a single with_experts loop.
1639        struct Pending {
1640            eid: usize,
1641            weight: f32,
1642        }
1643        let pending: Vec<Pending> = decision
1644            .expert_ids
1645            .iter()
1646            .zip(decision.weights.iter())
1647            .map(|(&eid, &w)| Pending { eid, weight: w })
1648            .collect();
1649
1650        // Validate all experts have Metal launches first.
1651        for p in &pending {
1652            let ok = layer.moe.with_expert(p.eid, |ex| {
1653                Self::metal_matvec_launch(&ex.gate).is_some()
1654                    && Self::metal_matvec_launch(&ex.up).is_some()
1655                    && Self::metal_matvec_launch(&ex.down).is_some()
1656            });
1657            if !ok {
1658                return None;
1659            }
1660        }
1661
1662        // A `MatvecLaunch` borrows the expert's bytes, so every expert
1663        // in the batch has to stay alive until the command buffer is
1664        // encoded. Resident experts live in a `Vec` and can simply be
1665        // indexed. Streamed experts used to fall back to the CPU here,
1666        // on the reasoning that `with_expert` lends one at a time so
1667        // all of top-k could not be held at once.
1668        //
1669        // That was true of `with_expert` and not of the store beneath
1670        // it: `StoredExpertLayout::materialize` returns an OWNED
1671        // `ExpertWeights` whose `WeightBytes::Shared` clones the
1672        // lease's `Arc`, so each one carries its own pin and the store
1673        // cannot evict it while the view is alive. Materialising all of
1674        // top-k into a vector that outlives the launches is therefore
1675        // sound, and the vector is what holds the pins.
1676        //
1677        // The fallback was not a small loss. Expert streaming is how a
1678        // model larger than memory runs at all, so refusing the fused
1679        // path here meant that turning streaming on silently disabled
1680        // the Metal MoE kernels: exactly the configuration where the
1681        // GPU matters most ran on the CPU instead.
1682        let streamed: Vec<ExpertWeights>;
1683        match &layer.moe.experts {
1684            ExpertBacking::Resident(experts) => {
1685                for p in &pending {
1686                    let ex = &experts[p.eid];
1687                    launches.push(frink_metal::gpu::MoeExpertLaunch {
1688                        gate: Self::metal_matvec_launch(&ex.gate)?,
1689                        up: Self::metal_matvec_launch(&ex.up)?,
1690                        down: Self::metal_matvec_launch(&ex.down)?,
1691                        weight: p.weight,
1692                    });
1693                }
1694            }
1695            ExpertBacking::Stored {
1696                store,
1697                layouts,
1698                layer: layer_idx,
1699            } => {
1700                // Ask for the whole batch before touching any of it, so
1701                // a miss on the last expert cannot evict the first: the
1702                // store is bounded, and top-k reads are what compete
1703                // for it.
1704                let keys: Vec<_> = pending
1705                    .iter()
1706                    .map(|p| frink_core::expert_store::ExpertKey {
1707                        layer: *layer_idx,
1708                        expert: p.eid as u32,
1709                    })
1710                    .collect();
1711                store.prefetch(&keys);
1712
1713                let mut held = Vec::with_capacity(pending.len());
1714                for (p, key) in pending.iter().zip(keys) {
1715                    // A read failure here is not fatal: the CPU path
1716                    // reads the same bytes and will report it. Falling
1717                    // back beats panicking mid-decode.
1718                    let lease = store.acquire(key).ok()?;
1719                    held.push(layouts[p.eid].materialize(&lease));
1720                }
1721                streamed = held;
1722
1723                for (p, ex) in pending.iter().zip(streamed.iter()) {
1724                    launches.push(frink_metal::gpu::MoeExpertLaunch {
1725                        gate: Self::metal_matvec_launch(&ex.gate)?,
1726                        up: Self::metal_matvec_launch(&ex.up)?,
1727                        down: Self::metal_matvec_launch(&ex.down)?,
1728                        weight: p.weight,
1729                    });
1730                }
1731            }
1732        }
1733
1734        match frink_metal::gpu::launch_moe_topk_swiglu(normed2, &launches) {
1735            Ok(out) => Some(out),
1736            Err(e) => {
1737                eprintln!("frink: Metal MoE top-k fuse failed, falling back: {e}");
1738                None
1739            }
1740        }
1741    }
1742
1743    /// Contiguous Q4_0 expert planes for llama-style `mul_mv_id` MoE.
1744    #[cfg(feature = "metal")]
1745    fn moe_packed_q4(moe: &MoeWeights) -> Option<frink_metal::gpu::MoePackedQ4<'_>> {
1746        moe.packed_q4.as_ref().map(MoePackedQ4Planes::view)
1747    }
1748
1749    /// Prefill MoE FFN on Metal: host route over T, then one packed-id CB
1750    /// (`launch_moe_prefill_q4_0`). Shared experts (if any) run as dense
1751    /// batch FFN on the host/GPU path afterwards — not through `mul_mm_id`.
1752    /// Returns FFN outs `[T, H]` or `None`.
1753    #[cfg(feature = "metal")]
1754    fn try_metal_moe_prefill_batch(
1755        layer_idx: usize,
1756        layer: &LayerWeights,
1757        normed2_batch: &[f32],
1758        router_logits_batch: &[f32],
1759        batch_size: usize,
1760        hidden_dim: usize,
1761        config: &ModelConfig,
1762    ) -> Option<Vec<f32>> {
1763        let acts = config.layer_ffn_acts(layer_idx);
1764        if batch_size == 0
1765            || !frink_core::metal_dense_enabled()
1766            || !acts.routed.is_swiglu()
1767            // The GPU router kernels take router weights and nothing
1768            // else: no `exp_probs_b` input, no `expert_weights_scale`
1769            // uniform, no groups. See `gpu_router_matches_host_routing`.
1770            || !Self::gpu_router_matches_host_routing(layer, config)
1771        {
1772            return None;
1773        }
1774        let ExpertBacking::Resident(_) = &layer.moe.experts else {
1775            return None;
1776        };
1777        let packed = Self::moe_packed_q4(&layer.moe)?;
1778        if !frink_metal::gpu::moe_packed_mul_mv_id_supported(
1779            packed.gate_kind,
1780            packed.up_kind,
1781            packed.down_kind,
1782        ) {
1783            return None;
1784        }
1785        let top_k = config.moe.n_experts_active;
1786        if top_k == 0 || top_k > 8 || packed.hidden_rows != hidden_dim {
1787            return None;
1788        }
1789        let n_experts = layer.moe.n_experts().max(1);
1790        let mut ids = Vec::with_capacity(batch_size * top_k);
1791        let mut route = Vec::with_capacity(batch_size * top_k);
1792        for b in 0..batch_size {
1793            let logits = &router_logits_batch[b * n_experts..(b + 1) * n_experts];
1794            let decision = route_top_k(logits, top_k, config.moe.gating, config.moe.norm_topk_prob);
1795            layer.moe.record_activations(&decision.expert_ids);
1796            if decision.expert_ids.len() != top_k {
1797                return None;
1798            }
1799            for (&eid, &w) in decision.expert_ids.iter().zip(decision.weights.iter()) {
1800                ids.push(eid as i32);
1801                route.push(w);
1802            }
1803        }
1804        let mut out = match frink_metal::gpu::launch_moe_prefill_q4_0(
1805            normed2_batch,
1806            batch_size,
1807            &packed,
1808            &ids,
1809            &route,
1810            top_k,
1811        ) {
1812            Ok(out) => out,
1813            Err(e) => {
1814                eprintln!("frink: Metal MoE prefill failed, CPU fallback: {e}");
1815                return None;
1816            }
1817        };
1818        Self::accumulate_shared_experts_batch(
1819            layer,
1820            normed2_batch,
1821            batch_size,
1822            hidden_dim,
1823            &mut out,
1824            // The shared experts are `build_ffn`'s site, not the routed
1825            // experts' the fence above checked; on the host either way.
1826            acts.dense,
1827        );
1828        Some(out)
1829    }
1830
1831    /// Shared expert as dense batch FFN (llama qwen2moe: not through
1832    /// `mul_mat_id`). Optional sigmoid gate scales per token.
1833    fn accumulate_shared_experts_batch(
1834        layer: &LayerWeights,
1835        normed2_batch: &[f32],
1836        batch_size: usize,
1837        hidden_dim: usize,
1838        acc: &mut [f32],
1839        act: GluAct,
1840    ) {
1841        for shex in &layer.moe.shared_experts {
1842            // Prefer one Metal FFN CB (gate∥up→SiLU→down) over three
1843            // `apply_batch` round-trips — Qwen shexp is 4× routed width.
1844            #[cfg(feature = "metal")]
1845            let down = if frink_core::metal_dense_enabled() && batch_size >= 4 {
1846                match (
1847                    shex.gate.mul_mm_sg_launch(),
1848                    shex.up.mul_mm_sg_launch(),
1849                    shex.down.mul_mm_sg_launch(),
1850                    // The launch's last argument selects GELU over
1851                    // SiLU inside the kernel; hardcoding `false` here
1852                    // ran a shared expert as SwiGLU on a GeGLU model,
1853                    // and `!is_swiglu()` would run a third activation
1854                    // as GELU. `None` keeps the host path.
1855                    act.fused_kernel_gelu_flag(),
1856                ) {
1857                    (Some(g), Some(u), Some(d), Some(gelu)) => {
1858                        frink_metal::gpu::launch_dense_ffn_swiglu_batch(
1859                            &g,
1860                            &u,
1861                            &d,
1862                            normed2_batch,
1863                            batch_size,
1864                            gelu,
1865                        )
1866                        .ok()
1867                    }
1868                    _ => None,
1869                }
1870            } else {
1871                None
1872            };
1873            #[cfg(not(feature = "metal"))]
1874            let down: Option<Vec<f32>> = None;
1875            // Without `metal` the binding above is a literal `None`; the
1876            // fallback is the only arm and clippy flags the unwrap.
1877            #[cfg_attr(not(feature = "metal"), allow(clippy::unnecessary_literal_unwrap))]
1878            let down = down.unwrap_or_else(|| {
1879                let ffn_acts = shex.gate.quantize_batch_acts(normed2_batch, batch_size);
1880                // One rotation for the pair when they share a fold
1881                // (`apply_batch_pair_with_acts`); two independent calls
1882                // otherwise, which is every unfolded model.
1883                let (gate, up) = WeightMatrix::apply_batch_pair_with_acts(
1884                    &shex.gate,
1885                    &shex.up,
1886                    normed2_batch,
1887                    batch_size,
1888                    ffn_acts.as_ref(),
1889                );
1890                let activated = act.apply(&gate, &up);
1891                shex.down.apply_batch(&activated, batch_size)
1892            });
1893            if let Some(gate_w) = &layer.moe.shared_expert_gate {
1894                for b in 0..batch_size {
1895                    let x = &normed2_batch[b * hidden_dim..(b + 1) * hidden_dim];
1896                    let logit: f32 = gate_w.iter().zip(x.iter()).map(|(g, v)| g * v).sum();
1897                    let scale = 1.0 / (1.0 + (-logit).exp());
1898                    let out = &down[b * hidden_dim..(b + 1) * hidden_dim];
1899                    let row = &mut acc[b * hidden_dim..(b + 1) * hidden_dim];
1900                    for (a, &o) in row.iter_mut().zip(out.iter()) {
1901                        *a += scale * o;
1902                    }
1903                }
1904            } else {
1905                for (a, &o) in acc.iter_mut().zip(down.iter()) {
1906                    *a += o;
1907                }
1908            }
1909        }
1910    }
1911
1912    /// Phase-2 of resident MoE decode: experts on GPU `x2`, add into GPU `h`.
1913    #[cfg(feature = "metal")]
1914    fn try_metal_moe_experts_resident(
1915        layer: &LayerWeights,
1916        decision: &frink_moe::RoutingDecision,
1917    ) -> Option<()> {
1918        if decision.expert_ids.is_empty() {
1919            return Some(());
1920        }
1921        let pending: Vec<(usize, f32)> = decision
1922            .expert_ids
1923            .iter()
1924            .zip(decision.weights.iter())
1925            .map(|(&eid, &w)| (eid, w))
1926            .collect();
1927        // Bail on the backing BEFORE validating the experts. The
1928        // validation loop below calls `with_expert`, which for
1929        // `Stored` backing acquires a lease and so can read from the
1930        // checkpoint file. Refusing afterwards meant a streamed layer
1931        // paid one read per top-k expert and then threw all of them
1932        // away, before `try_metal_moe_topk` read the same experts
1933        // again. This path stays resident-only for now, but it must
1934        // decline for free.
1935        let ExpertBacking::Resident(experts) = &layer.moe.experts else {
1936            return None;
1937        };
1938        for &(eid, _) in &pending {
1939            let ex = &experts[eid];
1940            if Self::metal_matvec_launch(&ex.gate).is_none()
1941                || Self::metal_matvec_launch(&ex.up).is_none()
1942                || Self::metal_matvec_launch(&ex.down).is_none()
1943            {
1944                return None;
1945            }
1946        }
1947        let mut launches = Vec::with_capacity(pending.len());
1948        for &(eid, weight) in &pending {
1949            let ex = &experts[eid];
1950            launches.push(frink_metal::gpu::MoeExpertLaunch {
1951                gate: Self::metal_matvec_launch(&ex.gate)?,
1952                up: Self::metal_matvec_launch(&ex.up)?,
1953                down: Self::metal_matvec_launch(&ex.down)?,
1954                weight,
1955            });
1956        }
1957        match frink_metal::attn::launch_moe_decode_experts(&launches) {
1958            Ok(()) => Some(()),
1959            Err(e) => {
1960                eprintln!("frink: Metal MoE experts failed, falling back: {e}");
1961                None
1962            }
1963        }
1964    }
1965
1966    /// Advance the host [`KvCache`] over the positions a Metal prefill
1967    /// kernel just wrote to the device.
1968    ///
1969    /// Two ways to do that, and which one is right depends on whether
1970    /// anyone will READ the host rows.
1971    ///
1972    /// The contiguous path never does: Metal stays authoritative from
1973    /// prefill through decode, so [`KvCache::advance_len`]'s zero fill
1974    /// is a placeholder that only has to keep `seq_len` in step for the
1975    /// sync checks, and skipping the download is the whole point.
1976    ///
1977    /// The PAGED path does. `forward_batch_last_paged` scatters these
1978    /// rows into the page store, and a caller that reads placeholders
1979    /// gets a prompt the model never saw -- which is exactly how paged
1980    /// KV on Metal came to answer fluent nonsense while paged-on-CPU
1981    /// and contiguous-on-Metal were each correct. So it asks for the
1982    /// real rows and pays one download per layer, against a gather and
1983    /// a scatter it was already paying.
1984    ///
1985    /// Done here, per layer, immediately after the launch, rather than
1986    /// once at the end: the Metal KV buffers are dropped outright when
1987    /// a later layer's launch fails, and rows nobody downloaded before
1988    /// that are simply gone.
1989    #[cfg(feature = "metal")]
1990    fn advance_host_kv_after_metal_prefill(
1991        mkv: &frink_metal::attn::MetalKvBuffers,
1992        cache: &mut KvCache,
1993        batch_size: usize,
1994        host_kv_authoritative: bool,
1995    ) {
1996        if host_kv_authoritative {
1997            Self::catch_up_host_kv_from_metal(mkv, cache);
1998            debug_assert_eq!(cache.positions(), mkv.seq_len);
1999        } else {
2000            cache
2001                .advance_len(batch_size)
2002                .expect("unbounded/planned KvCache growth is infallible");
2003        }
2004    }
2005
2006    /// Append host [`KvCache`] positions that Metal already holds but host
2007    /// skipped (dense-stack fast path). No-op when `cache.seq_len` is caught up.
2008    #[cfg(feature = "metal")]
2009    fn catch_up_host_kv_from_metal(mkv: &frink_metal::attn::MetalKvBuffers, cache: &mut KvCache) {
2010        // ROWS on both sides: this fills the host buffer with rows
2011        // Metal already holds, and `push` below advances positions with
2012        // them. Neither store evicts, so the two agree; when one learns
2013        // to (#61) this is a place that has to say which it meant.
2014        if cache.rows() >= mkv.seq_len {
2015            return;
2016        }
2017        let start = cache.rows();
2018        let n = mkv.seq_len - start;
2019        let (k, v) = mkv.tokens_host(start, n);
2020        let per = cache.n_kv_heads * cache.head_dim;
2021        for i in 0..n {
2022            let off = i * per;
2023            cache
2024                .push(&k[off..off + per], &v[off..off + per])
2025                .expect("unbounded/planned KvCache growth is infallible");
2026        }
2027    }
2028
2029    /// Pull every layer's Metal-ahead suffix into `kv_caches` (prefix-cache
2030    /// Poison-tolerant lock for the shared Metal KV arena. A panicked
2031    /// holder must not permanently brick every later decode.
2032    #[cfg(feature = "metal")]
2033    fn lock_metal_attn_kv(
2034        mutex: &std::sync::Mutex<Option<Vec<frink_metal::attn::MetalKvBuffers>>>,
2035    ) -> std::sync::MutexGuard<'_, Option<Vec<frink_metal::attn::MetalKvBuffers>>> {
2036        mutex
2037            .lock()
2038            .unwrap_or_else(|poisoned| poisoned.into_inner())
2039    }
2040
2041    /// True once a fused Metal attention launch has allocated this
2042    /// decoder's per-layer Metal KV, i.e. once a token has actually been
2043    /// served by a fused launch rather than by the host bodies.
2044    ///
2045    /// For tests that switch Metal ON and need to know whether the
2046    /// switch reached a kernel: a model the eligibility predicates keep
2047    /// on the host answers `false` forever, and a test asserting logits
2048    /// alone could not tell that from a launch that happened to agree.
2049    #[cfg(feature = "metal")]
2050    pub fn metal_attn_kv_allocated(&self) -> bool {
2051        Self::lock_metal_attn_kv(&self.metal_attn_kv).is_some()
2052    }
2053
2054    /// store, continuous-batch / CPU readers). Safe no-op without Metal KV.
2055    #[cfg(feature = "metal")]
2056    pub fn sync_metal_attn_kv_to_host(&self, kv_caches: &mut [KvCache]) {
2057        assert_eq!(kv_caches.len(), self.config.n_layers);
2058        let guard = Self::lock_metal_attn_kv(&self.metal_attn_kv);
2059        let Some(metal_kvs) = guard.as_ref() else {
2060            return;
2061        };
2062        if metal_kvs.len() != kv_caches.len() {
2063            return;
2064        }
2065        for (mkv, cache) in metal_kvs.iter().zip(kv_caches.iter_mut()) {
2066            Self::catch_up_host_kv_from_metal(mkv, cache);
2067        }
2068    }
2069
2070    /// GQA decode reduction for one token. Uses the CUDA `gqa_decode`
2071    /// kernel when built with `--features cuda` and `FRINK_CUDA_GQA=1`
2072    /// (falling back to the host path on any launch error), else the
2073    /// portable [`causal_gqa_attention`]. With residency enabled the
2074    /// K/V append stays in [`frink_cuda::attn::CudaKvBuffers`] so only
2075    /// Q crosses the bus per call (plus a prefix refresh on append).
2076    #[allow(clippy::too_many_arguments)]
2077    fn gqa_attention(
2078        &self,
2079        layer: usize,
2080        q: &[f32],
2081        k: &[f32],
2082        v: &[f32],
2083        n_heads: usize,
2084        n_kv_heads: usize,
2085        head_dim: usize,
2086        seq_len: usize,
2087    ) -> Vec<f32> {
2088        #[cfg(feature = "cuda")]
2089        {
2090            if cuda_gqa_enabled()
2091                && self.config.layer_shapes.is_uniform()
2092                && self.alibi_slopes.is_none()
2093            {
2094                match frink_cuda::attn::launch_gqa_decode_resident(
2095                    layer, q, k, v, n_heads, n_kv_heads, head_dim, seq_len,
2096                ) {
2097                    Ok(out) => return out,
2098                    Err(e) => {
2099                        eprintln!(
2100                            "frink: CUDA GQA resident decode failed, trying full upload: {e}"
2101                        );
2102                    }
2103                }
2104                match frink_cuda::attn::launch_gqa_decode(
2105                    q, k, v, n_heads, n_kv_heads, head_dim, seq_len,
2106                ) {
2107                    Ok(out) => return out,
2108                    Err(e) => {
2109                        eprintln!("frink: CUDA GQA decode failed, host fallback: {e}");
2110                    }
2111                }
2112            }
2113        }
2114        let _ = layer;
2115        causal_gqa_attention_softcap(
2116            q,
2117            k,
2118            v,
2119            n_heads,
2120            n_kv_heads,
2121            head_dim,
2122            seq_len,
2123            self.config.attn_logit_softcap,
2124        )
2125    }
2126
2127    /// The body of [`Self::forward_token`], already running on a
2128    /// CPU-pool worker. See `entry.rs` for why the split exists.
2129    fn forward_token_on_worker(
2130        &self,
2131        token_id: usize,
2132        pos: usize,
2133        kv_caches: &mut [KvCache],
2134    ) -> Vec<f32> {
2135        // Clear stale dense-stack activation TLS. MoE scratch buffers are
2136        // reused across tokens (re-seeded); cleared after lm_head below.
2137        #[cfg(feature = "metal")]
2138        frink_metal::gpu::clear_resident_activation();
2139
2140        assert_eq!(kv_caches.len(), self.config.n_layers);
2141        // Read only by the Metal arms below: the host layer body moved
2142        // into `attn_block` / `ffn_block_row`, which read the geometry
2143        // off `self.config` themselves.
2144        #[cfg(feature = "metal")]
2145        let hidden_dim = self.config.hidden_dim;
2146        #[cfg(feature = "metal")]
2147        let head_dim = self.config.head_dim;
2148        #[cfg(feature = "metal")]
2149        let n_heads = self.config.n_heads;
2150        #[cfg(feature = "metal")]
2151        let n_kv_heads = self.config.n_kv_heads;
2152
2153        #[cfg(feature = "metal")]
2154        let metal_embd_kind = {
2155            let metal_path = frink_core::metal_dense_enabled()
2156                && frink_metal::attn::metal_attn_enabled()
2157                && self
2158                    .layers
2159                    .iter()
2160                    .all(|l| self.layer_supports_metal_attn(l))
2161                && self.layers.iter().all(Self::layer_supports_metal_dense_ffn);
2162            // Gemma scales the embedding row (`embedding_scale`) — the GPU
2163            // gather has no scale op, so dequant + scale on the host; nor
2164            // has it a norm (`embedding_norm`) or a position table (the
2165            // latter already fenced through `metal_can_serve_model`).
2166            if metal_path
2167                && self.config.embedding_scale.is_none()
2168                && matches!(self.embedding_norm, NormOp::None)
2169            {
2170                Self::metal_matvec_launch(&self.embedding)
2171                    .and_then(|l| frink_metal::embd::EmbdKind::from_fn_name(l.fn_name))
2172            } else {
2173                None
2174            }
2175        };
2176        // `metal_embd_kind` is only `Some` when `embedding_scale` is
2177        // `None` (the GPU gather has no scale op), so the empty vector
2178        // this leaves behind is one the scale would not have touched.
2179        #[cfg(feature = "metal")]
2180        let mut hidden = if metal_embd_kind.is_some() {
2181            Vec::new()
2182        } else {
2183            self.embed_token(token_id, pos)
2184        };
2185        #[cfg(not(feature = "metal"))]
2186        let mut hidden = self.embed_token(token_id, pos);
2187        #[cfg(feature = "cuda")]
2188        if cuda_gqa_enabled()
2189            && self.config.layer_shapes.is_uniform()
2190            && self.alibi_slopes.is_none()
2191        {
2192            // Fixed capacity so ensure_layer_kv does not recreate (and
2193            // wipe) mid-sequence as pos grows. ONE geometry for every
2194            // layer, which is why a per-layer-shape model never seeds it
2195            // (`gqa_attention` skips the resident hook on the same
2196            // predicate).
2197            const CUDA_KV_CAP: usize = 4096;
2198            if let Err(e) = frink_cuda::attn::ensure_layer_kv(
2199                self.layers.len(),
2200                self.config.n_kv_heads,
2201                self.config.head_dim,
2202                CUDA_KV_CAP,
2203            ) {
2204                eprintln!("frink: CUDA KV residency init failed: {e}");
2205            }
2206            if pos == 0 {
2207                frink_cuda::attn::clear_layer_kv();
2208            }
2209        }
2210
2211        #[cfg(feature = "metal")]
2212        let use_metal_attn = frink_core::metal_dense_enabled()
2213            && frink_metal::attn::metal_attn_enabled()
2214            && self
2215                .layers
2216                .iter()
2217                .all(|l| self.layer_supports_metal_attn(l));
2218
2219        #[cfg(not(feature = "metal"))]
2220        let use_metal_attn = false;
2221
2222        let residency = self.expert_residency_plan(use_metal_attn);
2223
2224        #[cfg(feature = "metal")]
2225        let mut metal_kv_guard: Option<
2226            std::sync::MutexGuard<'_, Option<Vec<frink_metal::attn::MetalKvBuffers>>>,
2227        > = if use_metal_attn {
2228            Some(Self::lock_metal_attn_kv(&self.metal_attn_kv))
2229        } else {
2230            None
2231        };
2232
2233        #[cfg(feature = "metal")]
2234        if let Some(guard) = metal_kv_guard.as_mut() {
2235            let need = self.layers.len();
2236            let cap = kv_caches
2237                .iter()
2238                // POSITIONS: sized against `pos`, which is a position.
2239                .map(|c| c.positions().max(pos + 1).saturating_add(256))
2240                .max()
2241                .unwrap_or(512)
2242                .max(512)
2243                .max(pos + 1);
2244            let reset = match guard.as_ref() {
2245                None => true,
2246                Some(v) => {
2247                    if v.len() != need || v.iter().any(|m| m.capacity() < pos + 1) {
2248                        // Growing / reshaping: preserve Metal-ahead tokens on host first.
2249                        if v.len() == need {
2250                            for (m, c) in v.iter().zip(kv_caches.iter_mut()) {
2251                                Self::catch_up_host_kv_from_metal(m, c);
2252                            }
2253                        }
2254                        true
2255                    } else if v.iter().all(|m| m.seq_len == pos) {
2256                        // Metal already holds tokens [0, pos). Host may lag
2257                        // after dense-stack decode — do not re-upload from host.
2258                        false
2259                    } else {
2260                        // Stale Metal (new request / prefix restore): rebuild from host.
2261                        true
2262                    }
2263                }
2264            };
2265            if reset {
2266                let mut bufs = Vec::with_capacity(need);
2267                for _ in 0..need {
2268                    match frink_metal::attn::MetalKvBuffers::with_capacity(
2269                        n_kv_heads, head_dim, cap,
2270                    ) {
2271                        Ok(b) => bufs.push(b),
2272                        Err(_) => {
2273                            **guard = None;
2274                            break;
2275                        }
2276                    }
2277                }
2278                if bufs.len() == need {
2279                    // Sync from host after CPU prefill / prefix restore / capacity grow.
2280                    let mut ok = true;
2281                    for (m, c) in bufs.iter_mut().zip(kv_caches.iter()) {
2282                        // ROWS: this uploads `c.k` / `c.v` themselves, so the
2283                        // count must describe those buffers.
2284                        if c.rows() > 0 && m.upload_from_host(&c.k, &c.v, c.rows()).is_err() {
2285                            ok = false;
2286                            break;
2287                        }
2288                    }
2289                    if ok {
2290                        **guard = Some(bufs);
2291                    } else {
2292                        **guard = None;
2293                    }
2294                } else {
2295                    **guard = None;
2296                }
2297            }
2298        }
2299
2300        #[cfg(feature = "metal")]
2301        let mut metal_stack_done = false;
2302        #[cfg(feature = "metal")]
2303        let mut final_norm_done_in_stack = false;
2304        // OLMoE: all MoE layers in one CB (llama graph style).
2305        #[cfg(feature = "metal")]
2306        if use_metal_attn
2307            && self.layers.iter().enumerate().all(|(i, l)| {
2308                // Both halves are required. `layer_supports_metal_moe_resident`
2309                // answers "is this an MoE layer the GPU router can serve",
2310                // and says nothing about the four features
2311                // `MoeLayerMetal` has no fields for: a per-layer
2312                // `rope_theta`, a sliding `window`, `post_attn_norm`
2313                // and `post_ffn_norm`. `DenseLayerMetal` carries all
2314                // four and `launch_decode_dense_stack` implements
2315                // them; the MoE stack does neither, and nothing here
2316                // refused, so a windowed or sandwich-normed MoE
2317                // checkpoint would have answered as a different
2318                // model with no error.
2319                //
2320                // The per-layer path already pairs these two checks
2321                // (see the `metal_moe_resident` branch in the decode
2322                // loop). Only the whole-stack path was missing it.
2323                // Latent today because OLMoE and Qwen3-MoE ship none
2324                // of the four, which is exactly how `attention_scale`
2325                // stayed latent.
2326                Self::layer_supports_metal_moe_resident(l, &self.config, self.lora_attached())
2327                    && !self.layer_needs_metal_stack(l, i)
2328            })
2329            && !self.layers.iter().all(Self::layer_supports_metal_dense_ffn)
2330        {
2331            if let Some(guard) = metal_kv_guard.as_mut() {
2332                if let Some(metal_kvs) = guard.as_mut() {
2333                    if metal_kvs.iter().all(|m| m.seq_len == pos) {
2334                        let mut moe_layers = Vec::with_capacity(self.layers.len());
2335                        let mut ok = true;
2336                        for layer in &self.layers {
2337                            let ExpertBacking::Resident(_) = &layer.moe.experts else {
2338                                ok = false;
2339                                break;
2340                            };
2341                            let Some(packed) = Self::moe_packed_q4(&layer.moe) else {
2342                                ok = false;
2343                                break;
2344                            };
2345                            let (Some(q), Some(k), Some(v), Some(o), Some(r), Some(an), Some(fnw)) = (
2346                                Self::metal_matvec_launch(&layer.attn.q_proj),
2347                                Self::metal_matvec_launch(&layer.attn.k_proj),
2348                                Self::metal_matvec_launch(&layer.attn.v_proj),
2349                                Self::metal_matvec_launch(&layer.attn.o_proj),
2350                                Self::metal_matvec_launch(&layer.moe.router),
2351                                // The kernel bakes both RMSNorms in; the
2352                                // post-norm-only topology has neither.
2353                                layer.attn.norm_weight.rms_weights(),
2354                                layer.moe.norm_weight.rms_weights(),
2355                            ) else {
2356                                ok = false;
2357                                break;
2358                            };
2359                            moe_layers.push(frink_metal::attn::MoeLayerMetal {
2360                                attn_norm_w: an,
2361                                ffn_norm_w: fnw,
2362                                q,
2363                                k,
2364                                v,
2365                                o,
2366                                router: r,
2367                                packed,
2368                                extras: self.metal_attn_extras(layer),
2369                            });
2370                        }
2371                        if ok {
2372                            // Greedy: fold lm_head+argmax into the stack like the
2373                            // dense path does, and download one u32 instead of a
2374                            // hidden vector.
2375                            let greedy_gpu = frink_metal::attn::metal_greedy_argmax_active();
2376                            let lm_head_gpu_launch = Self::metal_matvec_launch(&self.output_head);
2377                            // One value carries both "lm_head runs in the
2378                            // stack" and "the stack returns an argmax id",
2379                            // so the second cannot drift off the first.
2380                            // See `decoder::lm_head`.
2381                            let folded = FoldedLmHead::permit(
2382                                greedy_gpu,
2383                                &self.final_norm,
2384                                self.output_bias.as_deref(),
2385                                lm_head_gpu_launch,
2386                            );
2387                            // The MoE stack used to be handed
2388                            // `Some(&self.final_norm)` unconditionally,
2389                            // which is unrepresentable now: a model whose
2390                            // final norm has no RMS weights leaves the
2391                            // norm to the host body below, and
2392                            // `final_norm_done_in_stack` is read off the
2393                            // SAME value rather than hardcoded `true`.
2394                            let final_norm_w = self.final_norm.rms_weights();
2395                            let embd_launch = Self::metal_matvec_launch(&self.embedding);
2396                            // Gemma scales embd on host; GPU gather has no scale.
2397                            let embd_gather = if self.config.embedding_scale.is_some() {
2398                                None
2399                            } else {
2400                                match (metal_embd_kind, embd_launch.as_ref()) {
2401                                    (Some(kind), Some(launch)) => {
2402                                        Some(frink_metal::attn::EmbdGatherMetal {
2403                                            kind,
2404                                            weights: launch.weights,
2405                                            rows: launch.rows,
2406                                            row_bytes: launch.row_bytes,
2407                                            n_cols: hidden_dim,
2408                                            token_id,
2409                                        })
2410                                    }
2411                                    _ => None,
2412                                }
2413                            };
2414                            if embd_gather.is_none() && hidden.is_empty() {
2415                                hidden = self.embedding.dequant_row(token_id);
2416                                if let Some(scale) = self.config.embedding_scale {
2417                                    for v in hidden.iter_mut() {
2418                                        *v *= scale;
2419                                    }
2420                                }
2421                            }
2422                            let seed = if embd_gather.is_some() {
2423                                frink_metal::attn::moe_decode_ensure(hidden_dim)
2424                            } else {
2425                                frink_metal::attn::moe_decode_seed(&hidden)
2426                            };
2427                            let hidden_ref: &[f32] =
2428                                if embd_gather.is_some() { &[] } else { &hidden };
2429                            match seed.and_then(|_| {
2430                                // A stack whose layer 0 does not rotate
2431                                // cannot ride this launch; refuse rather
2432                                // than rotate, and the CPU body serves
2433                                // the token.
2434                                let stack_rope = self
2435                                    .metal_layer_rope(0)
2436                                    .ok_or(frink_metal::gpu::MetalError::CommandFailed)?;
2437                                frink_metal::attn::launch_moe_decode_stack(
2438                                    hidden_ref,
2439                                    &moe_layers,
2440                                    metal_kvs,
2441                                    self.config.moe.n_experts_active,
2442                                    self.config.moe.norm_topk_prob,
2443                                    n_heads,
2444                                    self.metal_rope(),
2445                                    // ONE `LayerRope` for the whole
2446                                    // stack, sound because no layer here
2447                                    // `layer_needs_metal_stack`: none
2448                                    // slides, none has a base of its
2449                                    // own, and none is unrotated -- that
2450                                    // predicate now covers all three
2451                                    // through the same accessor, so the
2452                                    // stack-wide answer and the
2453                                    // per-layer one cannot disagree.
2454                                    stack_rope,
2455                                    pos,
2456                                    self.config.rms_norm_eps,
2457                                    final_norm_w,
2458                                    folded.as_ref().map(FoldedLmHead::launch),
2459                                    folded.as_ref().is_some_and(FoldedLmHead::argmax_only),
2460                                    true,
2461                                    embd_gather.as_ref(),
2462                                )
2463                            }) {
2464                                Ok((out, per_layer_ids)) => {
2465                                    for (layer, ids) in self.layers.iter().zip(per_layer_ids.iter())
2466                                    {
2467                                        if !ids.is_empty() {
2468                                            layer.moe.record_activations(ids);
2469                                        }
2470                                    }
2471                                    if let Some(folded) = folded.as_ref() {
2472                                        #[cfg(feature = "metal")]
2473                                        frink_metal::gpu::clear_resident_activation();
2474                                        // Softcaps anything vocabulary-shaped;
2475                                        // passes a 1-element argmax id through.
2476                                        return folded.interpret(
2477                                            out,
2478                                            self.output_head.rows(),
2479                                            self.config.final_logit_softcap,
2480                                            self.config.logit_multiplier,
2481                                        );
2482                                    }
2483                                    hidden = out;
2484                                    final_norm_done_in_stack = final_norm_w.is_some();
2485                                    metal_stack_done = true;
2486                                }
2487                                Err(e) => {
2488                                    eprintln!(
2489                                        "frink: Metal MoE stack failed, per-layer fallback: {e}"
2490                                    );
2491                                    if hidden.is_empty() {
2492                                        hidden = self.embedding.dequant_row(token_id);
2493                                        if let Some(scale) = self.config.embedding_scale {
2494                                            for v in hidden.iter_mut() {
2495                                                *v *= scale;
2496                                            }
2497                                        }
2498                                    }
2499                                }
2500                            }
2501                        }
2502                    }
2503                }
2504            }
2505        }
2506        #[cfg(feature = "metal")]
2507        if !metal_stack_done
2508            && use_metal_attn
2509            && self.layers.iter().all(Self::layer_supports_metal_dense_ffn)
2510        {
2511            if let Some(guard) = metal_kv_guard.as_mut() {
2512                let mut clear_metal_after_stack = false;
2513                if let Some(metal_kvs) = guard.as_mut() {
2514                    let seq_ok = metal_kvs.iter().all(|m| m.seq_len == pos);
2515                    if seq_ok {
2516                        // Build launches only for resident dense experts (Llama path).
2517                        let mut dense_layers = Vec::with_capacity(self.layers.len());
2518                        // The stack's activation uniform, or no stack
2519                        // at all for an activation it cannot spell.
2520                        let stack_gelu = self
2521                            .config
2522                            .model_ffn_act()
2523                            .and_then(GluAct::fused_kernel_gelu_flag);
2524                        let mut ok = stack_gelu.is_some();
2525                        for (li, layer) in self.layers.iter().enumerate() {
2526                            let ExpertBacking::Resident(experts) = &layer.moe.experts else {
2527                                ok = false;
2528                                break;
2529                            };
2530                            let ex = &experts[0];
2531                            let (
2532                                Some(q),
2533                                Some(k),
2534                                Some(v),
2535                                Some(o),
2536                                Some(g),
2537                                Some(u),
2538                                Some(d),
2539                                Some(an),
2540                                Some(fnw),
2541                            ) = (
2542                                Self::metal_matvec_launch(&layer.attn.q_proj),
2543                                Self::metal_matvec_launch(&layer.attn.k_proj),
2544                                Self::metal_matvec_launch(&layer.attn.v_proj),
2545                                Self::metal_matvec_launch(&layer.attn.o_proj),
2546                                Self::metal_matvec_launch(&ex.gate),
2547                                Self::metal_matvec_launch(&ex.up),
2548                                Self::metal_matvec_launch(&ex.down),
2549                                // The kernel bakes both RMSNorms in; the
2550                                // post-norm-only topology has neither.
2551                                layer.attn.norm_weight.rms_weights(),
2552                                layer.moe.norm_weight.rms_weights(),
2553                            )
2554                            else {
2555                                ok = false;
2556                                break;
2557                            };
2558                            dense_layers.push(frink_metal::attn::DenseLayerMetal {
2559                                attn_norm_w: an,
2560                                ffn_norm_w: fnw,
2561                                q,
2562                                k,
2563                                v,
2564                                o,
2565                                gate: g,
2566                                up: u,
2567                                down: d,
2568                                extras: self.metal_attn_extras(layer),
2569                                rope: self.metal_layer_rope(li),
2570                                window: self.config.layer_sliding_window(li),
2571                                post_attn_norm: layer.attn.post_attn_norm.as_deref(),
2572                                post_ffn_norm: layer.attn.post_ffn_norm.as_deref(),
2573                            });
2574                        }
2575                        if ok {
2576                            // Greedy GPU argmax-in-stack (1×u32 download) when
2577                            // generate marked this thread for temperature<=0.
2578                            // Otherwise host lm_head after the hidden download,
2579                            // which measured ~2x the tok/s of a full-vocab one.
2580                            let greedy_gpu = frink_metal::attn::metal_greedy_argmax_active();
2581                            let lm_head_gpu_launch = Self::metal_matvec_launch(&self.output_head);
2582                            // See `decoder::lm_head`: folding lm_head into
2583                            // the stack and the stack returning an argmax id
2584                            // are one decision, held in one value.
2585                            let folded = FoldedLmHead::permit(
2586                                greedy_gpu,
2587                                &self.final_norm,
2588                                self.output_bias.as_deref(),
2589                                lm_head_gpu_launch,
2590                            );
2591                            // Pass final_norm_w when: (1) lm_head runs in stack (folded),
2592                            // OR (2) lm_head will route to GPU after stack (lm_head_gpu_launch
2593                            // but no fold) so we can skip download→reupload via TLS.
2594                            //
2595                            // `rms_weights()` is what makes the second
2596                            // case safe for a non-RMS final norm: it is
2597                            // `None` there, so the stack does not norm
2598                            // and the host body does.
2599                            let final_norm_w = if folded.is_some() || lm_head_gpu_launch.is_some() {
2600                                self.final_norm.rms_weights()
2601                            } else {
2602                                None
2603                            };
2604                            let embd_launch = Self::metal_matvec_launch(&self.embedding);
2605                            // Gemma scales the embedding row on the host
2606                            // (`hidden` already carries sqrt(hidden_dim));
2607                            // the GPU gather has no scale op — skip it.
2608                            let embd_gather = if self.config.embedding_scale.is_some() {
2609                                None
2610                            } else {
2611                                match (metal_embd_kind, embd_launch.as_ref()) {
2612                                    (Some(kind), Some(launch)) => {
2613                                        Some(frink_metal::attn::EmbdGatherMetal {
2614                                            kind,
2615                                            weights: launch.weights,
2616                                            rows: launch.rows,
2617                                            row_bytes: launch.row_bytes,
2618                                            n_cols: hidden_dim,
2619                                            token_id,
2620                                        })
2621                                    }
2622                                    _ => None,
2623                                }
2624                            };
2625                            let hidden_ref: &[f32] =
2626                                if embd_gather.is_some() { &[] } else { &hidden };
2627                            match frink_metal::attn::launch_decode_dense_stack(
2628                                hidden_ref,
2629                                &dense_layers,
2630                                metal_kvs,
2631                                n_heads,
2632                                self.metal_rope(),
2633                                pos,
2634                                self.config.rms_norm_eps,
2635                                final_norm_w,
2636                                folded.as_ref().map(FoldedLmHead::launch),
2637                                folded.as_ref().is_some_and(FoldedLmHead::argmax_only),
2638                                embd_gather.as_ref(),
2639                                stack_gelu.expect("`ok` is false without it"),
2640                            ) {
2641                                Ok(out) => {
2642                                    // Metal KV advanced in-place. Skip host
2643                                    // last_token_host+push — host may lag until
2644                                    // sync_metal_attn_kv_to_host / CPU fallback.
2645                                    // Dense stack has no MoE routing; skip
2646                                    // per-layer hotness atomics on the hot path.
2647                                    if let Some(folded) = folded.as_ref() {
2648                                        // Skip host final_norm/lm_head. Clear TLS.
2649                                        #[cfg(feature = "metal")]
2650                                        frink_metal::gpu::clear_resident_activation();
2651                                        // `interpret` is what keeps
2652                                        // `final_logit_softcap` applied: the id
2653                                        // shape passes through, anything
2654                                        // vocabulary-shaped gets capped.
2655                                        return folded.interpret(
2656                                            out,
2657                                            self.output_head.rows(),
2658                                            self.config.final_logit_softcap,
2659                                            self.config.logit_multiplier,
2660                                        );
2661                                    }
2662                                    // Stack downloaded hidden (possibly normalized if
2663                                    // final_norm_w was Some). Track whether host should
2664                                    // skip final_norm.
2665                                    final_norm_done_in_stack = final_norm_w.is_some();
2666                                    hidden = out;
2667                                    metal_stack_done = true;
2668                                }
2669                                Err(e) => {
2670                                    eprintln!(
2671                                        "frink: Metal dense stack failed, per-layer fallback: {e}"
2672                                    );
2673                                    if hidden.is_empty() {
2674                                        hidden = self.embedding.dequant_row(token_id);
2675                                        if let Some(scale) = self.config.embedding_scale {
2676                                            for v in hidden.iter_mut() {
2677                                                *v *= scale;
2678                                            }
2679                                        }
2680                                    }
2681                                    // Preserve any prior Metal-ahead tokens on host
2682                                    // before dropping the device buffers.
2683                                    for (m, c) in metal_kvs.iter().zip(kv_caches.iter_mut()) {
2684                                        Self::catch_up_host_kv_from_metal(m, c);
2685                                    }
2686                                    clear_metal_after_stack = true;
2687                                }
2688                            }
2689                        }
2690                    }
2691                }
2692                if clear_metal_after_stack {
2693                    **guard = None;
2694                }
2695            }
2696        }
2697
2698        #[cfg(feature = "metal")]
2699        let run_cpu_layers = !metal_stack_done;
2700        #[cfg(not(feature = "metal"))]
2701        let run_cpu_layers = true;
2702
2703        // When true, residual lives in Metal MoE scratch — host `hidden` is stale.
2704        #[cfg(feature = "metal")]
2705        let mut metal_moe_resident = false;
2706
2707        #[cfg(feature = "metal")]
2708        if run_cpu_layers && hidden.is_empty() && !metal_moe_resident {
2709            // GPU embedding gather or a skipped Metal dense stack can leave
2710            // `hidden` empty; CPU fallback must not call rms_norm on it.
2711            hidden = self.embed_token(token_id, pos);
2712        }
2713
2714        // The skip source: the (normed) embedding, before any layer
2715        // touches `hidden` (`crate::skip_stream`). Captured here and
2716        // not at `embed_token`, because the Metal arms above leave
2717        // `hidden` empty on purpose -- and a skip-stream model never
2718        // reaches them (`metal_can_serve_model`).
2719        let skip_rows = self.config.skip_stream.then(|| hidden.clone());
2720        let mut hrm = self.hrm_streams(&hidden);
2721        if run_cpu_layers {
2722            // Indexed rather than iterated, because a RUN of consecutive
2723            // recurrent layers is submitted together
2724            // (`frink_metal::gdn_branch::GdnRun`) and needs several
2725            // caches at once. `fused_through` is how many layers that
2726            // consumed; every `continue` below is a `for`, so it cannot
2727            // spin.
2728            // `fused_through` is only ever written under `metal`; the
2729            // binding is unconditional so the loop reads the same either
2730            // way.
2731            #[allow(unused_mut, unused_assignments)]
2732            let mut fused_through = 0usize;
2733            // The Q/K/V a recurrent run encoded for the attention layer
2734            // that follows it, carried one iteration so that layer does
2735            // not project them again in a submission of its own.
2736            #[cfg(feature = "metal")]
2737            let mut pending_qkv: crate::decoder::fused_recurrent::PendingQkv = None;
2738            #[allow(clippy::needless_range_loop)]
2739            for l in 0..kv_caches.len() {
2740                self.hrm_stack_input(hrm.as_ref(), l, &mut hidden);
2741                if l < fused_through {
2742                    continue;
2743                }
2744                // Consecutive layers the fused launch serves whole pass
2745                // their residual stream to each other on the device and
2746                // wait ONCE, which is the submission count the decode
2747                // gap is made of (`docs/plans/gdn-resident-state.md`).
2748                #[cfg(feature = "metal")]
2749                if let Some(end) =
2750                    self.fused_recurrent_run(l, &mut hidden, kv_caches, &mut pending_qkv)
2751                {
2752                    fused_through = end;
2753                    continue;
2754                }
2755                let cache = &mut kv_caches[l];
2756                let layer = self.layer_for(l);
2757                // --- attention block ---
2758                #[cfg(feature = "metal")]
2759                if metal_moe_resident
2760                    && (!Self::layer_supports_metal_moe_resident(
2761                        layer,
2762                        &self.config,
2763                        self.lora_attached(),
2764                    ) || self.layer_needs_metal_stack(layer, l))
2765                {
2766                    if let Some(h) = frink_metal::attn::moe_decode_take_hidden() {
2767                        hidden = h;
2768                    }
2769                    metal_moe_resident = false;
2770                }
2771
2772                // The router's operand, captured where llama.cpp reads it:
2773                // BEFORE attention (`smallthinker.cpp:111`). A resident
2774                // Metal stack never serves that shape
2775                // (`gpu_router_matches_host_routing`), so `hidden` is not
2776                // stale for the one architecture that reads it here.
2777                let inputs = self.branch_inputs(layer, &hidden, 1);
2778                #[cfg(feature = "metal")]
2779                let normed = if metal_moe_resident {
2780                    // Residual is on-device; host rms_norm would use stale hidden.
2781                    Vec::new()
2782                } else {
2783                    self.pre_norm_residual(&layer.attn.norm_weight, &mut hidden, 1)
2784                };
2785                #[cfg(not(feature = "metal"))]
2786                let normed = self.pre_norm_residual(&layer.attn.norm_weight, &mut hidden, 1);
2787
2788                #[cfg(feature = "metal")]
2789                {
2790                    let mut did_metal_attn = false;
2791                    let mut did_metal_dense = false;
2792                    let mut did_metal_moe = false;
2793                    let mut clear_metal_kv = false;
2794                    if let Some(guard) = metal_kv_guard.as_mut() {
2795                        if let Some(metal_kvs) = guard.as_mut() {
2796                            // Metal-authoritative: host may lag after dense-stack skip.
2797                            // Stack-only features (SWA / sandwich norms / GeGLU /
2798                            // per-layer theta) are NOT encoded by the per-layer
2799                            // launches — those layers must go to CPU here.
2800                            if metal_kvs[l].seq_len == pos
2801                                && !self.layer_needs_metal_stack(layer, l)
2802                            {
2803                                // The two pre-norm weights join the four
2804                                // projection launches in ONE gate, because every
2805                                // fused launch below bakes the RMSNorm into its
2806                                // kernel and there is no weight to bake for the
2807                                // post-norm-only topology (`olmo2` / `exaone4`).
2808                                // `NormOp::rms_weights` returning `None` sends
2809                                // the whole layer to the host body, which reads
2810                                // the raw residual the way llama.cpp does.
2811                                // `metal_layer_rope` joins the six because
2812                                // these four launches all rope
2813                                // unconditionally, and a layer llama.cpp
2814                                // does not rotate must reach the host
2815                                // body instead. It is the same `None`
2816                                // `layer_needs_metal_stack` reads two
2817                                // lines up, from the same accessor, so
2818                                // the two cannot disagree about which
2819                                // layers those are.
2820                                if let (
2821                                    Some(q_l),
2822                                    Some(k_l),
2823                                    Some(v_l),
2824                                    Some(o_l),
2825                                    Some(attn_norm_w),
2826                                    Some(ffn_norm_w),
2827                                    Some(layer_rope),
2828                                ) = (
2829                                    Self::metal_matvec_launch(&layer.attn.q_proj),
2830                                    Self::metal_matvec_launch(&layer.attn.k_proj),
2831                                    Self::metal_matvec_launch(&layer.attn.v_proj),
2832                                    Self::metal_matvec_launch(&layer.attn.o_proj),
2833                                    layer.attn.norm_weight.rms_weights(),
2834                                    layer.moe.norm_weight.rms_weights(),
2835                                    self.metal_layer_rope(l),
2836                                ) {
2837                                    // Full dense layer on one CB when FFN is Metal-capable.
2838                                    if Self::layer_supports_metal_dense_ffn(layer) {
2839                                        let dense_ok = layer.moe.with_expert(0, |ex| {
2840                                        let (Some(g_l), Some(u_l), Some(d_l)) = (
2841                                            Self::metal_matvec_launch(&ex.gate),
2842                                            Self::metal_matvec_launch(&ex.up),
2843                                            Self::metal_matvec_launch(&ex.down),
2844                                        ) else {
2845                                            return false;
2846                                        };
2847                                        match frink_metal::attn::launch_decode_dense_layer(
2848                                            &hidden,
2849                                            attn_norm_w,
2850                                            &q_l,
2851                                            &k_l,
2852                                            &v_l,
2853                                            &o_l,
2854                                            &mut metal_kvs[l],
2855                                            ffn_norm_w,
2856                                            &g_l,
2857                                            &u_l,
2858                                            &d_l,
2859                                            n_heads,
2860                                            self.metal_rope(),
2861                                            layer_rope,
2862                                            pos,
2863                                            self.config.rms_norm_eps,
2864                                            &self.metal_attn_extras(layer),
2865                                        ) {
2866                                            Ok(new_h) => {
2867                                                // Catch up any dense-stack lag + this token.
2868                                                Self::catch_up_host_kv_from_metal(
2869                                                    &metal_kvs[l],
2870                                                    cache,
2871                                                );
2872                                                layer.moe.record_activations(&[0]);
2873                                                hidden = new_h;
2874                                                true
2875                                            }
2876                                            Err(e) => {
2877                                                eprintln!(
2878                                                    "frink: Metal dense layer failed, CPU fallback: {e}"
2879                                                );
2880                                                false
2881                                            }
2882                                        }
2883                                    });
2884                                        if dense_ok {
2885                                            did_metal_dense = true;
2886                                            did_metal_attn = true;
2887                                        } else if metal_kvs[l].seq_len != cache.rows() {
2888                                            // Dense path may have advanced Metal KV before failing.
2889                                            Self::catch_up_host_kv_from_metal(&metal_kvs[l], cache);
2890                                            clear_metal_kv = true;
2891                                        }
2892                                    }
2893
2894                                    // Resident MoE: attn+router on GPU, host top-k only,
2895                                    // then batched experts — no hidden download/upload.
2896                                    if !did_metal_dense
2897                                        && !clear_metal_kv
2898                                        && Self::layer_supports_metal_moe_resident(
2899                                            layer,
2900                                            &self.config,
2901                                            self.lora_attached(),
2902                                        )
2903                                    {
2904                                        if let Some(router_l) =
2905                                            Self::metal_matvec_launch(&layer.moe.router)
2906                                        {
2907                                            let seed_ok = if metal_moe_resident {
2908                                                true
2909                                            } else {
2910                                                match frink_metal::attn::moe_decode_seed(&hidden) {
2911                                                    Ok(()) => {
2912                                                        metal_moe_resident = true;
2913                                                        true
2914                                                    }
2915                                                    Err(e) => {
2916                                                        eprintln!(
2917                                                            "frink: Metal MoE seed failed: {e}"
2918                                                        );
2919                                                        false
2920                                                    }
2921                                                }
2922                                            };
2923                                            if seed_ok {
2924                                                // Prefer one-CB fused path (GPU top-k + packed experts).
2925                                                // See
2926                                                // `layer_supports_metal_moe_resident`:
2927                                                // the fused decode kernel
2928                                                // routes on the GPU and
2929                                                // has no `exp_probs_b` /
2930                                                // `expert_weights_scale`
2931                                                // input either.
2932                                                let fused_ok = match &layer.moe.experts {
2933                                                    ExpertBacking::Resident(_) => {
2934                                                        if let Some(packed) =
2935                                                            Self::moe_packed_q4(&layer.moe)
2936                                                        {
2937                                                            match frink_metal::attn::launch_moe_decode_layer_fused(
2938                                                                attn_norm_w,
2939                                                                &q_l,
2940                                                                &k_l,
2941                                                                &v_l,
2942                                                                &o_l,
2943                                                                &mut metal_kvs[l],
2944                                                                ffn_norm_w,
2945                                                                &router_l,
2946                                                                &packed,
2947                                                                self.config.moe.n_experts_active,
2948                                                                self.config.moe.norm_topk_prob,
2949                                                                n_heads,
2950                                                                self.metal_rope(),
2951                                                                layer_rope,
2952                                                                pos,
2953                                                                self.config.rms_norm_eps,
2954                                                                &self.metal_attn_extras(layer),
2955                                                            ) {
2956                                                                Ok(ids) => {
2957                                                                    layer.moe.record_activations(&ids);
2958                                                                    did_metal_moe = true;
2959                                                                    did_metal_attn = true;
2960                                                                    true
2961                                                                }
2962                                                                Err(e) => {
2963                                                                    eprintln!(
2964                                                                        "frink: Metal MoE fused layer failed: {e}"
2965                                                                    );
2966                                                                    false
2967                                                                }
2968                                                            }
2969                                                        } else {
2970                                                            false
2971                                                        }
2972                                                    }
2973                                                    _ => false,
2974                                                };
2975
2976                                                if !fused_ok {
2977                                                    match frink_metal::attn::launch_moe_decode_pre(
2978                                                        attn_norm_w,
2979                                                        &q_l,
2980                                                        &k_l,
2981                                                        &v_l,
2982                                                        &o_l,
2983                                                        &mut metal_kvs[l],
2984                                                        ffn_norm_w,
2985                                                        &router_l,
2986                                                        n_heads,
2987                                                        self.metal_rope(),
2988                                                        layer_rope,
2989                                                        pos,
2990                                                        self.config.rms_norm_eps,
2991                                                        &self.metal_attn_extras(layer),
2992                                                    ) {
2993                                                        Ok(logits) => {
2994                                                            // Routing happens HERE, on the host,
2995                                                            // so there is no kernel limitation to
2996                                                            // excuse a second router: call the
2997                                                            // one every other host path calls.
2998                                                            let decision = Self::route_for_layer(
2999                                                                layer,
3000                                                                &logits,
3001                                                                &self.config,
3002                                                            );
3003                                                            layer.moe.record_activations(
3004                                                                &decision.expert_ids,
3005                                                            );
3006                                                            if let Some(()) = Self::try_metal_moe_experts_resident(
3007                                                            layer,
3008                                                            &decision,
3009                                                        ) {
3010                                                            did_metal_moe = true;
3011                                                            did_metal_attn = true;
3012                                                        } else if let Some(h) =
3013                                                            frink_metal::attn::moe_decode_take_hidden()
3014                                                        {
3015                                                            hidden = h;
3016                                                            metal_moe_resident = false;
3017                                                            // KV already advanced; finish FFN on host.
3018                                                            let normed2 = self
3019                                                                .pre_norm_residual(
3020                                                                &layer.moe.norm_weight,
3021                                                                &mut hidden,
3022                                                                1,
3023                                                            );
3024                                                            let ffn_out = Self::combine_ffn_outputs_for_position(
3025                                                                l,
3026                                                                layer,
3027                                                                &normed2,
3028                                                                &normed2,
3029                                                                &logits,
3030                                                                &self.config,
3031                                                                hidden_dim,
3032                                                                residency.as_ref().map(|p| p.layer_plan(self.physical_index(l))),
3033                                                            );
3034                                                            residual_add(
3035                                                                &mut hidden,
3036                                                                &ffn_out,
3037                                                                self.config.residual_scale,
3038                                                            );
3039                                                            did_metal_attn = true;
3040                                                            did_metal_moe = true; // skip second FFN
3041                                                        }
3042                                                        }
3043                                                        Err(e) => {
3044                                                            eprintln!(
3045                                                            "frink: Metal MoE pre failed, fallback: {e}"
3046                                                        );
3047                                                            if let Some(h) =
3048                                                            frink_metal::attn::moe_decode_take_hidden()
3049                                                        {
3050                                                            hidden = h;
3051                                                        }
3052                                                            metal_moe_resident = false;
3053                                                            if metal_kvs[l].seq_len != cache.rows()
3054                                                            {
3055                                                                Self::catch_up_host_kv_from_metal(
3056                                                                    &metal_kvs[l],
3057                                                                    cache,
3058                                                                );
3059                                                                clear_metal_kv = true;
3060                                                            }
3061                                                        }
3062                                                    }
3063                                                }
3064                                            }
3065                                        }
3066                                    }
3067
3068                                    if !did_metal_dense && !did_metal_moe && !clear_metal_kv {
3069                                        match frink_metal::attn::launch_decode_attn_block(
3070                                            &normed,
3071                                            &q_l,
3072                                            &k_l,
3073                                            &v_l,
3074                                            &o_l,
3075                                            &mut metal_kvs[l],
3076                                            n_heads,
3077                                            self.metal_rope(),
3078                                            layer_rope,
3079                                            pos,
3080                                            &self.metal_attn_extras(layer),
3081                                            self.config.rms_norm_eps,
3082                                        ) {
3083                                            Ok(projected) => {
3084                                                // Keep Metal KV authoritative — skip per-layer
3085                                                // host catch-up (dense-stack style). Host is
3086                                                // flushed on CPU fallback / prefix sync.
3087                                                residual_add(
3088                                                    &mut hidden,
3089                                                    &projected,
3090                                                    self.config.residual_scale,
3091                                                );
3092                                                did_metal_attn = true;
3093                                            }
3094                                            Err(e) => {
3095                                                eprintln!(
3096                                                "frink: Metal attn block failed, CPU fallback: {e}"
3097                                            );
3098                                                Self::catch_up_host_kv_from_metal(
3099                                                    &metal_kvs[l],
3100                                                    cache,
3101                                                );
3102                                                clear_metal_kv = true;
3103                                            }
3104                                        }
3105                                    }
3106                                }
3107                            } else if metal_kvs[l].seq_len > cache.rows() {
3108                                // Leaving Metal path: host must see full KV for CPU attn.
3109                                Self::catch_up_host_kv_from_metal(&metal_kvs[l], cache);
3110                            }
3111                        }
3112                        if clear_metal_kv {
3113                            **guard = None;
3114                        }
3115                    }
3116                    if did_metal_attn {
3117                        if !did_metal_dense && !did_metal_moe {
3118                            self.ffn_block_row(
3119                                l,
3120                                layer,
3121                                &mut hidden,
3122                                None,
3123                                residency
3124                                    .as_ref()
3125                                    .map(|p| p.layer_plan(self.physical_index(l))),
3126                                inputs,
3127                                skip_rows.as_deref().map(|rows| SkipStream { rows }),
3128                            );
3129                        }
3130                        continue;
3131                    }
3132                }
3133
3134                let oai = self
3135                    .gpt_oss
3136                    .as_ref()
3137                    .map(|g| &g.layers[self.physical_index(l)]);
3138                // A recurrent layer whose whole shape the fused Metal
3139                // launch serves runs END TO END in one submission --
3140                // branch, residual, norm, FFN, residual -- and this
3141                // loop moves to the next layer. Everything it does not
3142                // serve it refuses (`crate::fused_layer`,
3143                // `decoder::fused_recurrent`), and the two bodies below
3144                // run exactly as before.
3145                #[cfg(feature = "metal")]
3146                if let Some(out) =
3147                    self.fused_recurrent_layer(l, layer, &normed, &hidden, &mut cache.recurrent)
3148                {
3149                    hidden = out;
3150                    cache
3151                        .advance_len(1)
3152                        .expect("unbounded/planned KvCache growth is infallible");
3153                    continue;
3154                }
3155                // An ATTENTION layer whose tail the fused launch serves
3156                // hands `wo` over to it, so `wo`, the residual add, the
3157                // FFN norm and the FFN are ONE command buffer instead
3158                // of two (`crate::decoder::fused_attention`). The
3159                // attention itself still runs on the host: the KV lives
3160                // there.
3161                #[cfg(feature = "metal")]
3162                let mut deferred: Option<Vec<f32>> = None;
3163                #[cfg(feature = "metal")]
3164                let mut ready: Option<attn_block::AttnReady> = None;
3165                #[cfg(feature = "metal")]
3166                let residual_in = hidden.clone();
3167                #[cfg(feature = "metal")]
3168                let tail = if self.fused_attention_tail_eligible(l, layer) {
3169                    attn_block::AttnTail::Defer {
3170                        branch: &mut deferred,
3171                        // The attention itself goes to the device when
3172                        // this layer's shape allows it; otherwise only
3173                        // the tail is fused and the host attends.
3174                        ready: self
3175                            .device_attention_shape_ok(l, layer)
3176                            .then_some(&mut ready),
3177                    }
3178                } else {
3179                    attn_block::AttnTail::apply()
3180                };
3181                #[cfg(not(feature = "metal"))]
3182                let tail = attn_block::AttnTail::apply();
3183                // The projections the preceding run encoded for THIS
3184                // layer, if it did: then nothing here projects again.
3185                #[cfg(feature = "metal")]
3186                let precomputed = match pending_qkv.take() {
3187                    Some((at, qkv)) if at == l => Some(qkv),
3188                    // A run encoded a head for a different layer than
3189                    // the one we reached, which only a refusal in
3190                    // between can cause: drop it rather than use it.
3191                    _ => None,
3192                };
3193                #[cfg(not(feature = "metal"))]
3194                let precomputed = None;
3195                let projected = self.attn_block_tail(
3196                    l,
3197                    layer,
3198                    &normed,
3199                    pos,
3200                    KvStep::Decode(&mut *cache),
3201                    tail,
3202                    precomputed,
3203                );
3204                // The attention layer's inputs came back instead of its
3205                // output: run the whole layer on the device, at the head
3206                // of the next recurrent run when there is one.
3207                #[cfg(feature = "metal")]
3208                if let Some(r) = ready {
3209                    let gate = r.gate.as_deref();
3210                    if let Some(end) = self.device_attention_layer_then_run(
3211                        l,
3212                        layer,
3213                        &r.q,
3214                        &r.k,
3215                        &r.v,
3216                        gate,
3217                        &mut hidden,
3218                        kv_caches,
3219                        &mut pending_qkv,
3220                    ) {
3221                        fused_through = end;
3222                        continue;
3223                    }
3224                    let cache = &mut kv_caches[l];
3225                    if let Some(out) = self.device_attention_layer(
3226                        l,
3227                        layer,
3228                        cache,
3229                        &r.q,
3230                        &r.k,
3231                        &r.v,
3232                        gate,
3233                        &residual_in,
3234                    ) {
3235                        cache
3236                            .push(&r.k, &r.v)
3237                            .expect("unbounded/planned KvCache growth is infallible");
3238                        hidden = out;
3239                        continue;
3240                    }
3241                    // Both device shapes refused after the layer had
3242                    // already been prepared, so finish it on the host
3243                    // from the very same inputs.
3244                    let mut attn_out = self.push_and_attend_row(
3245                        KvStep::Decode(&mut kv_caches[l]),
3246                        l,
3247                        layer,
3248                        &r.k,
3249                        &r.v,
3250                        &r.q,
3251                    );
3252                    if let Some(g) = gate {
3253                        crate::attn_gate::apply_interleaved_gate(&mut attn_out, g);
3254                    }
3255                    let projected = self.project_attn_rows(layer, &attn_out, 1);
3256                    residual_add(&mut hidden, &projected, self.config.residual_scale);
3257                    self.ffn_block_row(
3258                        l,
3259                        layer,
3260                        &mut hidden,
3261                        oai,
3262                        residency
3263                            .as_ref()
3264                            .map(|p| p.layer_plan(self.physical_index(l))),
3265                        inputs,
3266                        skip_rows.as_deref().map(|rows| SkipStream { rows }),
3267                    );
3268                    continue;
3269                }
3270                #[cfg(feature = "metal")]
3271                if let Some(branch) = deferred {
3272                    // The tail feeds the recurrent layers after it, so
3273                    // it rides in THEIR command buffer when there are
3274                    // any: one wait for the four layers instead of two.
3275                    if let Some(end) = self.fused_attention_tail_then_run(
3276                        l,
3277                        layer,
3278                        &branch,
3279                        &mut hidden,
3280                        kv_caches,
3281                        &mut pending_qkv,
3282                    ) {
3283                        fused_through = end;
3284                        continue;
3285                    }
3286                    if let Some(out) = self.fused_attention_tail(l, layer, &branch, &hidden) {
3287                        hidden = out;
3288                        continue;
3289                    }
3290                    // The launch refused after the predicate said yes,
3291                    // which only a device error does: finish the layer
3292                    // on the host from the branch it handed back.
3293                    let p = self.project_attn_rows(layer, &branch, 1);
3294                    residual_add(&mut hidden, &p, self.config.residual_scale);
3295                }
3296                if let Some(projected) = projected {
3297                    residual_add(&mut hidden, &projected, self.config.residual_scale);
3298                }
3299                self.ffn_block_row(
3300                    l,
3301                    layer,
3302                    &mut hidden,
3303                    oai,
3304                    residency
3305                        .as_ref()
3306                        .map(|p| p.layer_plan(self.physical_index(l))),
3307                    inputs,
3308                    skip_rows.as_deref().map(|rows| SkipStream { rows }),
3309                );
3310                self.hrm_store(hrm.as_mut(), l, &hidden);
3311            }
3312        } // run_cpu_layers
3313
3314        #[cfg(feature = "metal")]
3315        if metal_moe_resident {
3316            if let Some(h) = frink_metal::attn::moe_decode_take_hidden() {
3317                hidden = h;
3318            }
3319        }
3320
3321        // If Metal stack already ran final_norm, hidden is normalized; else
3322        // normalize here.
3323        #[cfg(feature = "metal")]
3324        let final_normed = if final_norm_done_in_stack {
3325            hidden.clone()
3326        } else {
3327            self.final_norm.apply(&hidden, self.config.rms_norm_eps)
3328        };
3329        #[cfg(not(feature = "metal"))]
3330        let final_normed = self.final_norm.apply(&hidden, self.config.rms_norm_eps);
3331
3332        let logits = self.logits_from_normed(&final_normed);
3333        // Clear dense-stack activation TLS after lm_head (may have consumed it).
3334        // Keep MoE scratch buffers alive across tokens — `moe_decode_seed`
3335        // overwrites `h` each token; clearing here forced full realloc.
3336        #[cfg(feature = "metal")]
3337        frink_metal::gpu::clear_resident_activation();
3338        logits
3339    }
3340
3341    /// The body of [`Self::forward_token_paged`], already running on a
3342    /// CPU-pool worker. See `entry.rs` for why the split exists.
3343    fn forward_token_paged_on_worker(
3344        &self,
3345        token_id: usize,
3346        pos: usize,
3347        kv_caches: &mut [PagedKvCache],
3348        stores: &SharedPagedKv,
3349    ) -> Result<Vec<f32>, PagedStoreExhausted> {
3350        assert_eq!(kv_caches.len(), self.config.n_layers);
3351        assert_eq!(stores.layer_count(), self.config.n_layers);
3352        // All layers advance or none do. Pushing per layer with `?` and
3353        // failing at layer 3 of 4 leaves layers 0..2 holding a position
3354        // the rest do not, and nothing downstream reports it: the next
3355        // step simply attends over a shorter history in the tail
3356        // layers. Reserving one position everywhere first turns that
3357        // into a clean refusal.
3358        //
3359        // The guards span the check AND the push for the same reason
3360        // the prefill path holds them: otherwise another request takes
3361        // the blocks in between.
3362        {
3363            let mut guards = stores.write_all();
3364            for (cache, store) in kv_caches.iter().zip(guards.iter()) {
3365                if cache.blocks_needed_for(store, 1) > store.free_block_count() {
3366                    return Err(PagedStoreExhausted);
3367                }
3368            }
3369            // Reserve by taking the blocks now, so the per-layer pushes
3370            // below cannot fail. `PagedKvCache::reserve` grows the block
3371            // table without advancing `seq_len`, leaving each push a
3372            // pure write into a block this sequence already owns.
3373            for (cache, store) in kv_caches.iter_mut().zip(guards.iter_mut()) {
3374                cache
3375                    .reserve(store, 1)
3376                    .expect("checked against free_block_count under this same guard");
3377            }
3378        }
3379
3380        let mut hidden = self.embed_token(token_id, pos);
3381        let skip_rows = self.config.skip_stream.then(|| hidden.clone());
3382        let mut hrm = self.hrm_streams(&hidden);
3383        let residency = self.gpu_vram_budget_bytes.map(|b| self.residency_plan(b));
3384
3385        for (l, cache) in kv_caches.iter_mut().enumerate() {
3386            self.hrm_stack_input(hrm.as_ref(), l, &mut hidden);
3387            let layer = self.layer_for(l);
3388            // --- attention block ---
3389            let inputs = self.branch_inputs(layer, &hidden, 1);
3390            let normed = self.pre_norm_residual(&layer.attn.norm_weight, &mut hidden, 1);
3391
3392            // The same body the contiguous path runs, with the paged
3393            // backing as its one parameter. It used to be a copy, and
3394            // the copy had silently dropped `attention_scale`,
3395            // `post_attn_norm`, `post_ffn_norm`, gpt-oss's `o_bias` and
3396            // `gpt_oss_ffn` -- five features that each produce a
3397            // plausible distribution rather than an error.
3398            let oai = self
3399                .gpt_oss
3400                .as_ref()
3401                .map(|g| &g.layers[self.physical_index(l)]);
3402            if let Some(projected) = self.attn_block(
3403                l,
3404                layer,
3405                &normed,
3406                pos,
3407                KvStep::Paged {
3408                    cache: &mut *cache,
3409                    stores,
3410                },
3411            ) {
3412                residual_add(&mut hidden, &projected, self.config.residual_scale);
3413            }
3414            self.ffn_block_row(
3415                l,
3416                layer,
3417                &mut hidden,
3418                oai,
3419                residency
3420                    .as_ref()
3421                    .map(|p| p.layer_plan(self.physical_index(l))),
3422                inputs,
3423                skip_rows.as_deref().map(|rows| SkipStream { rows }),
3424            );
3425            self.hrm_store(hrm.as_mut(), l, &hidden);
3426        }
3427
3428        let final_normed = self.final_norm.apply(&hidden, self.config.rms_norm_eps);
3429        Ok(self.logits_from_normed(&final_normed))
3430    }
3431
3432    /// The shared expert store's live counters, when this model runs
3433    /// with store-backed (streamed) routed experts -- `None` for fully
3434    /// resident models. Every store-backed layer shares one store, so
3435    /// the first one found speaks for the whole model.
3436    pub fn expert_store_stats(&self) -> Option<frink_core::expert_store::ExpertStoreStats> {
3437        self.layers.iter().find_map(|l| match &l.moe.experts {
3438            ExpertBacking::Stored { store, .. } => Some(store.stats()),
3439            ExpertBacking::Resident(_) => None,
3440        })
3441    }
3442
3443    /// Builds one global device-residency plan across ALL layers'
3444    /// routed experts against the single configured VRAM budget --
3445    /// every `(layer, expert)` candidate competes in one hotness-
3446    /// ordered pass and the running byte total is shared, so the
3447    /// budget cannot be re-spent per layer (the accounting bug the
3448    /// earlier per-layer `placement_plan` calls had: N layers would
3449    /// plan N x the configured bytes). Dense layers contribute no
3450    /// candidates (their sole expert always runs on CPU). Rebuilt per
3451    /// forward call so it tracks observed hotness; not yet
3452    /// performance-tuned, a disclosed limit.
3453    fn residency_plan(&self, vram_budget_bytes: u64) -> frink_moe::ResidencyPlan {
3454        let mut sizes_per_layer: Vec<Vec<usize>> = Vec::with_capacity(self.layers.len());
3455        let mut counts_per_layer: Vec<Vec<u64>> = Vec::with_capacity(self.layers.len());
3456        let mut any_observed = false;
3457        for layer in &self.layers {
3458            if Self::is_dense_layer(layer) {
3459                sizes_per_layer.push(Vec::new());
3460                counts_per_layer.push(Vec::new());
3461                continue;
3462            }
3463            sizes_per_layer.push(
3464                (0..layer.moe.n_experts())
3465                    .map(|e| layer.moe.expert_bytes(e))
3466                    .collect(),
3467            );
3468            let counts: Vec<u64> = layer
3469                .moe
3470                .activation_counts
3471                .iter()
3472                .map(|c| c.load(Ordering::Relaxed))
3473                .collect();
3474            any_observed |= counts.iter().any(|&c| c > 0);
3475            counts_per_layer.push(counts);
3476        }
3477        PlacementPlan::plan_layers_against_global_budget(
3478            &sizes_per_layer,
3479            any_observed.then_some(counts_per_layer.as_slice()),
3480            vram_budget_bytes,
3481        )
3482    }
3483
3484    /// True if this layer has nothing to route: exactly one expert and
3485    /// no shared experts, the shape every non-MoE model (and every
3486    /// DeepSeek-style "leading dense layer") loads as. Top-1 selection
3487    /// out of one expert always picks it, and its weight is always
3488    /// exactly 1.0 regardless of gating function (softmax over one
3489    /// logit is trivially 1.0; sigmoid-then-renormalize divides the
3490    /// selected score by itself) -- so skipping the router matmul,
3491    /// `route_top_k`'s sort/exp/renormalize work, and
3492    /// `combine_expert_outputs`'s Vec-wrapping for this case is not an
3493    /// approximation, it produces the exact same result.
3494    fn is_dense_layer(layer: &LayerWeights) -> bool {
3495        layer.moe.n_experts() == 1 && layer.moe.shared_experts.is_empty()
3496    }
3497
3498    /// llama.cpp `mul_mat_id` style: shared Q8 act + one flat parallel
3499    /// region over `(slot, row_pair)` for gate∥up (2-row SDOT), then
3500    /// SwiGLU, then per-slot down. Three regions, none nested, all
3501    /// through `frink_core::par` so they follow whichever scheduler
3502    /// `FRINK_CPU_POOL` selected.
3503    fn cpu_moe_topk_parallel_slots(
3504        experts: &[ExpertWeights],
3505        normed2: &[f32],
3506        decision: &frink_moe::RoutingDecision,
3507        hidden_dim: usize,
3508        act: GluAct,
3509    ) -> Option<Vec<(Vec<f32>, f32)>> {
3510        if !frink_core::weight_matrix::cpu_int_dot_for(
3511            frink_core::weight_matrix::IntDotShape::Matvec,
3512        ) || !normed2.len().is_multiple_of(32)
3513        {
3514            return None;
3515        }
3516        let n_slots = decision.expert_ids.len();
3517        if n_slots == 0 {
3518            return Some(Vec::new());
3519        }
3520        for &eid in &decision.expert_ids {
3521            let ex = experts.get(eid)?;
3522            if ex.gate.rows() == 0
3523                || ex.up.rows() != ex.gate.rows()
3524                || ex.down.rows() != hidden_dim
3525                || ex.gate.cols() != normed2.len()
3526                || ex.up.cols() != normed2.len()
3527                || ex.down.cols() != ex.gate.rows()
3528            {
3529                return None;
3530            }
3531            if !matches!(
3532                &ex.gate,
3533                WeightMatrix::Quantized {
3534                    kind: frink_core::QuantKind::Q4_0 | frink_core::QuantKind::Q8_0,
3535                    ..
3536                }
3537            ) || !matches!(
3538                &ex.up,
3539                WeightMatrix::Quantized {
3540                    kind: frink_core::QuantKind::Q4_0 | frink_core::QuantKind::Q8_0,
3541                    ..
3542                }
3543            ) {
3544                return None;
3545            }
3546        }
3547        let ffn_rows = experts[decision.expert_ids[0]].gate.rows();
3548        // Even ffn_rows: par_chunks_mut(2) never crosses a slot boundary.
3549        if !ffn_rows.is_multiple_of(2) {
3550            return None;
3551        }
3552        let q8 = frink_quant::quantize_activations_q8(normed2);
3553        let eids = &decision.expert_ids;
3554        let mut gate = vec![0f32; n_slots * ffn_rows];
3555        let mut up = vec![0f32; n_slots * ffn_rows];
3556        frink_core::par::chunks_mut2(&mut gate, &mut up, 2, 1, |p, gc, uc| {
3557            let row0 = p * 2;
3558            let slot = row0 / ffn_rows;
3559            let r = row0 % ffn_rows;
3560            let ex = &experts[eids[slot]];
3561            if let (Some((g0, g1)), Some((u0, u1))) = (
3562                ex.gate.dot_pair_cpu_q8(r, &q8),
3563                ex.up.dot_pair_cpu_q8(r, &q8),
3564            ) {
3565                gc[0] = g0;
3566                gc[1] = g1;
3567                uc[0] = u0;
3568                uc[1] = u1;
3569            } else {
3570                gc[0] = ex.gate.dot_row_cpu_q8(r, &q8).unwrap_or(0.0);
3571                gc[1] = ex.gate.dot_row_cpu_q8(r + 1, &q8).unwrap_or(0.0);
3572                uc[0] = ex.up.dot_row_cpu_q8(r, &q8).unwrap_or(0.0);
3573                uc[1] = ex.up.dot_row_cpu_q8(r + 1, &q8).unwrap_or(0.0);
3574            }
3575        });
3576        let mut activated = vec![0f32; n_slots * ffn_rows];
3577        // The combine here is always parallel (decode's `n_slots *
3578        // ffn_rows` sits under `frink_core::matmul`'s own fork
3579        // threshold), which is why this does not just call `act.apply`.
3580        // `GluAct::combine` is that function one element at a time --
3581        // this used to match the variant here and multiply `f(gate) *
3582        // up` itself, a second spelling of the activation that a
3583        // parameterised variant (xIELU reads `up` alone) could not fit.
3584        frink_core::par::items_mut(&mut activated, 1, |idx, a| {
3585            *a = act.combine(gate[idx], up[idx])
3586        });
3587        let mut outs: Vec<(Vec<f32>, f32)> = decision
3588            .weights
3589            .iter()
3590            .map(|&w| (vec![0f32; hidden_dim], w))
3591            .collect();
3592        frink_core::par::items_mut(&mut outs, 1, |slot, (out, _)| {
3593            let ex = &experts[eids[slot]];
3594            let act_slot = &activated[slot * ffn_rows..(slot + 1) * ffn_rows];
3595            if act_slot.len().is_multiple_of(32) {
3596                let down_q8 = frink_quant::quantize_activations_q8(act_slot);
3597                if let Some(d) = ex.down.apply_cpu_q8(&down_q8) {
3598                    *out = d;
3599                    return;
3600                }
3601            }
3602            *out = ex.down.apply(act_slot);
3603        });
3604        Some(outs)
3605    }
3606
3607    /// Fallback: serial top-k with shared Q8 act (pre-mul_mat_id path).
3608    ///
3609    /// `weight_before_ffn` (`crate::routed_weight_site`): each slot
3610    /// reads its own scaled input and the shared activation is not
3611    /// built.
3612    fn cpu_moe_serial_experts(
3613        layer: &LayerWeights,
3614        normed2: &[f32],
3615        decision: &frink_moe::RoutingDecision,
3616        plan: Option<&PlacementPlan>,
3617        act: GluAct,
3618        weight_before_ffn: bool,
3619    ) -> Vec<(Vec<f32>, f32)> {
3620        let shared_act = if !weight_before_ffn
3621            && frink_core::weight_matrix::cpu_int_dot_for(
3622                frink_core::weight_matrix::IntDotShape::Matvec,
3623            )
3624            && normed2.len().is_multiple_of(32)
3625            && plan
3626                .map(|p| {
3627                    decision
3628                        .expert_ids
3629                        .iter()
3630                        .all(|&eid| matches!(p.placement_for(eid), ExpertPlacement::Cpu))
3631                })
3632                .unwrap_or(true)
3633        {
3634            Some(frink_quant::quantize_activations_q8(normed2))
3635        } else {
3636            None
3637        };
3638        decision
3639            .expert_ids
3640            .iter()
3641            .zip(decision.weights.iter())
3642            .map(|(&eid, &w)| {
3643                let placement = plan
3644                    .map(|p| p.placement_for(eid))
3645                    .unwrap_or(ExpertPlacement::Cpu);
3646                let (input, w) =
3647                    crate::routed_weight_site::routed_slot(normed2, w, weight_before_ffn);
3648                let out = layer.moe.with_expert(eid, |ex| {
3649                    if let Some(ref q8) = shared_act {
3650                        if let (Some(gate), Some(up)) =
3651                            (ex.gate.apply_cpu_q8(q8), ex.up.apply_cpu_q8(q8))
3652                        {
3653                            let activated = act.apply(&gate, &up);
3654                            return ex.down.apply(&activated);
3655                        }
3656                    }
3657                    run_expert_placed(&input, ex, placement, act)
3658                });
3659                (out, w)
3660            })
3661            .collect()
3662    }
3663
3664    /// Runs one position's normalized hidden state through this
3665    /// layer's MoE FFN block, given already-computed router logits for
3666    /// that position, returning the combined output to add back into
3667    /// the residual stream. Shared by `forward_token` (router computed
3668    /// via a single `apply` call, since there's only one position) and
3669    /// `forward_batch`'s per-position loop (router computed via one
3670    /// batched `apply_batch` call up front, sliced per position here --
3671    /// see `forward_batch`'s doc comment for why that batching matters
3672    /// and must not be lost by calling this per position instead).
3673    /// `gpu_vram_budget_bytes`: see `Decoder::gpu_vram_budget_bytes`'s
3674    /// doc comment -- `None` dispatches every routed expert through
3675    /// `run_expert_placed` with `ExpertPlacement::Cpu`, which is
3676    /// exactly `run_expert`'s own behavior, so this is a real
3677    /// zero-behavior-change default, not just "probably fine."
3678    /// One token's routing decision for one MoE layer.
3679    ///
3680    /// Three shapes, in the order llama.cpp's `build_moe_ffn` decides
3681    /// them: grouped selection when the checkpoint declares expert
3682    /// groups; the biased/scaled port when the layer carries
3683    /// `exp_probs_b` or the model carries a non-unit
3684    /// `expert_weights_scale`; otherwise the plain top-k this decoder has
3685    /// always used. The last arm is kept rather than folded into
3686    /// `route_top_k_biased` so that every checkpoint without those two
3687    /// features routes through byte-identical code to before.
3688    ///
3689    /// `exp_probs_b` together with expert groups is refused at load
3690    /// (`loader.rs`), so that combination cannot reach here.
3691    fn route_for_layer(
3692        layer: &LayerWeights,
3693        router_logits: &[f32],
3694        config: &ModelConfig,
3695    ) -> frink_moe::RoutingDecision {
3696        match (
3697            config.moe.expert_group_count,
3698            config.moe.expert_group_used_count,
3699        ) {
3700            (Some(n_groups), Some(k_per_group)) if n_groups > 1 && k_per_group > 0 => {
3701                frink_moe::route_top_k_grouped(
3702                    router_logits,
3703                    n_groups,
3704                    k_per_group,
3705                    config.moe.n_experts_active,
3706                    config.moe.gating,
3707                    config.moe.norm_topk_prob,
3708                )
3709            }
3710            _ if layer.moe.exp_probs_bias.is_some() || config.moe.expert_weights_scale != 1.0 => {
3711                frink_moe::route_top_k_biased(
3712                    router_logits,
3713                    layer.moe.exp_probs_bias.as_deref(),
3714                    config.moe.n_experts_active,
3715                    config.moe.gating,
3716                    config.moe.norm_topk_prob,
3717                    config.moe.expert_weights_scale,
3718                )
3719            }
3720            _ => route_top_k(
3721                router_logits,
3722                config.moe.n_experts_active,
3723                config.moe.gating,
3724                config.moe.norm_topk_prob,
3725            ),
3726        }
3727    }
3728
3729    /// `normed2` is what the DENSE half (the shared experts) reads;
3730    /// `routed_input` is what the routed experts read -- the same slice
3731    /// for every architecture but Arctic, whose routed branch reads
3732    /// `ffn_norm_exps(inpSA)` (`crate::router_input`). Two arguments
3733    /// rather than one so a caller cannot hand the experts the wrong
3734    /// vector without saying so.
3735    #[allow(clippy::too_many_arguments)]
3736    fn combine_ffn_outputs_for_position(
3737        layer_idx: usize,
3738        layer: &LayerWeights,
3739        normed2: &[f32],
3740        routed_input: &[f32],
3741        router_logits: &[f32],
3742        config: &ModelConfig,
3743        hidden_dim: usize,
3744        plan: Option<&PlacementPlan>,
3745    ) -> Vec<f32> {
3746        let decision = Self::route_for_layer(layer, router_logits, config);
3747        let acts = config.layer_ffn_acts(layer_idx);
3748        let act = acts.routed;
3749        layer.moe.record_activations(&decision.expert_ids);
3750        // Best-effort warm of the routed experts for this layer into
3751        // the store cache (SSD streaming overlap). Resident-backed
3752        // layers skip this entirely.
3753        if let ExpertBacking::Stored {
3754            store,
3755            layer: layer_id,
3756            ..
3757        } = &layer.moe.experts
3758        {
3759            let keys: Vec<frink_core::expert_store::ExpertKey> = decision
3760                .expert_ids
3761                .iter()
3762                .map(|&eid| frink_core::expert_store::ExpertKey {
3763                    layer: *layer_id,
3764                    expert: eid as u32,
3765                })
3766                .collect();
3767            store.prefetch(&keys);
3768        }
3769
3770        // Metal: fuse all top-k experts into one CB (one wait) when every
3771        // routed expert has Metal matvec launches. Shared experts (rare
3772        // for OLMoE) still run on the host after.
3773        // `launch_moe_topk_swiglu` is SwiGLU-only, so a GeGLU MoE layer
3774        // keeps the host path rather than taking a kernel that computes
3775        // a different activation.
3776        #[cfg(feature = "metal")]
3777        if frink_core::metal_dense_enabled()
3778            && act.is_swiglu()
3779            && layer.moe.shared_experts.is_empty()
3780        {
3781            if let Some(fused) = Self::try_metal_moe_topk(layer, routed_input, &decision) {
3782                return fused;
3783            }
3784        }
3785
3786        let routed_outputs: Vec<(Vec<f32>, f32)> = {
3787            // llama.cpp mul_mat_id: one shared Q8 act + flat (slot,row)
3788            // parallel over all top-k experts (not serial expert loops each
3789            // with their own rayon fork-join).
3790            let all_cpu = plan
3791                .map(|p| {
3792                    decision
3793                        .expert_ids
3794                        .iter()
3795                        .all(|&eid| matches!(p.placement_for(eid), ExpertPlacement::Cpu))
3796                })
3797                .unwrap_or(true);
3798            // The parallel-slot kernel quantises ONE input for every
3799            // slot; a model that weights the input per slot
3800            // (`crate::routed_weight_site`) takes the serial site.
3801            let before = config.moe.routed_weight_before_ffn;
3802            if let (true, false, ExpertBacking::Resident(experts)) =
3803                (all_cpu, before, &layer.moe.experts)
3804            {
3805                if let Some(outs) = Self::cpu_moe_topk_parallel_slots(
3806                    experts,
3807                    routed_input,
3808                    &decision,
3809                    hidden_dim,
3810                    act,
3811                ) {
3812                    outs
3813                } else {
3814                    Self::cpu_moe_serial_experts(layer, routed_input, &decision, plan, act, before)
3815                }
3816            } else {
3817                Self::cpu_moe_serial_experts(layer, routed_input, &decision, plan, act, before)
3818            }
3819        };
3820        // Shared experts fire on every token regardless of routing, so
3821        // there's no offload decision to make for them the way there
3822        // is for routed experts -- always CPU, matching `run_expert`.
3823        let mut shared_outputs: Vec<Vec<f32>> = layer
3824            .moe
3825            .shared_experts
3826            .iter()
3827            .map(|e| run_expert(normed2, e, acts.dense))
3828            .collect();
3829        // Qwen2-MoE-specific: see `MoeWeights::shared_expert_gate`'s doc
3830        // comment. Scaling here (before `combine_expert_outputs`, which
3831        // is architecture-agnostic and knows nothing about this gate)
3832        // keeps the gate a decoder-level detail, not a frink-moe API
3833        // change.
3834        if let Some(gate) = &layer.moe.shared_expert_gate {
3835            let gate_logit: f32 = gate.iter().zip(normed2.iter()).map(|(g, x)| g * x).sum();
3836            let gate_value = 1.0 / (1.0 + (-gate_logit).exp());
3837            for out in shared_outputs.iter_mut() {
3838                for x in out.iter_mut() {
3839                    *x *= gate_value;
3840                }
3841            }
3842        }
3843
3844        combine_expert_outputs(&routed_outputs, &shared_outputs, hidden_dim)
3845    }
3846
3847    /// The dense FFN for a whole batch of positions in three batched
3848    /// matmuls (gate, up, down) instead of three per position.
3849    ///
3850    /// This is the counterpart of what `forward_hidden_batch` already
3851    /// did for Q/K/V and the router, and it is where a dense model's
3852    /// prefill time actually goes: `WeightMatrix::apply_batch` reads
3853    /// each weight row once and dots it against every position, rather
3854    /// than re-reading the whole FFN for each one.
3855    ///
3856    /// `None` for anything that is not a plain dense layer -- MoE
3857    /// routing is per position by construction, so those keep the
3858    /// sequential path.
3859    ///
3860    /// On a GPU backend the per-position alternative is one *fused*
3861    /// gate+up+SiLU+down launch (`apply_gpu_dense_ffn_swiglu`), so this
3862    /// used to be gated off there: three separate batched launches lost
3863    /// to it while `apply_batch` was still a batched *matvec*.
3864    ///
3865    /// That stopped being true once the simdgroup GEMM landed, and the
3866    /// old gate turned out to be the dominant cost of Metal prefill --
3867    /// a 512-token prompt ran the FFN one position at a time, 512 x
3868    /// n_layers fused launches, which a profile put at 90% of prefill
3869    /// while the GEMM it bypassed accounted for 21%.
3870    ///
3871    /// Decode (`batch_size == 1`) still takes the fused per-position
3872    /// launch, which is the right shape there.
3873    fn dense_ffn_batch(
3874        layer_idx: usize,
3875        layer: &LayerWeights,
3876        normed2_batch: &[f32],
3877        batch_size: usize,
3878        config: &ModelConfig,
3879    ) -> Option<Vec<f32>> {
3880        let act = config.layer_ffn_acts(layer_idx).dense;
3881        // Match the GPU `mul_mm` threshold: below it the per-call launch
3882        // overhead outweighs the weight reuse.
3883        if !Self::is_dense_layer(layer) || batch_size < 4 {
3884            return None;
3885        }
3886        // On a GPU backend this only wins when the weights have a real
3887        // batched GEMM; otherwise `apply_batch` is a batched matvec and
3888        // loses to the fused per-position launch.
3889        #[cfg(any(feature = "metal", feature = "cuda"))]
3890        {
3891            #[cfg(feature = "metal")]
3892            let gpu_dense = frink_core::weight_matrix::metal_dense_enabled();
3893            #[cfg(not(feature = "metal"))]
3894            let gpu_dense = false;
3895            #[cfg(feature = "cuda")]
3896            let gpu_dense = gpu_dense || frink_core::weight_matrix::cuda_dense_enabled();
3897            if gpu_dense {
3898                let all_gemm = layer.moe.with_expert(0, |ex| {
3899                    ex.gate.prefers_gpu_batch()
3900                        && ex.up.prefers_gpu_batch()
3901                        && ex.down.prefers_gpu_batch()
3902                });
3903                if !all_gemm {
3904                    return None;
3905                }
3906            }
3907        }
3908        layer.moe.record_activations(&[0]);
3909        // One command buffer for the whole FFN when every matrix has a
3910        // simdgroup GEMM: gate and up feed the activation and the down
3911        // projection without the intermediates ever touching the host.
3912        // Three separate launches cost three round trips per layer plus
3913        // four copies of a `batch x ffn_dim` tensor. Not for a layer
3914        // with a norm between the activation and `down`
3915        // (`ffn_sub_norm`): the kernel has no such site.
3916        // ...nor a bias at any of its three sites (`crate::proj_bias`).
3917        #[cfg(feature = "metal")]
3918        if let (true, Some(gelu), None, None) = (
3919            frink_core::weight_matrix::metal_dense_enabled(),
3920            act.fused_kernel_gelu_flag(),
3921            layer.moe.ffn_sub_norm.as_ref(),
3922            layer.moe.dense_bias.as_ref(),
3923        ) {
3924            let fused = layer.moe.with_expert(0, |ex| {
3925                let (g, u, d) = (
3926                    ex.gate.mul_mm_sg_launch()?,
3927                    ex.up.mul_mm_sg_launch()?,
3928                    ex.down.mul_mm_sg_launch()?,
3929                );
3930                frink_metal::gpu::launch_dense_ffn_swiglu_batch(
3931                    &g,
3932                    &u,
3933                    &d,
3934                    normed2_batch,
3935                    batch_size,
3936                    gelu,
3937                )
3938                .ok()
3939            });
3940            if let Some(out) = fused {
3941                return Some(out);
3942            }
3943        }
3944        Some(layer.moe.with_expert(0, |ex| {
3945            let ffn_acts = ex.gate.quantize_batch_acts(normed2_batch, batch_size);
3946            let (mut gate, mut up) = WeightMatrix::apply_batch_pair_with_acts(
3947                &ex.gate,
3948                &ex.up,
3949                normed2_batch,
3950                batch_size,
3951                ffn_acts.as_ref(),
3952            );
3953            // `build_ffn`: `up_b` / `gate_b` before the activation
3954            // (`crate::proj_bias`). On an ungated layer `gate` is an
3955            // alias of `up` and only `up_b` exists; `act.apply` reads
3956            // `up` alone for it, so the gate copy going unbiased is not
3957            // read.
3958            if let Some(bias) = &layer.moe.dense_bias {
3959                bias.add_pre_activation(&mut gate, &mut up, batch_size);
3960            }
3961            let mut activated = act.apply(&gate, &up);
3962            // bitnet.cpp:135-140, per row, on the same vector the row
3963            // body norms in `frink_moe::run_expert_sub_normed`.
3964            if let Some(w) = &layer.moe.ffn_sub_norm {
3965                let width = w.len();
3966                activated = activated
3967                    .chunks(width)
3968                    .flat_map(|row| rms_norm(row, w, config.rms_norm_eps))
3969                    .collect();
3970            }
3971            let mut down = ex.down.apply_batch(&activated, batch_size);
3972            if let Some(bias) = &layer.moe.dense_bias {
3973                bias.add_post_down(&mut down, batch_size);
3974            }
3975            down
3976        }))
3977    }
3978
3979    /// CPU MoE prefill: bucket tokens by expert, then one
3980    /// `apply_batch` per expert with tokens instead of per-token
3981    /// `combine_ffn_outputs_for_position`. Shared experts append via
3982    /// [`Self::accumulate_shared_experts_batch`]. `None` when gates fail
3983    /// (small batch, dense, Metal preferred, non-resident, or any
3984    /// GPU-placed expert). Both gated activations are served here --
3985    /// the combine goes through [`GluAct`], so GeGLU no longer falls out
3986    /// to the per-position path.
3987    /// `routed_batch` is what the experts read, `normed2_batch` what the
3988    /// shared experts read: the same rows for every architecture but
3989    /// Arctic (see `combine_ffn_outputs_for_position`).
3990    #[allow(clippy::too_many_arguments)]
3991    fn moe_ffn_batch(
3992        layer_idx: usize,
3993        layer: &LayerWeights,
3994        normed2_batch: &[f32],
3995        routed_batch: &[f32],
3996        router_logits_batch: &[f32],
3997        batch_size: usize,
3998        config: &ModelConfig,
3999        plan: Option<&PlacementPlan>,
4000    ) -> Option<Vec<f32>> {
4001        let hidden_dim = config.hidden_dim;
4002        if batch_size < 32 || Self::is_dense_layer(layer) {
4003            return None;
4004        }
4005        // Metal prefill owns MoE when dense Metal is on
4006        // (`try_metal_moe_prefill_batch`); do not steal the path.
4007        #[cfg(feature = "metal")]
4008        if frink_core::metal_dense_enabled() {
4009            return None;
4010        }
4011        let acts = config.layer_ffn_acts(layer_idx);
4012        let act = acts.routed;
4013        let ExpertBacking::Resident(experts) = &layer.moe.experts else {
4014            return None;
4015        };
4016        let n_experts = experts.len();
4017        let all_cpu = plan
4018            .map(|p| (0..n_experts).all(|eid| matches!(p.placement_for(eid), ExpertPlacement::Cpu)))
4019            .unwrap_or(true);
4020        if !all_cpu || n_experts == 0 {
4021            return None;
4022        }
4023
4024        let mut buckets: Vec<Vec<(usize, f32)>> = vec![Vec::new(); n_experts];
4025        for b in 0..batch_size {
4026            let logits = &router_logits_batch[b * n_experts..(b + 1) * n_experts];
4027            let decision = Self::route_for_layer(layer, logits, config);
4028            layer.moe.record_activations(&decision.expert_ids);
4029            for (&eid, &w) in decision.expert_ids.iter().zip(decision.weights.iter()) {
4030                buckets[eid].push((b, w));
4031            }
4032        }
4033
4034        let mut acc = vec![0f32; batch_size * hidden_dim];
4035        for (eid, toks) in buckets.iter().enumerate() {
4036            if toks.is_empty() {
4037                continue;
4038            }
4039            let n = toks.len();
4040            let mut gathered = vec![0f32; n * hidden_dim];
4041            // Each gathered row is one (token, slot): the slot's input
4042            // and the weight its output carries come from the one
4043            // helper the row body uses (`crate::routed_weight_site`).
4044            let mut out_weights = Vec::with_capacity(n);
4045            for (i, &(tok, w)) in toks.iter().enumerate() {
4046                let (input, w) = crate::routed_weight_site::routed_slot(
4047                    &routed_batch[tok * hidden_dim..(tok + 1) * hidden_dim],
4048                    w,
4049                    config.moe.routed_weight_before_ffn,
4050                );
4051                gathered[i * hidden_dim..(i + 1) * hidden_dim].copy_from_slice(&input);
4052                out_weights.push(w);
4053            }
4054            let ex = &experts[eid];
4055            let ffn_acts = ex.gate.quantize_batch_acts(&gathered, n);
4056            let gate = ex
4057                .gate
4058                .apply_batch_with_acts(&gathered, n, ffn_acts.as_ref());
4059            let up = ex.up.apply_batch_with_acts(&gathered, n, ffn_acts.as_ref());
4060            let activated = act.apply(&gate, &up);
4061            let down = ex.down.apply_batch(&activated, n);
4062            for (i, (&(tok, _), &w)) in toks.iter().zip(out_weights.iter()).enumerate() {
4063                let out = &down[i * hidden_dim..(i + 1) * hidden_dim];
4064                let row = &mut acc[tok * hidden_dim..(tok + 1) * hidden_dim];
4065                for (a, &o) in row.iter_mut().zip(out.iter()) {
4066                    *a += w * o;
4067                }
4068            }
4069        }
4070
4071        Self::accumulate_shared_experts_batch(
4072            layer,
4073            normed2_batch,
4074            batch_size,
4075            hidden_dim,
4076            &mut acc,
4077            acts.dense,
4078        );
4079        Some(acc)
4080    }
4081
4082    /// gpt-oss's MoE FFN for one position.
4083    ///
4084    /// A separate function rather than another branch inside
4085    /// `combine_ffn_outputs_for_position` on purpose: that path carries
4086    /// expert-store prefetch, residency placement, a Metal top-k fusion
4087    /// and a batched parallel-slot kernel, and every one of them would
4088    /// need its own gpt-oss variant to stay honest. This is the whole
4089    /// gpt-oss FFN in one readable block, checked end-to-end against
4090    /// llama.cpp, and slow — routed experts run serially. It is the
4091    /// correct-first shape; making it fast is a separate change with its
4092    /// own A/B, not something to smuggle in under a correctness fix.
4093    ///
4094    /// Ported from `llama-graph.cpp::build_moe_ffn` with
4095    /// `gating_op = LLAMA_EXPERT_GATING_FUNC_TYPE_SOFTMAX_WEIGHT`,
4096    /// `type_op = LLM_FFN_SWIGLU_OAI_MOE`, `norm_w = false`,
4097    /// `w_scale = 1`, all four bias tensors present.
4098    fn gpt_oss_ffn(
4099        layer: &LayerWeights,
4100        oai: &GptOssLayer,
4101        normed2: &[f32],
4102        config: &ModelConfig,
4103        hidden_dim: usize,
4104    ) -> Vec<f32> {
4105        let mut router_logits = layer.moe.router.apply(normed2);
4106        for (x, b) in router_logits.iter_mut().zip(oai.router_bias.iter()) {
4107            *x += b;
4108        }
4109        // Selection on the raw biased logits, softmax over the winners
4110        // only -- see `route_top_k_softmax_weight`.
4111        let decision =
4112            frink_moe::route_top_k_softmax_weight(&router_logits, config.moe.n_experts_active);
4113        layer.moe.record_activations(&decision.expert_ids);
4114
4115        let mut out = vec![0f32; hidden_dim];
4116        for (slot, &eid) in decision.expert_ids.iter().enumerate() {
4117            let w = decision.weights[slot];
4118            let expert_out = layer.moe.with_expert(eid, |ex| {
4119                frink_moe::run_expert_oai(
4120                    normed2,
4121                    ex,
4122                    &oai.expert_bias[eid],
4123                    frink_moe::SWIGLU_OAI_ALPHA,
4124                    frink_moe::SWIGLU_OAI_LIMIT,
4125                )
4126            });
4127            for (o, e) in out.iter_mut().zip(expert_out.iter()) {
4128                *o += w * e;
4129            }
4130        }
4131        out
4132    }
4133
4134    /// `forward_token`'s MoE FFN block for one position: the dense
4135    /// fast path (see `is_dense_layer`) or the full router+combine path
4136    /// with the router computed inline via a single-position `apply`.
4137    fn run_ffn_block(
4138        layer_idx: usize,
4139        layer: &LayerWeights,
4140        normed2: &[f32],
4141        config: &ModelConfig,
4142        hidden_dim: usize,
4143        plan: Option<&PlacementPlan>,
4144        operand: ffn_block::RouterOperand,
4145    ) -> Vec<f32> {
4146        if Self::is_dense_layer(layer) {
4147            layer.moe.record_activations(&[0]);
4148            // One expert, run exactly the way a routed one is. The GeGLU
4149            // arm used to be spelled out here and nowhere else, which is
4150            // precisely how the routed paths ended up SwiGLU-only.
4151            let act = config.layer_ffn_acts(layer_idx).dense;
4152            return Self::run_dense_expert(layer, normed2, act, config.rms_norm_eps);
4153        }
4154        // What the router reads is the caller's fact (`crate::router_input`):
4155        // the normed FFN input here, logits computed before attention, or
4156        // Arctic's normed layer input, which its experts read too.
4157        let (router_logits, routed_input): (Vec<f32>, &[f32]) = match &operand {
4158            ffn_block::RouterOperand::FfnInput => (layer.moe.router.apply(normed2), normed2),
4159            ffn_block::RouterOperand::Precomputed(logits) => (logits.clone(), normed2),
4160            ffn_block::RouterOperand::BranchInput(x) => (layer.moe.router.apply(x), x.as_slice()),
4161        };
4162        Self::combine_ffn_outputs_for_position(
4163            layer_idx,
4164            layer,
4165            normed2,
4166            routed_input,
4167            &router_logits,
4168            config,
4169            hidden_dim,
4170            plan,
4171        )
4172    }
4173
4174    /// One token's embedding row, scaled if this checkpoint scales it.
4175    ///
4176    /// `embedding_scale` is `sqrt(hidden_dim)` on the Gemma family and
4177    /// `None` everywhere else, so a path that dequantizes the row and
4178    /// forgets the multiply is wrong on exactly one family and right on
4179    /// every other -- which is why it survived as a drift for as long as
4180    /// it did. The lookup and the scale live in one function so a caller
4181    /// cannot obtain the row without it.
4182    /// The embedding row, scaled by `embedding_scale` where the
4183    /// architecture has one, and -- for a skip-stream model
4184    /// (`crate::skip_stream`) -- RMS-normed without a weight, as
4185    /// `talkie.cpp:50` norms `inpL` before layer 0. The ONE embedding
4186    /// site; what it returns is both layer 0's input and the skip
4187    /// source.
4188    ///
4189    /// `pos` is the token's position, which a learned position table
4190    /// (`crate::position_embd`) indexes: `gpt2.cpp:74-77` add row `pos`
4191    /// after `build_inp_embd`'s scale and before anything else.
4192    fn embed_token(&self, token_id: usize, pos: usize) -> Vec<f32> {
4193        let mut row = self.embedding.dequant_row(token_id);
4194        if let Some(scale) = self.config.embedding_scale {
4195            for v in row.iter_mut() {
4196                *v *= scale;
4197            }
4198        }
4199        if let Some(table) = &self.position_embd {
4200            assert!(
4201                pos < table.rows(),
4202                "position {pos} is past the {}-row learned position table \
4203                 (`position_embd.weight`); the trained context is the model's limit",
4204                table.rows()
4205            );
4206            for (v, p) in row.iter_mut().zip(table.dequant_row(pos)) {
4207                *v += p;
4208            }
4209        }
4210        // `bloom.cpp:77-80`: the embedding normed before layer 0. No
4211        // graph has both a position table and this norm, so their
4212        // order is not a graph's; it is the order the two arrived in.
4213        if !matches!(self.embedding_norm, NormOp::None) {
4214            row = self.embedding_norm.apply(&row, self.config.rms_norm_eps);
4215        }
4216        if self.config.skip_stream {
4217            row = crate::norm::rms_norm_no_params(&row, self.config.rms_norm_eps);
4218        }
4219        row
4220    }
4221
4222    /// [`Self::embed_token`] for a whole batch: `[batch, hidden]`,
4223    /// flattened row-major, row `b` at `position(b)`.
4224    fn embed_tokens(&self, tokens: &[usize], position: impl Fn(usize) -> usize) -> Vec<f32> {
4225        tokens
4226            .iter()
4227            .enumerate()
4228            .flat_map(|(b, &t)| self.embed_token(t, position(b)))
4229            .collect()
4230    }
4231
4232    /// The `output_head` half of a single-position forward: project the
4233    /// final-normed hidden state and softcap the result if this
4234    /// checkpoint softcaps it.
4235    ///
4236    /// The counterpart to [`Self::logits_from_flat_hidden`] for the
4237    /// one-row case, and held here for the same reason: Gemma-2 caps its
4238    /// final logits at 30.0, so a path that projects and returns without
4239    /// capping produces a different distribution -- not an error, just a
4240    /// quietly wrong one.
4241    fn logits_from_normed(&self, final_normed: &[f32]) -> Vec<f32> {
4242        Logits::project(
4243            &self.output_head,
4244            final_normed,
4245            self.output_bias.as_deref(),
4246            self.config.final_logit_softcap,
4247            self.config.logit_multiplier,
4248        )
4249        .into_vec()
4250    }
4251
4252    /// The `output_head` half of [`Self::forward_batch`], split out so
4253    /// the hidden-state-returning variant cannot drift from it (a
4254    /// second copy of the softcap would be a silent quality bug).
4255    fn logits_from_flat_hidden(&self, flat: Vec<f32>, batch_size: usize) -> Vec<Vec<f32>> {
4256        let vocab_size = self.output_head.rows();
4257        let logits_batch = Logits::from_output_head(
4258            self.output_head.apply_batch(&flat, batch_size),
4259            self.output_bias.as_deref(),
4260            self.config.final_logit_softcap,
4261            self.config.logit_multiplier,
4262        );
4263        logits_batch
4264            .as_slice()
4265            .chunks(vocab_size)
4266            .map(|c| c.to_vec())
4267            .collect()
4268    }
4269
4270    /// [`Self::forward_batch_last`], plus the choice of whether the host
4271    /// caches have to hold the real K/V when it returns. See
4272    /// [`Self::advance_host_kv_after_metal_prefill`] for why that is a
4273    /// choice at all.
4274    fn forward_batch_last_inner(
4275        &self,
4276        tokens: &[usize],
4277        start_pos: usize,
4278        kv_caches: &mut [KvCache],
4279        host_kv_authoritative: bool,
4280    ) -> Vec<f32> {
4281        let hiddens =
4282            self.forward_hidden_batch_inner(tokens, start_pos, kv_caches, host_kv_authoritative);
4283        let Some(last) = hiddens.last() else {
4284            return Vec::new();
4285        };
4286        self.logits_from_normed(last)
4287    }
4288
4289    /// The body of [`Self::forward_batch_last_paged`], already running on a
4290    /// CPU-pool worker. See `entry.rs` for why the split exists.
4291    ///
4292    /// `every_row` is the ONE difference between prefilling a paged
4293    /// prompt and SCORING one: the gather, the reservation and the
4294    /// scatter are identical, and what changes is whether the lm_head
4295    /// is projected once or at every position. A second copy of this
4296    /// function to vary that is how this repo has lost model features
4297    /// before, so it is a parameter.
4298    fn forward_batch_paged_on_worker(
4299        &self,
4300        tokens: &[usize],
4301        start_pos: usize,
4302        kv_caches: &mut [PagedKvCache],
4303        stores: &SharedPagedKv,
4304        every_row: bool,
4305    ) -> Result<Vec<Vec<f32>>, PagedStoreExhausted> {
4306        assert_eq!(kv_caches.len(), self.config.n_layers);
4307        assert_eq!(stores.layer_count(), self.config.n_layers);
4308        if tokens.is_empty() {
4309            return Ok(Vec::new());
4310        }
4311
4312        // Reserve every layer up front, under guards spanning the check
4313        // AND the take. Each layer has its own store, so one having
4314        // room says nothing about the next -- and under concurrency,
4315        // checking and then taking as separate steps lets another
4316        // request slip in between and leave this one half-written.
4317        //
4318        // Reserving before the forward rather than after also means a
4319        // request that cannot fit is refused before it burns a prefill.
4320        {
4321            let mut guards = stores.write_all();
4322            for (cache, store) in kv_caches.iter().zip(guards.iter()) {
4323                if cache.blocks_needed_for(store, tokens.len()) > store.free_block_count() {
4324                    return Err(PagedStoreExhausted);
4325                }
4326            }
4327            for (cache, store) in kv_caches.iter_mut().zip(guards.iter_mut()) {
4328                cache
4329                    .reserve(store, tokens.len())
4330                    .expect("checked against free_block_count under this same guard");
4331            }
4332        }
4333
4334        // Gather under read guards, one layer at a time: the forward
4335        // below is the expensive part and holds nothing.
4336        let mut scratch: Vec<KvCache> = kv_caches
4337            .iter()
4338            .enumerate()
4339            .map(|(l, cache)| cache.to_contiguous(&stores.read(l)))
4340            .collect();
4341
4342        // `host_kv_authoritative`: the scatter below READS these caches,
4343        // and a Metal prefill otherwise leaves them holding
4344        // `advance_len` placeholders while the real K/V sits on the
4345        // device. Copying those placeholders into the page store is
4346        // what made paged KV on Metal answer fluent nonsense from a
4347        // prompt the model never attended over.
4348        let logits = if every_row {
4349            // One logit row per POSITION, which is what scoring a
4350            // prompt needs and what prefilling one must not pay for:
4351            // on a 6000-token prompt this is 6000 vocabulary-wide
4352            // matmuls instead of one.
4353            let hiddens = self.forward_hidden_batch_inner(tokens, start_pos, &mut scratch, true);
4354            let batch_size = hiddens.len();
4355            let flat: Vec<f32> = hiddens.into_iter().flatten().collect();
4356            self.logits_from_flat_hidden(flat, batch_size)
4357        } else {
4358            vec![self.forward_batch_last_inner(tokens, start_pos, &mut scratch, true)]
4359        };
4360
4361        // Scatter into blocks this sequence already owns. Nothing here
4362        // can fail, which is the point of reserving above.
4363        for (l, (cache, gathered)) in kv_caches.iter_mut().zip(&scratch).enumerate() {
4364            let mut store = stores.write(l);
4365            let width = store.n_kv_heads() * store.head_dim();
4366            let base = cache.seq_len() * width;
4367            cache
4368                .append_contiguous(
4369                    &mut store,
4370                    &gathered.k[base..],
4371                    &gathered.v[base..],
4372                    tokens.len(),
4373                )
4374                .expect("blocks reserved above are still held by this sequence");
4375            // A recurrent layer's state moved in the scratch copy and
4376            // is not rows (`frink_core::recurrent_state`).
4377            cache.recurrent = gathered.recurrent.clone();
4378        }
4379        Ok(logits)
4380    }
4381
4382    /// [`Self::forward_hidden_batch`] with one extra promise the public
4383    /// signature cannot express.
4384    ///
4385    /// `host_kv_authoritative` says whether the caller will READ
4386    /// `kv_caches` afterwards. Metal prefill normally leaves K/V on the
4387    /// device and fills the host rows with a `advance_len` placeholder,
4388    /// which is correct only because the contiguous decode path then
4389    /// reads the device buffers too. `forward_batch_last_paged` reads
4390    /// the host rows -- it copies them into the page store -- so it
4391    /// passes `true` and pays for the download.
4392    fn forward_hidden_batch_inner(
4393        &self,
4394        tokens: &[usize],
4395        start_pos: usize,
4396        kv_caches: &mut [KvCache],
4397        host_kv_authoritative: bool,
4398    ) -> Vec<Vec<f32>> {
4399        // Read only by the Metal arms below; a CPU-only build fills the
4400        // host cache with real rows on every path and has nothing to
4401        // choose between.
4402        let _ = host_kv_authoritative;
4403        assert_eq!(kv_caches.len(), self.config.n_layers);
4404        let batch_size = tokens.len();
4405        if batch_size == 0 {
4406            return Vec::new();
4407        }
4408
4409        let hidden_dim = self.config.hidden_dim;
4410        let head_dim = self.config.head_dim;
4411        let v_head_dim = self.config.v_head_dim();
4412        // The Metal KV plane holds ONE geometry, and `use_metal_attn`
4413        // below is false for a per-layer-shape model
4414        // (`metal_can_serve_model`), so the widest layer's count is
4415        // every layer's wherever this is read.
4416        #[cfg(feature = "metal")]
4417        let n_kv_heads = self.config.n_kv_heads;
4418
4419        // [batch, hidden], flattened row-major.
4420        let mut hidden_batch: Vec<f32> = self.embed_tokens(tokens, |b| start_pos + b);
4421        let skip_rows = self.config.skip_stream.then(|| hidden_batch.clone());
4422
4423        #[cfg(feature = "metal")]
4424        let use_metal_attn = frink_core::metal_dense_enabled()
4425            && frink_metal::attn::metal_attn_enabled()
4426            && self
4427                .layers
4428                .iter()
4429                .all(|l| self.layer_supports_metal_attn(l));
4430
4431        #[cfg(not(feature = "metal"))]
4432        let use_metal_attn = false;
4433
4434        let residency = self.expert_residency_plan(use_metal_attn);
4435
4436        #[cfg(feature = "metal")]
4437        let mut metal_kv_guard: Option<
4438            std::sync::MutexGuard<'_, Option<Vec<frink_metal::attn::MetalKvBuffers>>>,
4439        > = if use_metal_attn {
4440            Some(Self::lock_metal_attn_kv(&self.metal_attn_kv))
4441        } else {
4442            None
4443        };
4444
4445        #[cfg(feature = "metal")]
4446        if let Some(guard) = metal_kv_guard.as_mut() {
4447            let need = self.layers.len();
4448            let need_cap = start_pos
4449                .saturating_add(batch_size)
4450                .saturating_add(256)
4451                .max(512);
4452            let reset = match guard.as_ref() {
4453                None => true,
4454                Some(v) => {
4455                    v.len() != need
4456                        || v.iter().any(|m| m.capacity() < need_cap)
4457                        || v.iter()
4458                            .zip(kv_caches.iter())
4459                            // ROWS: Metal holds rows, and this asks whether the
4460                            // host buffer matches them.
4461                            .any(|(m, c)| m.seq_len != c.rows())
4462                }
4463            };
4464            if reset {
4465                let mut bufs = Vec::with_capacity(need);
4466                for _ in 0..need {
4467                    match frink_metal::attn::MetalKvBuffers::with_capacity(
4468                        n_kv_heads, head_dim, need_cap,
4469                    ) {
4470                        Ok(b) => bufs.push(b),
4471                        Err(_) => {
4472                            **guard = None;
4473                            break;
4474                        }
4475                    }
4476                }
4477                if bufs.len() == need {
4478                    let mut ok = true;
4479                    for (m, c) in bufs.iter_mut().zip(kv_caches.iter()) {
4480                        if c.rows() > 0 && m.upload_from_host(&c.k, &c.v, c.rows()).is_err() {
4481                            ok = false;
4482                            break;
4483                        }
4484                    }
4485                    if ok {
4486                        **guard = Some(bufs);
4487                    } else {
4488                        **guard = None;
4489                    }
4490                } else {
4491                    **guard = None;
4492                }
4493            }
4494        }
4495
4496        let n_layers = self.config.n_layers;
4497        let mut hrm = self.hrm_streams(&hidden_batch);
4498        let mut l = 0usize;
4499        // Labelled for the Metal arm inside the `'attention` block below,
4500        // whose `continue` must name the loop it leaves.
4501        #[allow(unused_labels)]
4502        'layers: while l < n_layers {
4503            self.hrm_stack_input(hrm.as_ref(), l, &mut hidden_batch);
4504            let layer = self.layer_for(l);
4505            // THIS layer's head counts. Zero for the two attention-less
4506            // shapes, which leave the loop below before a width is used;
4507            // the Metal arms only run on a uniform model
4508            // (`metal_can_serve_model`), where every layer's counts are
4509            // the config's.
4510            let shape = self.config.layer_shape(l);
4511            let (n_heads, n_kv_heads) = (shape.attention.n_heads(), shape.attention.n_kv_heads());
4512            let q_width = n_heads * head_dim;
4513            let kv_width = n_kv_heads * head_dim;
4514            let v_width = n_kv_heads * v_head_dim;
4515            let out_width = n_heads * v_head_dim;
4516
4517            // Multi-layer dense prefill: one CB, activations stay on GPU.
4518            #[cfg(feature = "metal")]
4519            if use_metal_attn && batch_size >= 4 {
4520                if let Some(guard) = metal_kv_guard.as_mut() {
4521                    if let Some(metal_kvs) = guard.as_mut() {
4522                        if let Some(run_len) = self.metal_prefill_dense_stack_run_len(
4523                            l,
4524                            start_pos,
4525                            batch_size,
4526                            kv_caches,
4527                            Some(metal_kvs.as_slice()),
4528                        ) {
4529                            if let Some(h_out) = self.try_metal_prefill_dense_stack(
4530                                l,
4531                                run_len,
4532                                &hidden_batch,
4533                                start_pos,
4534                                batch_size,
4535                                n_heads,
4536                                metal_kvs,
4537                                kv_caches,
4538                                host_kv_authoritative,
4539                            ) {
4540                                hidden_batch = h_out;
4541                                l += run_len;
4542                                continue;
4543                            }
4544                        }
4545                    }
4546                }
4547            }
4548
4549            // A run of dense layers on the device, the hidden batch
4550            // resident across them, instead of seven round trips per
4551            // layer (#259). Declines before touching any cache; the
4552            // host body below then runs the layer.
4553            #[cfg(feature = "cuda")]
4554            if let Some((h_out, run_len)) = self.try_cuda_prefill_dense_stack(
4555                l,
4556                &hidden_batch,
4557                start_pos,
4558                batch_size,
4559                kv_caches,
4560            ) {
4561                hidden_batch = h_out;
4562                l += run_len;
4563                continue;
4564            }
4565
4566            let cache = &mut kv_caches[l];
4567
4568            // One-CB dense prefill (RMSNorm→QKV GEMM→attn→O→FFN) when every
4569            // projection has mul_mm_sg and the layer has no QKV bias / QK-norm.
4570            #[cfg(feature = "metal")]
4571            if use_metal_attn
4572                && batch_size >= 4
4573                && Self::metal_prefill_dense_layer_eligible(
4574                    layer,
4575                    &self.config,
4576                    self.lora_attached(),
4577                )
4578            {
4579                let swa_fits = self.metal_prefill_dense_swa_fits(l, start_pos, batch_size);
4580                if swa_fits {
4581                    if let Some(guard) = metal_kv_guard.as_mut() {
4582                        if let Some(metal_kvs) = guard.as_mut() {
4583                            // POSITIONS: compared against `start_pos`.
4584                            if metal_kvs[l].seq_len == cache.positions()
4585                                && start_pos == cache.positions()
4586                            {
4587                                layer.moe.record_activations(&[0]);
4588                                let fused = layer.moe.with_expert(0, |ex| {
4589                                    let (q, k, v, o) = (
4590                                        layer.attn.q_proj.mul_mm_sg_launch()?,
4591                                        layer.attn.k_proj.mul_mm_sg_launch()?,
4592                                        layer.attn.v_proj.mul_mm_sg_launch()?,
4593                                        layer.attn.o_proj.mul_mm_sg_launch()?,
4594                                    );
4595                                    let ffn = frink_metal::attn::PrefillFfnMetal::Dense {
4596                                        gate: ex.gate.mul_mm_sg_launch()?,
4597                                        up: ex.up.mul_mm_sg_launch()?,
4598                                        down: ex.down.mul_mm_sg_launch()?,
4599                                    };
4600                                    let gelu = self
4601                                        .config
4602                                        .model_ffn_act()
4603                                        .and_then(GluAct::fused_kernel_gelu_flag)?;
4604                                    let prefill_layer = frink_metal::attn::PrefillDenseLayerMetal {
4605                                        // See `try_metal_prefill_dense_stack`:
4606                                        // no weight, no fused launch.
4607                                        attn_norm_w: layer.attn.norm_weight.rms_weights()?,
4608                                        ffn_norm_w: layer.moe.norm_weight.rms_weights()?,
4609                                        q,
4610                                        k,
4611                                        v,
4612                                        o,
4613                                        ffn,
4614                                        post_attn_norm: layer.attn.post_attn_norm.as_deref(),
4615                                        post_ffn_norm: layer.attn.post_ffn_norm.as_deref(),
4616                                        extras: self.metal_attn_extras(layer),
4617                                        rope: self.metal_layer_rope(l),
4618                                        layer_idx: l as u32,
4619                                    };
4620                                    frink_metal::attn::launch_prefill_dense_layer(
4621                                        &hidden_batch,
4622                                        &prefill_layer,
4623                                        &mut metal_kvs[l],
4624                                        n_heads,
4625                                        batch_size,
4626                                        self.metal_rope(),
4627                                        start_pos,
4628                                        self.config.rms_norm_eps,
4629                                        gelu,
4630                                        self.config.attn_logit_softcap,
4631                                    )
4632                                    .ok()
4633                                });
4634                                if let Some(h_out) = fused {
4635                                    Self::advance_host_kv_after_metal_prefill(
4636                                        &metal_kvs[l],
4637                                        cache,
4638                                        batch_size,
4639                                        host_kv_authoritative,
4640                                    );
4641                                    hidden_batch = h_out;
4642                                    l += 1;
4643                                    continue;
4644                                }
4645                            }
4646                        }
4647                    }
4648                }
4649            }
4650
4651            // --- attention block ---
4652            let inputs = self.branch_inputs(layer, &hidden_batch, batch_size);
4653            let normed_batch: Vec<f32> =
4654                self.pre_norm_residual(&layer.attn.norm_weight, &mut hidden_batch, batch_size);
4655            let oai = self
4656                .gpt_oss
4657                .as_ref()
4658                .map(|g| &g.layers[self.physical_index(l)]);
4659
4660            // The GQA body, or the two shapes that have none of it
4661            // (`crate::layer_shapes::AttnShape`): a labelled block so the
4662            // FFN half below is reached by all three without a copy of
4663            // it, and so the Metal arms inside keep their `continue`s.
4664            'attention: {
4665                match shape.attention {
4666                    // deci.cpp:107-109: the residual passes straight through.
4667                    crate::layer_shapes::AttnShape::Absent => break 'attention,
4668                    // deci.cpp:115-118: `attn_norm` then `wo`, nothing else.
4669                    crate::layer_shapes::AttnShape::Linear => {
4670                        let projected = layer.attn.o_proj.apply_batch(&normed_batch, batch_size);
4671                        residual_add(&mut hidden_batch, &projected, self.config.residual_scale);
4672                        break 'attention;
4673                    }
4674                    // lfm2.cpp:197 / granite-hybrid.cpp:163: the rows are
4675                    // consecutive positions of one sequence, on this
4676                    // layer's cache.
4677                    crate::layer_shapes::AttnShape::ShortConv
4678                    | crate::layer_shapes::AttnShape::Mamba2
4679                    | crate::layer_shapes::AttnShape::Mamba1
4680                    | crate::layer_shapes::AttnShape::Plamo2Ssm
4681                    | crate::layer_shapes::AttnShape::Gdn
4682                    | crate::layer_shapes::AttnShape::Lightning => {
4683                        let out = self.recurrent_block(
4684                            l,
4685                            layer,
4686                            &normed_batch,
4687                            batch_size,
4688                            KvStep::Batched(cache),
4689                        );
4690                        residual_add(&mut hidden_batch, &out, self.config.residual_scale);
4691                        break 'attention;
4692                    }
4693                    crate::layer_shapes::AttnShape::Gqa { .. } => {}
4694                }
4695
4696                // falcon-h1.cpp:156-160: the parallel Mamba-2 block over the
4697                // same normed rows, on this layer's cache, before the push.
4698                let parallel_ssm = self.parallel_ssm_rows(
4699                    l,
4700                    layer,
4701                    &normed_batch,
4702                    batch_size,
4703                    &mut cache.recurrent,
4704                );
4705                // One shared activation-quant pass for q/k/v (plan 1e): the
4706                // three projections read the same normed batch, so quantize it
4707                // once instead of once per projection. A kind mismatch inside
4708                // the group just re-quantizes locally.
4709                let qkv_acts = layer
4710                    .attn
4711                    .q_proj
4712                    .quantize_batch_acts(&normed_batch, batch_size);
4713                let mut q_batch = layer.attn.q_proj.apply_batch_with_acts(
4714                    &normed_batch,
4715                    batch_size,
4716                    qkv_acts.as_ref(),
4717                );
4718                // qwen35.cpp:191-199: the gate rides in `wq`; split it
4719                // off before anything reads a Q width.
4720                let q_gate = layer.attn.q_gate_interleaved.then(|| {
4721                    let (q, gate) = crate::attn_gate::split_interleaved_q_gate(
4722                        &q_batch, batch_size, n_heads, head_dim,
4723                    );
4724                    q_batch = q;
4725                    gate
4726                });
4727                let mut k_batch = layer.attn.k_proj.apply_batch_with_acts(
4728                    &normed_batch,
4729                    batch_size,
4730                    qkv_acts.as_ref(),
4731                );
4732                let mut v_batch = layer.attn.v_proj.apply_batch_with_acts(
4733                    &normed_batch,
4734                    batch_size,
4735                    qkv_acts.as_ref(),
4736                );
4737                drop(qkv_acts);
4738
4739                self.apply_qkv_bias_and_clamp(
4740                    layer,
4741                    &mut q_batch,
4742                    &mut k_batch,
4743                    &mut v_batch,
4744                    q_width,
4745                    kv_width,
4746                    v_width,
4747                );
4748
4749                self.apply_qk_norms_pre_rope(layer, &mut q_batch, &mut k_batch, q_width, kv_width);
4750                // Host-side `mscale`, applied before either backend ropes.
4751                // The Metal branch below therefore hands its kernels
4752                // `attn_factor_applied_by_caller()` — folding it into cos/sin
4753                // there as well would square it.
4754                self.apply_rope_attn_factor(&mut q_batch, &mut k_batch, l);
4755
4756                #[cfg(feature = "metal")]
4757                {
4758                    let mut did_metal_prefill = false;
4759                    // The Metal prefill kernel is full-causal: only safe on a
4760                    // SWA layer while every causal position is still inside
4761                    // the window. Longer prompts fall back to CPU attention.
4762                    let swa_fits = match self.config.layer_sliding_window(l) {
4763                        Some(window) => start_pos + batch_size <= window,
4764                        None => true,
4765                    };
4766                    // Metal prefill applies attn softcap in FA-vec / legacy GQA.
4767                    if let Some(guard) = metal_kv_guard.as_mut() {
4768                        if let Some(metal_kvs) = guard.as_mut() {
4769                            // POSITIONS: compared against `start_pos`.
4770                            // `layer_rope` is `None` where llama.cpp does
4771                            // not rotate this layer at all
4772                            // (`crate::rope_layers`); the Metal prefill
4773                            // block always ropes, so such a layer takes the
4774                            // CPU body below rather than a rotation the
4775                            // checkpoint never trained.
4776                            if let (true, Some(layer_rope)) = (
4777                                metal_kvs[l].seq_len == cache.positions()
4778                                    && start_pos == cache.positions()
4779                                    && swa_fits,
4780                                self.metal_layer_rope(l),
4781                            ) {
4782                                let prefill_res = {
4783                                    frink_metal::attn::launch_prefill_attn_block(
4784                                        &q_batch,
4785                                        &k_batch,
4786                                        &v_batch,
4787                                        &mut metal_kvs[l],
4788                                        n_heads,
4789                                        batch_size,
4790                                        self.metal_rope().attn_factor_applied_by_caller(),
4791                                        layer_rope,
4792                                        start_pos,
4793                                        self.config.attn_logit_softcap,
4794                                        false,
4795                                    )
4796                                    .map(
4797                                        |(attn_out_batch, _, _)| {
4798                                            Self::advance_host_kv_after_metal_prefill(
4799                                                &metal_kvs[l],
4800                                                cache,
4801                                                batch_size,
4802                                                host_kv_authoritative,
4803                                            );
4804                                            let projected_batch = layer
4805                                                .attn
4806                                                .o_proj
4807                                                .apply_batch(&attn_out_batch, batch_size);
4808                                            let projected_batch =
4809                                                if let Some(post) = &layer.attn.post_attn_norm {
4810                                                    projected_batch
4811                                                        .chunks(hidden_dim)
4812                                                        .flat_map(|row| {
4813                                                            rms_norm(
4814                                                                row,
4815                                                                post,
4816                                                                self.config.rms_norm_eps,
4817                                                            )
4818                                                        })
4819                                                        .collect::<Vec<_>>()
4820                                                } else {
4821                                                    projected_batch
4822                                                };
4823                                            residual_add(
4824                                                &mut hidden_batch,
4825                                                &projected_batch,
4826                                                self.config.residual_scale,
4827                                            );
4828                                            true
4829                                        },
4830                                    )
4831                                };
4832                                match prefill_res {
4833                                    Ok(true) => {
4834                                        did_metal_prefill = true;
4835                                    }
4836                                    Ok(false) => {}
4837                                    Err(e) => {
4838                                        eprintln!(
4839                                            "frink: Metal prefill attn failed, CPU fallback: {e}"
4840                                        );
4841                                        **guard = None;
4842                                    }
4843                                }
4844                            }
4845                        }
4846                    }
4847                    if did_metal_prefill {
4848                        self.ffn_block_batch(
4849                            l,
4850                            layer,
4851                            &mut hidden_batch,
4852                            batch_size,
4853                            oai,
4854                            residency
4855                                .as_ref()
4856                                .map(|p| p.layer_plan(self.physical_index(l))),
4857                            inputs,
4858                            ffn_block::BatchedFfnKernels::Prefill,
4859                            skip_rows.as_deref().map(|rows| SkipStream { rows }),
4860                        );
4861                        l += 1;
4862                        continue 'layers;
4863                    }
4864                }
4865
4866                // RoPE per token is independent; parallelize for CPU pp512.
4867                q_batch
4868                    .par_chunks_mut(q_width)
4869                    .zip(k_batch.par_chunks_mut(kv_width))
4870                    .enumerate()
4871                    .for_each(|(b, (q_row, k_row))| {
4872                        let pos = start_pos + b;
4873                        for h in 0..n_heads {
4874                            self.apply_rope_head_layer(
4875                                &mut q_row[h * head_dim..(h + 1) * head_dim],
4876                                pos,
4877                                l,
4878                            );
4879                        }
4880                        for h in 0..n_kv_heads {
4881                            self.apply_rope_head_layer(
4882                                &mut k_row[h * head_dim..(h + 1) * head_dim],
4883                                pos,
4884                                l,
4885                            );
4886                        }
4887                    });
4888                // `maincoder` / `hunyuan-moe` norm HERE instead. Reachable
4889                // only on the host path, which is why
4890                // `layer_supports_metal_attn` refuses the layer outright
4891                // rather than letting the Metal arms above consume a batch
4892                // that has not been normed yet.
4893                self.apply_qk_norms_post_rope(
4894                    layer,
4895                    l,
4896                    &mut q_batch,
4897                    &mut k_batch,
4898                    q_width,
4899                    kv_width,
4900                );
4901                // Elementwise, so the whole Q batch in one call. Like the
4902                // multi-sequence path, this body did not apply it at all
4903                // until the decoration audit. It is placed AFTER the Metal
4904                // arms above deliberately: none of the seven fused launches
4905                // has an `attention_scale` uniform, and Q never returns to
4906                // the host inside `launch_prefill_dense_layer` /
4907                // `launch_prefill_dense_stack` for it to be scaled. The
4908                // refusal that keeps those arms out of reach when
4909                // `attention_scale` is set is in `layer_supports_metal_attn`.
4910                self.apply_attention_scale(&mut q_batch);
4911                // Same placement and same fence, per row: row `b` of this
4912                // batch is position `start_pos + b`, which is what the
4913                // RoPE loop above used for it.
4914                self.apply_attn_temperature(l, &mut q_batch, q_width, |b| start_pos + b);
4915
4916                // ROWS, not positions: it is added to `b + 1` below to give
4917                // each query in the batch the length of the KV it attends
4918                // over, which is a count of resident rows.
4919                let base_seq_len = cache.rows();
4920                for b in 0..batch_size {
4921                    cache
4922                        .push(
4923                            &k_batch[b * kv_width..(b + 1) * kv_width],
4924                            &v_batch[b * v_width..(b + 1) * v_width],
4925                        )
4926                        .expect("unbounded/planned KvCache growth is infallible");
4927                }
4928
4929                // Prefill attention over the just-written KV prefix. Parallel
4930                // over query positions — the serial loop was a dominant CPU
4931                // pp512 bottleneck (each query still attends only its causal
4932                // prefix; K/V slices are immutable after the pushes above).
4933                let cache_k = &cache.k;
4934                let cache_v = &cache.v;
4935                let softcap = self.config.attn_logit_softcap;
4936                let batch_window = self.config.batch_window(l, start_pos, batch_size);
4937                // A layer with sinks takes the per-query path, windowed
4938                // or not: the blocked kernel has no sink term. So does a
4939                // chunked layer whose queries straddle a chunk boundary
4940                // (`crate::chunked_swa`): the blocked kernel takes ONE
4941                // window. Everything
4942                // else goes through the blocked kernel, which is Rayon over
4943                // `[query-block x head]` against one shared KV buffer,
4944                // windowed or not. SWA layers used to take a per-query
4945                // `causal_gqa_attention_windowed_softcap` instead, which is
4946                // `online_attn_accumulate`: two scalar `exp` and a
4947                // head_dim-wide rescale per KV position, with the head axis
4948                // serial inside each task. On Gemma-3-1B (22 of 26 layers
4949                // are SWA) that arm was 19.6% of non-idle CPU `pp512`
4950                // samples while doing the *same* KV work as this one - at
4951                // `pp512` the 512-wide window covers the whole prompt.
4952                let sinks = layer.attn.sinks.as_deref();
4953                let blocked = match (batch_window, sinks) {
4954                    (crate::config::BatchWindow::Uniform(window), None) => Some(window),
4955                    (crate::config::BatchWindow::Uniform(_), Some(_))
4956                    | (crate::config::BatchWindow::PerQuery, _) => None,
4957                };
4958                let mut attn_out_batch = match blocked {
4959                    Some(window) => self.prefill_attention_blocked(
4960                        &q_batch,
4961                        cache_k,
4962                        cache_v,
4963                        n_heads,
4964                        n_kv_heads,
4965                        head_dim,
4966                        v_head_dim,
4967                        batch_size,
4968                        base_seq_len,
4969                        softcap,
4970                        window,
4971                    ),
4972                    None => {
4973                        let mut out = vec![0f32; batch_size * out_width];
4974                        out.par_chunks_mut(out_width)
4975                            .enumerate()
4976                            .for_each(|(b, dest)| {
4977                                let seq_len_b = base_seq_len + b + 1;
4978                                let attn_out = causal_gqa_attention_row(
4979                                    &q_batch[b * q_width..(b + 1) * q_width],
4980                                    &cache_k[..seq_len_b * kv_width],
4981                                    &cache_v[..seq_len_b * v_width],
4982                                    n_heads,
4983                                    n_kv_heads,
4984                                    head_dim,
4985                                    v_head_dim,
4986                                    seq_len_b,
4987                                    // Row `b` is position `start_pos + b`,
4988                                    // as the RoPE loop above had it.
4989                                    self.config.layer_window_for_query(l, start_pos + b),
4990                                    sinks,
4991                                    // The sink arm carries no softcap, as
4992                                    // `push_and_attend_row` has it.
4993                                    if sinks.is_some() { None } else { softcap },
4994                                    self.alibi_slopes.as_deref(),
4995                                );
4996                                dest.copy_from_slice(&attn_out);
4997                            });
4998                        out
4999                    }
5000                };
5001
5002                // Every query in this batch has now been answered, so the
5003                // rows behind the window are rows nothing will read again
5004                // (#61). This is why eviction is not inside `KvCache::push`:
5005                // `base_seq_len` above was captured BEFORE the batch's
5006                // pushes and every query's KV length is derived from it, so
5007                // a drop between the push loop and here would attend the
5008                // whole prompt over shifted keys.
5009                //
5010                // Per layer rather than after the stack, and that is where
5011                // most of the prefill saving is: a windowed layer hands its
5012                // prompt rows back before the next layer allocates its own,
5013                // so a 32k prompt holds ONE layer's full history at a time
5014                // instead of every windowed layer's at once.
5015                self.evict_layer_kv(l, cache);
5016
5017                let mut projected_batch = self.attn_out_to_residual_rows(
5018                    layer,
5019                    &normed_batch,
5020                    &mut attn_out_batch,
5021                    batch_size,
5022                    q_gate.as_deref(),
5023                );
5024                Self::add_parallel_ssm(&mut projected_batch, parallel_ssm);
5025                residual_add(
5026                    &mut hidden_batch,
5027                    &projected_batch,
5028                    self.config.residual_scale,
5029                );
5030            } // 'attention
5031
5032            // --- FFN block ---
5033            self.ffn_block_batch(
5034                l,
5035                layer,
5036                &mut hidden_batch,
5037                batch_size,
5038                oai,
5039                residency
5040                    .as_ref()
5041                    .map(|p| p.layer_plan(self.physical_index(l))),
5042                inputs,
5043                ffn_block::BatchedFfnKernels::Prefill,
5044                skip_rows.as_deref().map(|rows| SkipStream { rows }),
5045            );
5046            self.hrm_store(hrm.as_mut(), l, &hidden_batch);
5047            l += 1;
5048        }
5049
5050        hidden_batch
5051            .chunks(hidden_dim)
5052            .map(|h| self.final_norm.apply(h, self.config.rms_norm_eps))
5053            .collect()
5054    }
5055
5056    /// Appends one position to sequence `b`'s layer-`l` KV, then
5057    /// attends over everything that sequence holds.
5058    ///
5059    /// The only place `forward_multi_seq_kv` touches a cache, and so
5060    /// the only place the backing matters.
5061    ///
5062    /// Selects sequence `b`'s layer-`l` cache and hands it to
5063    /// [`Decoder::push_and_attend_row`], the one attend body the whole
5064    /// crate shares. This used to spell that body out a second time; the
5065    /// contiguous arm of the copy differed from `forward_token`'s by
5066    /// exactly one call (the CUDA resident hook), which is the kind of
5067    /// difference nobody notices until it is a wrong answer.
5068    #[allow(clippy::too_many_arguments)] // one per thing the step needs
5069    fn push_and_attend(
5070        &self,
5071        kv: &mut MultiSeqKv<'_>,
5072        b: usize,
5073        l: usize,
5074        layer: &LayerWeights,
5075        k: &[f32],
5076        v: &[f32],
5077        q: &[f32],
5078    ) -> Vec<f32> {
5079        self.push_and_attend_row(kv.step(b, l), l, layer, k, v, q)
5080    }
5081
5082    /// The body of [`Self::forward_multi_seq_kv`], already running on a
5083    /// CPU-pool worker. See `entry.rs` for why the split exists.
5084    fn forward_multi_seq_kv_on_worker(
5085        &self,
5086        tokens: &[usize],
5087        positions: &[usize],
5088        kv: &mut MultiSeqKv<'_>,
5089    ) -> Vec<Vec<f32>> {
5090        assert_eq!(tokens.len(), positions.len());
5091        assert_eq!(tokens.len(), kv.len());
5092        let batch_size = tokens.len();
5093        if batch_size == 0 {
5094            return Vec::new();
5095        }
5096        for seq in 0..batch_size {
5097            assert_eq!(kv.layers_per_seq(seq), self.config.n_layers);
5098        }
5099
5100        let hidden_dim = self.config.hidden_dim;
5101        let head_dim = self.config.head_dim;
5102        let v_head_dim = self.config.v_head_dim();
5103
5104        // [batch, hidden], flattened row-major.
5105        let mut hidden_batch: Vec<f32> = self.embed_tokens(tokens, |b| positions[b]);
5106        let skip_rows = self.config.skip_stream.then(|| hidden_batch.clone());
5107        let mut hrm = self.hrm_streams(&hidden_batch);
5108
5109        let residency = self.gpu_vram_budget_bytes.map(|b| self.residency_plan(b));
5110
5111        for l in 0..self.config.n_layers {
5112            self.hrm_stack_input(hrm.as_ref(), l, &mut hidden_batch);
5113            let layer = self.layer_for(l);
5114            // THIS layer's head counts; see the prefill body.
5115            let shape = self.config.layer_shape(l);
5116            let (n_heads, n_kv_heads) = (shape.attention.n_heads(), shape.attention.n_kv_heads());
5117            // --- attention block ---
5118            let inputs = self.branch_inputs(layer, &hidden_batch, batch_size);
5119            let normed_batch: Vec<f32> =
5120                self.pre_norm_residual(&layer.attn.norm_weight, &mut hidden_batch, batch_size);
5121            let oai = self
5122                .gpt_oss
5123                .as_ref()
5124                .map(|g| &g.layers[self.physical_index(l)]);
5125
5126            'attention: {
5127                match shape.attention {
5128                    crate::layer_shapes::AttnShape::Absent => break 'attention,
5129                    crate::layer_shapes::AttnShape::Linear => {
5130                        let projected = layer.attn.o_proj.apply_batch(&normed_batch, batch_size);
5131                        residual_add(&mut hidden_batch, &projected, self.config.residual_scale);
5132                        break 'attention;
5133                    }
5134                    // lfm2.cpp:197 / granite-hybrid.cpp:163: each row is
5135                    // ONE position of its own sequence, so each runs on
5136                    // its own cache.
5137                    crate::layer_shapes::AttnShape::ShortConv
5138                    | crate::layer_shapes::AttnShape::Mamba2
5139                    | crate::layer_shapes::AttnShape::Mamba1
5140                    | crate::layer_shapes::AttnShape::Plamo2Ssm
5141                    | crate::layer_shapes::AttnShape::Gdn
5142                    | crate::layer_shapes::AttnShape::Lightning => {
5143                        let mut out = Vec::with_capacity(batch_size * hidden_dim);
5144                        for b in 0..batch_size {
5145                            let step = kv.step(b, l);
5146                            out.extend(self.recurrent_block(
5147                                l,
5148                                layer,
5149                                &normed_batch[b * hidden_dim..(b + 1) * hidden_dim],
5150                                1,
5151                                step,
5152                            ));
5153                        }
5154                        residual_add(&mut hidden_batch, &out, self.config.residual_scale);
5155                        break 'attention;
5156                    }
5157                    crate::layer_shapes::AttnShape::Gqa { .. } => {}
5158                }
5159
5160                // falcon-h1.cpp:156-160: the parallel Mamba-2 block, each
5161                // row on its own sequence's cache, before the push.
5162                let parallel_ssm = layer.attn.ssm.as_ref().map(|_| {
5163                    let mut out = Vec::with_capacity(batch_size * hidden_dim);
5164                    for b in 0..batch_size {
5165                        let mut step = kv.step(b, l);
5166                        out.extend(
5167                            self.parallel_ssm_rows(
5168                                l,
5169                                layer,
5170                                &normed_batch[b * hidden_dim..(b + 1) * hidden_dim],
5171                                1,
5172                                step.recurrent_slot(),
5173                            )
5174                            .expect("the layer has the block"),
5175                        );
5176                    }
5177                    out
5178                });
5179                // One shared activation-quant pass for q/k/v (plan 1e): the
5180                // three projections read the same normed batch, so quantize it
5181                // once instead of once per projection. A kind mismatch inside
5182                // the group just re-quantizes locally.
5183                let qkv_acts = layer
5184                    .attn
5185                    .q_proj
5186                    .quantize_batch_acts(&normed_batch, batch_size);
5187                let mut q_batch = layer.attn.q_proj.apply_batch_with_acts(
5188                    &normed_batch,
5189                    batch_size,
5190                    qkv_acts.as_ref(),
5191                );
5192                // qwen35.cpp:191-199: the gate rides in `wq`; split it
5193                // off before anything reads a Q width.
5194                let q_gate = layer.attn.q_gate_interleaved.then(|| {
5195                    let (q, gate) = crate::attn_gate::split_interleaved_q_gate(
5196                        &q_batch, batch_size, n_heads, head_dim,
5197                    );
5198                    q_batch = q;
5199                    gate
5200                });
5201                let mut k_batch = layer.attn.k_proj.apply_batch_with_acts(
5202                    &normed_batch,
5203                    batch_size,
5204                    qkv_acts.as_ref(),
5205                );
5206                let mut v_batch = layer.attn.v_proj.apply_batch_with_acts(
5207                    &normed_batch,
5208                    batch_size,
5209                    qkv_acts.as_ref(),
5210                );
5211                drop(qkv_acts);
5212
5213                let q_width = n_heads * head_dim;
5214                let kv_width = n_kv_heads * head_dim;
5215                let v_width = n_kv_heads * v_head_dim;
5216                let out_width = n_heads * v_head_dim;
5217
5218                self.apply_qkv_bias_and_clamp(
5219                    layer,
5220                    &mut q_batch,
5221                    &mut k_batch,
5222                    &mut v_batch,
5223                    q_width,
5224                    kv_width,
5225                    v_width,
5226                );
5227
5228                self.apply_qk_norms_pre_rope(layer, &mut q_batch, &mut k_batch, q_width, kv_width);
5229                self.apply_rope_attn_factor(&mut q_batch, &mut k_batch, l);
5230
5231                for b in 0..batch_size {
5232                    let pos = positions[b];
5233                    let q_row = &mut q_batch[b * q_width..(b + 1) * q_width];
5234                    for h in 0..n_heads {
5235                        self.apply_rope_head_layer(
5236                            &mut q_row[h * head_dim..(h + 1) * head_dim],
5237                            pos,
5238                            l,
5239                        );
5240                    }
5241                    let k_row = &mut k_batch[b * kv_width..(b + 1) * kv_width];
5242                    for h in 0..n_kv_heads {
5243                        self.apply_rope_head_layer(
5244                            &mut k_row[h * head_dim..(h + 1) * head_dim],
5245                            pos,
5246                            l,
5247                        );
5248                    }
5249                }
5250                self.apply_qk_norms_post_rope(
5251                    layer,
5252                    l,
5253                    &mut q_batch,
5254                    &mut k_batch,
5255                    q_width,
5256                    kv_width,
5257                );
5258                // Applied to the whole Q batch at once because it is
5259                // elementwise. This path did not apply it at all until the
5260                // decoration audit: `attention_scale` reached only
5261                // `forward_token`'s CPU arm and `forward_token_paged`, so a
5262                // checkpoint carrying one answered at one temperature when
5263                // decoded alone and another when batched with its neighbours.
5264                self.apply_attention_scale(&mut q_batch);
5265                self.apply_attn_temperature(l, &mut q_batch, q_width, |b| positions[b]);
5266
5267                let mut attn_out_batch = vec![0f32; batch_size * out_width];
5268                for b in 0..batch_size {
5269                    let attn_out = self.push_and_attend(
5270                        kv,
5271                        b,
5272                        l,
5273                        layer,
5274                        &k_batch[b * kv_width..(b + 1) * kv_width],
5275                        &v_batch[b * v_width..(b + 1) * v_width],
5276                        &q_batch[b * q_width..(b + 1) * q_width],
5277                    );
5278                    attn_out_batch[b * out_width..(b + 1) * out_width].copy_from_slice(&attn_out);
5279                }
5280
5281                let mut projected_batch = self.attn_out_to_residual_rows(
5282                    layer,
5283                    &normed_batch,
5284                    &mut attn_out_batch,
5285                    batch_size,
5286                    q_gate.as_deref(),
5287                );
5288                Self::add_parallel_ssm(&mut projected_batch, parallel_ssm);
5289                residual_add(
5290                    &mut hidden_batch,
5291                    &projected_batch,
5292                    self.config.residual_scale,
5293                );
5294            } // 'attention
5295
5296            // --- FFN block --- per row, as this body has always run it;
5297            // see `BatchedFfnKernels::PerRow`.
5298            self.ffn_block_batch(
5299                l,
5300                layer,
5301                &mut hidden_batch,
5302                batch_size,
5303                oai,
5304                residency
5305                    .as_ref()
5306                    .map(|p| p.layer_plan(self.physical_index(l))),
5307                inputs,
5308                ffn_block::BatchedFfnKernels::PerRow,
5309                skip_rows.as_deref().map(|rows| SkipStream { rows }),
5310            );
5311            self.hrm_store(hrm.as_mut(), l, &hidden_batch);
5312        }
5313
5314        let final_normed_batch: Vec<f32> = hidden_batch
5315            .par_chunks(hidden_dim)
5316            .map(|h| self.final_norm.apply(h, self.config.rms_norm_eps))
5317            .flatten()
5318            .collect();
5319        self.logits_from_flat_hidden(final_normed_batch, batch_size)
5320    }
5321}
5322
5323#[cfg(test)]
5324mod tests {
5325    use super::*;
5326    use crate::config::glm_5_2;
5327    use frink_core::cache::PagedKvStore;
5328
5329    /// Small config used purely to keep the test fast: same
5330    /// architecture *shape* (GQA ratio, MoE topology) as GLM-5.2, but
5331    /// with tiny dims so the whole thing runs in milliseconds.
5332    fn tiny_test_config() -> ModelConfig {
5333        let mut cfg = glm_5_2();
5334        cfg.hidden_dim = 16;
5335        cfg.n_heads = 4;
5336        cfg.n_kv_heads = 2;
5337        cfg.head_dim = 4;
5338        cfg.moe.hidden_dim = 16;
5339        cfg.moe.n_experts = 6;
5340        cfg.moe.n_experts_active = 2;
5341        cfg.moe.n_shared_experts = 1;
5342        cfg.moe.expert_ffn_dim = 8;
5343        cfg
5344    }
5345
5346    /// A GeGLU model's ROUTED experts must run GeGLU.
5347    ///
5348    /// `run_ffn_block` used to consult `ffn_activation` only in its dense
5349    /// arm; `combine_ffn_outputs_for_position` and everything under it
5350    /// was unconditionally SwiGLU, so a GeGLU MoE would have produced
5351    /// fluent, wrong logits with nothing in the tree to notice. That is
5352    /// not hypothetical: llama.cpp's `grok` passes `LLM_FFN_GELU` to
5353    /// `build_moe_ffn` (`.scratch/llama.cpp/src/models/grok.cpp`), and
5354    /// `grok` sits on `ArchPath::GenericGqa` in `capability.rs`.
5355    ///
5356    /// The reference is written out here in plain loops -- its own GELU
5357    /// and SiLU, not `frink_core`'s -- so it cannot agree with the code
5358    /// under test by sharing its bug. The second assertion is the one
5359    /// that makes this a test rather than a smoke check: the SwiGLU
5360    /// answer must be visibly different, so an implementation that
5361    /// ignores the activation cannot pass.
5362    #[test]
5363    fn a_geglu_moe_layer_runs_geglu_in_its_routed_experts_not_swiglu() {
5364        let mut cfg = tiny_test_config();
5365        cfg.ffn_activation = crate::config::FfnActivation::Gelu;
5366        let decoder = Decoder::new_random_small(cfg, 2, 8);
5367        let hidden_dim = decoder.config.hidden_dim;
5368        let layer = &decoder.layers[1];
5369        assert!(
5370            !Decoder::is_dense_layer(layer),
5371            "this test is about the ROUTED path; layer 1 must be a real MoE layer"
5372        );
5373
5374        // Larger than the usual unit inputs on purpose: GELU and SiLU
5375        // are close near zero, and a reference that cannot tell them
5376        // apart cannot catch the bug this test exists for.
5377        let normed2: Vec<f32> = (0..hidden_dim)
5378            .map(|i| (i as f32 * 0.37).sin() * 12.0)
5379            .collect();
5380
5381        let gelu = |x: f32| {
5382            let t = (0.797_884_6f32 * (x + 0.044_715 * x * x * x)).tanh();
5383            0.5 * x * (1.0 + t)
5384        };
5385        let silu = |x: f32| x / (1.0 + (-x).exp());
5386        let expert_ref = |ex: &ExpertWeights, f: &dyn Fn(f32) -> f32| -> Vec<f32> {
5387            let g = ex.gate.apply(&normed2);
5388            let u = ex.up.apply(&normed2);
5389            let a: Vec<f32> = g.iter().zip(u.iter()).map(|(&g, &u)| f(g) * u).collect();
5390            ex.down.apply(&a)
5391        };
5392
5393        let ExpertBacking::Resident(experts) = &layer.moe.experts else {
5394            panic!("new_random_small builds resident experts");
5395        };
5396        let router_logits = layer.moe.router.apply(&normed2);
5397        let decision = Decoder::route_for_layer(layer, &router_logits, &decoder.config);
5398        let block_ref = |f: &dyn Fn(f32) -> f32| -> Vec<f32> {
5399            let mut out = vec![0f32; hidden_dim];
5400            for (&eid, &w) in decision.expert_ids.iter().zip(decision.weights.iter()) {
5401                for (o, e) in out.iter_mut().zip(expert_ref(&experts[eid], f).iter()) {
5402                    *o += w * e;
5403                }
5404            }
5405            assert!(
5406                layer.moe.shared_expert_gate.is_none(),
5407                "tiny_test_config's shared experts are ungated; reference assumes it"
5408            );
5409            for shex in &layer.moe.shared_experts {
5410                for (o, e) in out.iter_mut().zip(expert_ref(shex, f).iter()) {
5411                    *o += e;
5412                }
5413            }
5414            out
5415        };
5416        let expected_geglu = block_ref(&gelu);
5417        let expected_swiglu = block_ref(&silu);
5418
5419        let got = Decoder::run_ffn_block(
5420            1,
5421            layer,
5422            &normed2,
5423            &decoder.config,
5424            hidden_dim,
5425            None,
5426            ffn_block::RouterOperand::FfnInput,
5427        );
5428        assert_eq!(got.len(), hidden_dim);
5429        for (i, (a, b)) in got.iter().zip(expected_geglu.iter()).enumerate() {
5430            assert!(
5431                (a - b).abs() < 1e-4 * b.abs().max(1.0),
5432                "routed GeGLU FFN element {i}: got {a}, expected {b}"
5433            );
5434        }
5435        assert!(
5436            expected_geglu
5437                .iter()
5438                .zip(expected_swiglu.iter())
5439                .any(|(a, b)| (a - b).abs() > 1e-3),
5440            "GeGLU and SwiGLU must differ measurably on this input, or this test \
5441             could not detect a routed expert that silently ran SwiGLU"
5442        );
5443    }
5444
5445    #[test]
5446    fn forward_pass_produces_finite_logits_of_correct_shape() {
5447        let vocab = 10;
5448        let decoder = Decoder::new_random_small(tiny_test_config(), 2, vocab);
5449        let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
5450
5451        let logits = decoder.forward_token(3, 0, &mut caches);
5452        assert_eq!(logits.len(), vocab);
5453        assert!(
5454            logits.iter().all(|v| v.is_finite()),
5455            "logits must not contain NaN/Inf"
5456        );
5457    }
5458
5459    /// `gpu_vram_budget_bytes` must be a real zero-behavior-change
5460    /// default at `None`, and a *real placement plan that places
5461    /// nothing* (a zero VRAM budget, so `PlacementPlan::from_budget`
5462    /// fits no expert at all) must produce byte-identical output to
5463    /// `None` too -- proving the new plumbing (building a plan,
5464    /// looking up each routed expert's placement, dispatching through
5465    /// `run_expert_placed`) doesn't change results when nothing is
5466    /// actually GPU-placed, without needing real CUDA hardware to
5467    /// check (that hardware-dependent half is
5468    /// `frink-moe`'s/`frink-core`'s own `#[ignore]`d tests).
5469    #[test]
5470    fn gpu_vram_budget_bytes_with_nothing_placed_matches_the_default() {
5471        let mut decoder = Decoder::new_random_small(tiny_test_config(), 2, 10);
5472        let mut caches_default: Vec<KvCache> = decoder.config.new_kv_caches();
5473        let default_logits = decoder.forward_token(3, 0, &mut caches_default);
5474
5475        decoder.gpu_vram_budget_bytes = Some(0);
5476        let mut caches_zero_budget: Vec<KvCache> = decoder.config.new_kv_caches();
5477        let zero_budget_logits = decoder.forward_token(3, 0, &mut caches_zero_budget);
5478
5479        assert_eq!(
5480            default_logits, zero_budget_logits,
5481            "a placement plan that places nothing on GPU must match the None default exactly"
5482        );
5483    }
5484
5485    /// Qwen2-MoE's real shared-expert sigmoid gate
5486    /// (`MoeWeights::shared_expert_gate`): exact math check by mutating
5487    /// `layer.moe.shared_expert_gate` in place on an already-built
5488    /// decoder (no need to reconstruct a `LayerWeights`/`MoeWeights`
5489    /// from scratch) and comparing against a hand-derived expectation:
5490    /// the *only* thing the gate changes is the shared experts' own
5491    /// contribution, scaled by `sigmoid(gate . x)` -- so
5492    /// `gated_shared_output == ungated_shared_output * sigmoid_value`
5493    /// exactly, computed independently here via `run_expert` on the
5494    /// same layer's shared expert.
5495    #[test]
5496    fn shared_expert_gate_scales_shared_output_by_sigmoid_of_the_gate_logit() {
5497        let cfg = tiny_test_config();
5498        let mut decoder = Decoder::new_random_small(cfg, 2, 8);
5499        let hidden_dim = decoder.config.hidden_dim;
5500        assert_eq!(
5501            decoder.layers[1].moe.shared_experts.len(),
5502            1,
5503            "test assumes tiny_test_config's real MoE layer has exactly one shared expert"
5504        );
5505
5506        let normed2: Vec<f32> = (0..hidden_dim).map(|i| (i as f32 * 0.37).sin()).collect();
5507        let gate_vec: Vec<f32> = (0..hidden_dim).map(|i| i as f32 * 0.13 - 0.5).collect();
5508
5509        // Independently compute what the shared expert alone produces,
5510        // and what sigmoid(gate . x) should scale it by -- this is the
5511        // ground truth the gated code path must reproduce exactly.
5512        let shared_out_raw = run_expert(
5513            &normed2,
5514            &decoder.layers[1].moe.shared_experts[0],
5515            decoder.config.layer_ffn_acts(1).dense,
5516        );
5517        let gate_logit: f32 = gate_vec
5518            .iter()
5519            .zip(normed2.iter())
5520            .map(|(g, x)| g * x)
5521            .sum();
5522        let gate_value = 1.0 / (1.0 + (-gate_logit).exp());
5523        let expected_gated_shared: Vec<f32> =
5524            shared_out_raw.iter().map(|x| x * gate_value).collect();
5525
5526        // Run the real FFN combine path twice (gate absent, then
5527        // present) and recover each run's shared-only contribution by
5528        // subtracting the routed contribution, which the gate never
5529        // touches and is identical between the two runs (same router,
5530        // same experts, same input).
5531        let router_logits = decoder.layers[1].moe.router.apply(&normed2);
5532        let ungated_total = Decoder::combine_ffn_outputs_for_position(
5533            1,
5534            &decoder.layers[1],
5535            &normed2,
5536            &normed2,
5537            &router_logits,
5538            &decoder.config,
5539            hidden_dim,
5540            None,
5541        );
5542        decoder.layers[1].moe.shared_expert_gate = Some(gate_vec);
5543        let gated_total = Decoder::combine_ffn_outputs_for_position(
5544            1,
5545            &decoder.layers[1],
5546            &normed2,
5547            &normed2,
5548            &router_logits,
5549            &decoder.config,
5550            hidden_dim,
5551            None,
5552        );
5553
5554        for (i, ((u, g), expected_shared)) in ungated_total
5555            .iter()
5556            .zip(gated_total.iter())
5557            .zip(expected_gated_shared.iter())
5558            .enumerate()
5559        {
5560            let routed_contribution = u - shared_out_raw[i];
5561            let gated_shared_recovered = g - routed_contribution;
5562            assert!(
5563                (gated_shared_recovered - expected_shared).abs() < 1e-4,
5564                "index {i}: recovered gated shared output {gated_shared_recovered} != expected {expected_shared} (sigmoid({gate_logit})={gate_value})"
5565            );
5566        }
5567    }
5568
5569    #[test]
5570    fn kv_cache_grows_by_one_position_per_layer_per_step() {
5571        let decoder = Decoder::new_random_small(tiny_test_config(), 3, 5);
5572        let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
5573
5574        decoder.forward_token(0, 0, &mut caches);
5575        decoder.forward_token(1, 1, &mut caches);
5576        decoder.forward_token(2, 2, &mut caches);
5577
5578        for cache in &caches {
5579            assert_eq!(cache.positions(), 3);
5580        }
5581    }
5582
5583    #[test]
5584    fn same_token_same_position_is_deterministic() {
5585        let decoder = Decoder::new_random_small(tiny_test_config(), 2, 8);
5586        let mut caches_a: Vec<KvCache> = decoder.config.new_kv_caches();
5587        let mut caches_b: Vec<KvCache> = decoder.config.new_kv_caches();
5588
5589        let out_a = decoder.forward_token(4, 0, &mut caches_a);
5590        let out_b = decoder.forward_token(4, 0, &mut caches_b);
5591        assert_eq!(out_a, out_b, "identical input state must yield identical output (no hidden randomness in the forward pass)");
5592    }
5593
5594    #[test]
5595    fn multi_step_decode_stays_finite_across_positions() {
5596        let decoder = Decoder::new_random_small(tiny_test_config(), 2, 8);
5597        let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
5598
5599        for pos in 0..16 {
5600            let logits = decoder.forward_token(pos % 8, pos, &mut caches);
5601            assert!(
5602                logits.iter().all(|v| v.is_finite()),
5603                "position {pos}: logits must stay finite across an extended decode run"
5604            );
5605        }
5606    }
5607
5608    /// `forward_token_paged` must produce bit-identical output to
5609    /// `forward_token` across a multi-step decode (each layer's paged
5610    /// store sized generously so no layer ever exhausts its blocks) --
5611    /// the block-table indirection is a storage-layout detail, not a
5612    /// math change.
5613    #[test]
5614    fn forward_token_paged_matches_forward_token_bit_identical() {
5615        paged_matches_contiguous(tiny_test_config());
5616    }
5617
5618    /// Every arm of the attention dispatch, not just the plain one.
5619    ///
5620    /// The paged path used to implement only full causal attention, and
5621    /// `forward_token_paged` asserted rather than run gpt-oss, because a
5622    /// missing sink term would have changed the distribution silently.
5623    /// Now that it mirrors all three arms, each one has to be held to
5624    /// the same bar the plain arm always was: BIT-identical, not close.
5625    ///
5626    /// A sliding window and a softcap are both driven from the config
5627    /// here, so a future edit that wires one arm and forgets another
5628    /// fails on the arm it forgot rather than on a model nobody tests.
5629    #[test]
5630    fn every_paged_attention_arm_is_bit_identical_to_its_contiguous_twin() {
5631        let windowed = || {
5632            let mut cfg = tiny_test_config();
5633            // Smaller than the decode length below, so the window really
5634            // drops positions rather than degenerating to full causal.
5635            cfg.sliding_window = Some(2);
5636            cfg.swa_layers = crate::swa_layers::SwaLayers::All;
5637            cfg
5638        };
5639        let softcapped = || {
5640            let mut cfg = tiny_test_config();
5641            // Small enough that `sc * tanh(s / sc)` actually compresses.
5642            // A realistic 30.0 is numerically indistinguishable from no
5643            // cap at these tiny weights, so a test using it would pass
5644            // whether or not the arm was wired -- checked by breaking
5645            // the arm on purpose and watching it still pass.
5646            cfg.attn_logit_softcap = Some(0.05);
5647            cfg
5648        };
5649        let both = || {
5650            let mut cfg = windowed();
5651            cfg.attn_logit_softcap = Some(0.05);
5652            cfg
5653        };
5654        // Alternating window/full layers: the per-layer arm choice has
5655        // to be honoured per layer, not decided once for the model.
5656        let alternating = || {
5657            let mut cfg = tiny_test_config();
5658            cfg.sliding_window = Some(2);
5659            cfg.swa_layers = crate::swa_layers::SwaLayers::period(2, false);
5660            cfg
5661        };
5662
5663        for cfg in [windowed(), softcapped(), both(), alternating()] {
5664            paged_matches_contiguous(cfg);
5665        }
5666    }
5667
5668    /// Five MORE model features the paged path had lost the same way
5669    /// the first five went: by being a copy of the contiguous loop that
5670    /// nothing forced to stay in step.
5671    ///
5672    /// Found by running Gemma-2-2B through paged KV and watching it
5673    /// answer differently from the same model on the same backend with
5674    /// a contiguous cache -- on CPU, with no GPU involved at all. None
5675    /// of the arm tests above could see it, because `tiny_test_config`
5676    /// sets none of these and `new_random_small` builds every layer
5677    /// without the two sandwich norms.
5678    ///
5679    /// - `attention_scale`: Gemma scales Q itself and asks the kernel
5680    ///   for a score scale of 1.0, so the built-in `1/sqrt(head_dim)`
5681    ///   has to be compensated for. Missing, the model answers at a
5682    ///   different temperature.
5683    /// - `post_attn_norm` / `post_ffn_norm`: Gemma-2's sandwich norms,
5684    ///   applied to each branch before it rejoins the residual.
5685    /// - gpt-oss's `o_bias`, and its own FFN (`gpt_oss_ffn`, which
5686    ///   biases the router and runs the clamped OAI SwiGLU) instead of
5687    ///   the generic one.
5688    ///
5689    /// Every one of them produces a plausible distribution rather than
5690    /// an error, which is exactly why they are pinned rather than
5691    /// trusted. Values are chosen so each really bites: a scale of 1.0
5692    /// or an all-ones norm would let this pass either way.
5693    #[test]
5694    fn the_paged_path_keeps_every_per_layer_feature_the_contiguous_one_applies() {
5695        // Gemma's query pre-attention scalar, well away from the
5696        // kernel's own 1/sqrt(head_dim).
5697        let mut scaled = tiny_test_config();
5698        scaled.attention_scale = Some(0.37);
5699        paged_matches_contiguous_with(scaled, |_| {});
5700
5701        // Sandwich norms, one at a time and then together, so a wired
5702        // half is not covered for by the other.
5703        for (attn, ffn) in [(true, false), (false, true), (true, true)] {
5704            paged_matches_contiguous_with(tiny_test_config(), with_sandwich_norms(attn, ffn));
5705        }
5706
5707        // gpt-oss: the O bias and the OAI FFN, which the paged path was
5708        // substituting the generic router+SwiGLU for.
5709        paged_matches_contiguous_with(tiny_test_config(), with_gpt_oss_graph);
5710    }
5711
5712    /// The same feature list as
5713    /// [`the_paged_path_keeps_every_per_layer_feature_the_contiguous_one_applies`],
5714    /// checked against `forward_hidden_batch_inner` instead.
5715    ///
5716    /// Necessary because `forward_token` and `forward_token_paged` now
5717    /// share ONE body (`Decoder::attn_block`) that differs only in its
5718    /// `KvStep`, so the paged test can no longer see a decoration
5719    /// dropped from that body -- deleting `post_attn_norm` or gpt-oss's
5720    /// `o_bias` from it leaves the whole suite green, which was measured
5721    /// rather than assumed. `forward_hidden_batch_inner` is deliberately
5722    /// NOT collapsed into the same body, so it is the independent
5723    /// ground truth that keeps these features pinned.
5724    #[test]
5725    fn the_batched_path_keeps_every_per_layer_feature_the_token_path_applies() {
5726        for (attn, ffn) in [(true, false), (false, true), (true, true)] {
5727            batched_matches_contiguous_with(tiny_test_config(), with_sandwich_norms(attn, ffn));
5728        }
5729        batched_matches_contiguous_with(tiny_test_config(), with_gpt_oss_graph);
5730    }
5731
5732    /// Gemma-2's two sandwich norms, as a switch both parity helpers
5733    /// take, so the paged and batched tests cannot drift over WHICH
5734    /// features they claim to cover.
5735    ///
5736    /// Per-layer values, so a path that applied layer 0's norm
5737    /// everywhere would still fail.
5738    fn with_sandwich_norms(attn: bool, ffn: bool) -> impl Fn(&mut Decoder) {
5739        move |d: &mut Decoder| {
5740            let hidden = d.config.hidden_dim;
5741            for (i, layer) in d.layers.iter_mut().enumerate() {
5742                let w: Vec<f32> = (0..hidden)
5743                    .map(|j| 0.5 + (i * hidden + j) as f32 * 0.01)
5744                    .collect();
5745                if attn {
5746                    layer.attn.post_attn_norm = Some(w.clone());
5747                }
5748                if ffn {
5749                    layer.attn.post_ffn_norm = Some(w);
5750                }
5751            }
5752        }
5753    }
5754
5755    /// The whole gpt-oss graph: attention sinks on every layer's
5756    /// attention weights, and the side table -- the O bias, the router
5757    /// bias and the per-expert biases `gpt_oss_ffn` reads.
5758    fn with_gpt_oss_graph(d: &mut Decoder) {
5759        let hidden = d.config.hidden_dim;
5760        let n_heads = d.config.n_heads;
5761        let n_experts = d.config.moe.n_experts;
5762        let ffn = d.config.moe.expert_ffn_dim;
5763        let n_layers = d.layers.len();
5764        for (l, layer) in d.layers.iter_mut().enumerate() {
5765            layer.attn.sinks = Some((0..n_heads).map(|h| 0.1 + (l + h) as f32 * 0.05).collect());
5766            layer.attn.o_bias = Some((0..hidden).map(|j| 0.02 * (j as f32 - 8.0)).collect());
5767        }
5768        d.gpt_oss = Some(GptOssWeights {
5769            layers: (0..n_layers)
5770                .map(|_| GptOssLayer {
5771                    router_bias: (0..n_experts).map(|e| 0.03 * e as f32).collect(),
5772                    expert_bias: (0..n_experts)
5773                        .map(|e| frink_moe::ExpertBias {
5774                            gate: vec![0.01 * (e + 1) as f32; ffn],
5775                            up: vec![-0.02 * (e + 1) as f32; ffn],
5776                            down: vec![0.005 * (e + 1) as f32; hidden],
5777                        })
5778                        .collect(),
5779                })
5780                .collect(),
5781        });
5782    }
5783
5784    /// [`paged_matches_contiguous_with`] for `forward_batch` against
5785    /// sequential `forward_token`.
5786    ///
5787    /// Not bit-identity: batched prefill runs the blocked three-pass
5788    /// softmax while decode keeps the online accumulator, so the two
5789    /// agree to a tolerance rather than to the bit -- the same reason
5790    /// `decoder_via_engine_trait_matches_forward_batch_ground_truth`
5791    /// gives. 1e-5 is four orders below the ~1e-1 a dropped decoration
5792    /// moves these logits by.
5793    fn batched_matches_contiguous_with(config: ModelConfig, prepare: impl Fn(&mut Decoder)) {
5794        let n_layers = 2;
5795        let vocab = 10;
5796        let tokens = [3usize, 5, 7, 2, 9, 1];
5797
5798        let mut seq_decoder = Decoder::new_random_small(config.clone(), n_layers, vocab);
5799        prepare(&mut seq_decoder);
5800        let mut seq_caches: Vec<KvCache> = seq_decoder.config.new_kv_caches();
5801        let sequential: Vec<Vec<f32>> = tokens
5802            .iter()
5803            .enumerate()
5804            .map(|(pos, &t)| seq_decoder.forward_token(t, pos, &mut seq_caches))
5805            .collect();
5806
5807        // Same seed -> identical weights before `prepare`, and `prepare`
5808        // is deterministic, so this is a like-for-like comparison.
5809        let mut batch_decoder = Decoder::new_random_small(config, n_layers, vocab);
5810        prepare(&mut batch_decoder);
5811        let mut batch_caches: Vec<KvCache> = batch_decoder.config.new_kv_caches();
5812        let batched = batch_decoder.forward_batch(&tokens, 0, &mut batch_caches);
5813
5814        assert_eq!(sequential.len(), batched.len());
5815        for (pos, (a, b)) in sequential.iter().zip(batched.iter()).enumerate() {
5816            assert_eq!(a.len(), b.len(), "position {pos}: logit count");
5817            for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
5818                assert!(
5819                    (x - y).abs() < 1e-5,
5820                    "position {pos}, logit {i}: token path={x} batched={y}"
5821                );
5822            }
5823        }
5824    }
5825
5826    /// The two rules that live OUTSIDE the layer loop, which the arm
5827    /// test above cannot reach.
5828    ///
5829    /// The paged path had drifted from the contiguous one at both ends
5830    /// of the stack, and neither drift was visible to any existing test
5831    /// because `tiny_test_config` sets neither field:
5832    ///
5833    /// - it called `embedding.dequant_row` directly instead of scaling
5834    ///   the row by `embedding_scale`, so every Gemma token entered the
5835    ///   stack `sqrt(hidden_dim)` times too small;
5836    /// - it returned `output_head.apply(..)` raw instead of applying
5837    ///   `final_logit_softcap`, so Gemma-2's 30.0 cap never ran.
5838    ///
5839    /// Both produce a plausible distribution rather than an error, which
5840    /// is the whole reason to pin them: a wrong answer that still looks
5841    /// like an answer is what a parity test is for. Values here are
5842    /// chosen so each one actually bites -- a scale of 1.0 or a cap far
5843    /// above the logit range would let this pass either way.
5844    #[test]
5845    fn the_paged_path_scales_embeddings_and_softcaps_logits_like_the_contiguous_one() {
5846        let scaled = || {
5847            let mut cfg = tiny_test_config();
5848            cfg.embedding_scale = Some(7.5);
5849            cfg
5850        };
5851        let capped = || {
5852            let mut cfg = tiny_test_config();
5853            // Small enough that `sc * tanh(x / sc)` really compresses at
5854            // this model's logit magnitudes, on the same reasoning as
5855            // the attention softcap above.
5856            cfg.final_logit_softcap = Some(0.05);
5857            cfg
5858        };
5859        let both = || {
5860            let mut cfg = scaled();
5861            cfg.final_logit_softcap = Some(0.05);
5862            cfg
5863        };
5864
5865        for cfg in [scaled(), capped(), both()] {
5866            paged_matches_contiguous(cfg);
5867        }
5868    }
5869
5870    /// Paged prefill must agree with contiguous prefill, and must leave
5871    /// the KV in a state a paged DECODE can continue from.
5872    ///
5873    /// The second half is the one worth having. `forward_batch_last`
5874    /// returns only the last row's logits, so a gather/scatter that
5875    /// mangled the KV -- wrote the rows in the wrong order, dropped the
5876    /// part-full tail block, mis-sized a copy -- could still return the
5877    /// right logits for THIS call and only surface on the next token.
5878    /// Decoding four more tokens after the prefill is what makes the
5879    /// stored KV observable, so both paths are compared over the whole
5880    /// continuation rather than at the seam.
5881    ///
5882    /// A block size of 2 against a 5-token prompt is deliberate: it
5883    /// leaves the tail block part-full, which is the case
5884    /// `blocks_needed_for` exists for and the one a `n / block_size`
5885    /// reservation would get wrong.
5886    fn paged_prefill_matches_contiguous(config: ModelConfig) {
5887        let n_layers = 2;
5888        let decoder = Decoder::new_random_small(config, n_layers, 10);
5889        let prompt = [3usize, 1, 4, 1, 5];
5890        let continuation = [9usize, 2, 6, 5];
5891
5892        let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
5893        let mut plain = vec![decoder.forward_batch_last(&prompt, 0, &mut caches)];
5894        for (i, &tok) in continuation.iter().enumerate() {
5895            plain.push(decoder.forward_token(tok, prompt.len() + i, &mut caches));
5896        }
5897
5898        let mut paged_caches: Vec<PagedKvCache> =
5899            (0..n_layers).map(|_| PagedKvCache::new()).collect();
5900        let stores = SharedPagedKv::from_stores(
5901            (0..n_layers)
5902                .map(|_| {
5903                    PagedKvStore::new(
5904                        /* block_size = */ 2,
5905                        /* total_blocks = */ 16,
5906                        decoder.config.n_kv_heads,
5907                        decoder.config.head_dim,
5908                    )
5909                })
5910                .collect(),
5911        );
5912        let mut paged = vec![decoder
5913            .forward_batch_last_paged(&prompt, 0, &mut paged_caches, &stores)
5914            .expect("store sized generously, must not exhaust")];
5915        for (i, &tok) in continuation.iter().enumerate() {
5916            paged.push(
5917                decoder
5918                    .forward_token_paged(tok, prompt.len() + i, &mut paged_caches, &stores)
5919                    .expect("store sized generously, must not exhaust"),
5920            );
5921        }
5922
5923        assert_eq!(
5924            paged_caches[0].seq_len(),
5925            prompt.len() + continuation.len(),
5926            "paged prefill must advance seq_len by exactly the batch size"
5927        );
5928        assert_eq!(plain.len(), paged.len());
5929        for (step, (a, b)) in plain.iter().zip(paged.iter()).enumerate() {
5930            assert_eq!(a.len(), b.len(), "step {step}: logit count");
5931            for (x, y) in a.iter().zip(b.iter()) {
5932                assert_eq!(
5933                    x.to_bits(),
5934                    y.to_bits(),
5935                    "step {step}: paged prefill + decode must be bit-identical to contiguous"
5936                );
5937            }
5938        }
5939    }
5940
5941    /// Every arm again, this time through the prefill entry point. The
5942    /// gather is shared, but the kernel the gathered buffer reaches is
5943    /// the BLOCKED prefill one rather than the per-query decode one, so
5944    /// arm coverage here is not implied by the decode tests above.
5945    #[test]
5946    fn paged_prefill_is_bit_identical_across_every_arm() {
5947        let windowed = || {
5948            let mut cfg = tiny_test_config();
5949            cfg.sliding_window = Some(2);
5950            cfg.swa_layers = crate::swa_layers::SwaLayers::All;
5951            cfg
5952        };
5953        let scaled_and_capped = || {
5954            let mut cfg = tiny_test_config();
5955            cfg.embedding_scale = Some(7.5);
5956            cfg.final_logit_softcap = Some(0.05);
5957            cfg.attn_logit_softcap = Some(0.05);
5958            cfg
5959        };
5960        let alternating = || {
5961            let mut cfg = tiny_test_config();
5962            cfg.sliding_window = Some(2);
5963            cfg.swa_layers = crate::swa_layers::SwaLayers::period(2, false);
5964            cfg
5965        };
5966
5967        for cfg in [
5968            tiny_test_config(),
5969            windowed(),
5970            scaled_and_capped(),
5971            alternating(),
5972        ] {
5973            paged_prefill_matches_contiguous(cfg);
5974        }
5975    }
5976
5977    /// A prefill the stores cannot hold refuses having written NOTHING
5978    /// -- checked on the case that actually needs the up-front loop.
5979    ///
5980    /// Each layer owns its own store, so layer 0 having room says
5981    /// nothing about layer 1. `append_contiguous` already refuses
5982    /// rather than half-writing a single layer, so a test whose layers
5983    /// are sized alike passes with the cross-layer reservation deleted
5984    /// -- it would be asserting a property it never exercises. Here
5985    /// layer 0 has room for the whole prompt and layer 1 does not, so
5986    /// without the up-front check layer 0 is written, layer 1 refuses,
5987    /// and the sequence ends up with its layers at DIFFERENT lengths.
5988    /// No caller can recover from that, and nothing downstream would
5989    /// report it: the next decode step simply attends over a shorter
5990    /// history in one layer than the others.
5991    ///
5992    /// Verified by deleting the reservation loop and watching this fail
5993    /// on `layer 1 must be untouched`.
5994    /// Three requests sharing one set of per-layer stores must get
5995    /// exactly what they would get alone.
5996    ///
5997    /// This is the property the RwLock exists for, and it cannot be
5998    /// asserted single-threaded. Every request writes only blocks it
5999    /// owns, so sharing changes where rows live and nothing else --
6000    /// bit-identical, not close. A store that let one request's rows
6001    /// land in another's blocks shows up here and nowhere else.
6002    #[test]
6003    fn concurrent_decodes_against_one_shared_store_match_running_them_alone() {
6004        use std::sync::Arc;
6005
6006        let decoder = Arc::new(Decoder::new_random_small(tiny_test_config(), 2, 10));
6007        let prompts: [&[usize]; 3] = [&[3, 1, 4], &[1, 5, 9], &[2, 6, 5]];
6008        let continuation = [7usize, 8, 3];
6009
6010        // Each request run alone, against its own store, is the answer
6011        // sharing must not change.
6012        let solo: Vec<Vec<Vec<f32>>> = prompts
6013            .iter()
6014            .map(|prompt| {
6015                let stores = SharedPagedKv::new(
6016                    2,
6017                    4,
6018                    32,
6019                    decoder.config.n_kv_heads,
6020                    decoder.config.head_dim,
6021                );
6022                let mut caches: Vec<PagedKvCache> = (0..2).map(|_| PagedKvCache::new()).collect();
6023                run_one(&decoder, prompt, &continuation, &mut caches, &stores)
6024            })
6025            .collect();
6026
6027        // The same three, concurrently, sharing ONE set of per-layer
6028        // stores. Every request writes only blocks it owns, so the
6029        // answers must be identical -- not close, identical. A store
6030        // that let one request's rows land in another's blocks would
6031        // show up here and nowhere else.
6032        let shared = Arc::new(SharedPagedKv::new(
6033            2,
6034            4,
6035            96,
6036            decoder.config.n_kv_heads,
6037            decoder.config.head_dim,
6038        ));
6039        let together: Vec<Vec<Vec<f32>>> = std::thread::scope(|scope| {
6040            let handles: Vec<_> = prompts
6041                .iter()
6042                .map(|prompt| {
6043                    let decoder = Arc::clone(&decoder);
6044                    let shared = Arc::clone(&shared);
6045                    scope.spawn(move || {
6046                        let mut caches: Vec<PagedKvCache> =
6047                            (0..2).map(|_| PagedKvCache::new()).collect();
6048                        run_one(&decoder, prompt, &continuation, &mut caches, &shared)
6049                    })
6050                })
6051                .collect();
6052            handles.into_iter().map(|h| h.join().unwrap()).collect()
6053        });
6054
6055        for (r, (alone, concurrent)) in solo.iter().zip(together.iter()).enumerate() {
6056            assert_eq!(alone.len(), concurrent.len(), "request {r}: step count");
6057            for (step, (a, b)) in alone.iter().zip(concurrent.iter()).enumerate() {
6058                for (x, y) in a.iter().zip(b.iter()) {
6059                    assert_eq!(
6060                        x.to_bits(),
6061                        y.to_bits(),
6062                        "request {r} step {step}: sharing a store changed the answer"
6063                    );
6064                }
6065            }
6066        }
6067    }
6068
6069    /// Prefill then decode, returning every step's logits.
6070    fn run_one(
6071        decoder: &Decoder,
6072        prompt: &[usize],
6073        continuation: &[usize],
6074        caches: &mut [PagedKvCache],
6075        stores: &SharedPagedKv,
6076    ) -> Vec<Vec<f32>> {
6077        let mut out = vec![decoder
6078            .forward_batch_last_paged(prompt, 0, caches, stores)
6079            .expect("sized generously")];
6080        for (i, &tok) in continuation.iter().enumerate() {
6081            out.push(
6082                decoder
6083                    .forward_token_paged(tok, prompt.len() + i, caches, stores)
6084                    .expect("sized generously"),
6085            );
6086        }
6087        out
6088    }
6089
6090    /// A decode step the stores cannot hold advances NO layer.
6091    ///
6092    /// This was a real defect until the reservation moved into
6093    /// `forward_token_paged`: it pushed per layer with `?`, so a store
6094    /// exhausting at layer 1 of 2 left layer 0 holding a position layer
6095    /// 1 did not. Nothing downstream reports that -- the next step just
6096    /// attends over a shorter history in the tail layers -- and the
6097    /// prefill path had the guard while decode never did.
6098    ///
6099    /// Layer 0 is given room and layer 1 none, so the bug is reachable:
6100    /// with the reservation removed, layer 0 advances and layer 1
6101    /// refuses.
6102    #[test]
6103    fn a_decode_step_the_stores_cannot_hold_advances_no_layer() {
6104        let decoder = Decoder::new_random_small(tiny_test_config(), 2, 10);
6105        let mut caches: Vec<PagedKvCache> = (0..2).map(|_| PagedKvCache::new()).collect();
6106        // Block size 1 so "one more position" always needs a block.
6107        // Layer 0 gets two, layer 1 exactly one: the prompt fills layer
6108        // 1 completely, so the decode step below cannot fit there.
6109        let stores = SharedPagedKv::from_stores(
6110            [2usize, 1]
6111                .into_iter()
6112                .map(|blocks| {
6113                    PagedKvStore::new(
6114                        1,
6115                        blocks,
6116                        decoder.config.n_kv_heads,
6117                        decoder.config.head_dim,
6118                    )
6119                })
6120                .collect(),
6121        );
6122
6123        decoder
6124            .forward_batch_last_paged(&[1usize], 0, &mut caches, &stores)
6125            .expect("one position fits in both layers");
6126        assert_eq!(caches[0].seq_len(), 1);
6127        assert_eq!(caches[1].seq_len(), 1);
6128
6129        let result = decoder.forward_token_paged(2, 1, &mut caches, &stores);
6130        assert!(result.is_err(), "layer 1 has no block left");
6131        assert_eq!(
6132            caches[0].seq_len(),
6133            1,
6134            "layer 0 must not advance past a layer that could not"
6135        );
6136        assert_eq!(caches[1].seq_len(), 1);
6137    }
6138
6139    #[test]
6140    fn a_prefill_the_stores_cannot_hold_refuses_before_writing_any_layer() {
6141        let decoder = Decoder::new_random_small(tiny_test_config(), 2, 10);
6142        let prompt = [1usize, 2, 3, 4, 5, 6];
6143        let mut paged_caches: Vec<PagedKvCache> = (0..2).map(|_| PagedKvCache::new()).collect();
6144        // Layer 0 fits the prompt with room to spare; layer 1's two
6145        // blocks of 2 hold 4 positions against a prompt of 6.
6146        let stores = SharedPagedKv::from_stores(
6147            [8usize, 2]
6148                .into_iter()
6149                .map(|blocks| {
6150                    PagedKvStore::new(
6151                        2,
6152                        blocks,
6153                        decoder.config.n_kv_heads,
6154                        decoder.config.head_dim,
6155                    )
6156                })
6157                .collect(),
6158        );
6159
6160        let result = decoder.forward_batch_last_paged(&prompt, 0, &mut paged_caches, &stores);
6161        assert!(result.is_err(), "layer 1's store cannot hold the prompt");
6162        for (i, cache) in paged_caches.iter().enumerate() {
6163            assert_eq!(cache.seq_len(), 0, "layer {i} must be untouched");
6164            assert!(cache.block_table().is_empty(), "layer {i} holds no block");
6165        }
6166        for (i, expected) in [8usize, 2].into_iter().enumerate() {
6167            assert_eq!(stores.free_blocks(i), expected, "layer {i} leaked no block");
6168        }
6169    }
6170
6171    /// Chunked prefill: two calls appending into the same sequence must
6172    /// equal one call over the concatenation.
6173    ///
6174    /// This is the case the part-full tail block breaks if
6175    /// `to_contiguous` or the reservation is wrong, and it is how the
6176    /// serving path actually prefills long prompts.
6177    #[test]
6178    fn two_paged_prefill_chunks_equal_one_call_over_the_whole_prompt() {
6179        let decoder = Decoder::new_random_small(tiny_test_config(), 2, 10);
6180        let prompt = [3usize, 1, 4, 1, 5, 9, 2];
6181        let split = 3;
6182
6183        let run = |chunks: &[&[usize]]| {
6184            let mut caches: Vec<PagedKvCache> = (0..2).map(|_| PagedKvCache::new()).collect();
6185            let stores = SharedPagedKv::from_stores(
6186                (0..2)
6187                    .map(|_| {
6188                        PagedKvStore::new(2, 16, decoder.config.n_kv_heads, decoder.config.head_dim)
6189                    })
6190                    .collect(),
6191            );
6192            let mut pos = 0;
6193            let mut last = Vec::new();
6194            for chunk in chunks {
6195                last = decoder
6196                    .forward_batch_last_paged(chunk, pos, &mut caches, &stores)
6197                    .expect("sized generously");
6198                pos += chunk.len();
6199            }
6200            last
6201        };
6202
6203        let whole = run(&[&prompt]);
6204        let chunked = run(&[&prompt[..split], &prompt[split..]]);
6205        assert_eq!(whole.len(), chunked.len());
6206        for (x, y) in whole.iter().zip(chunked.iter()) {
6207            assert_eq!(
6208                x.to_bits(),
6209                y.to_bits(),
6210                "a chunked prefill must equal one call over the same tokens"
6211            );
6212        }
6213    }
6214
6215    fn paged_matches_contiguous(config: ModelConfig) {
6216        paged_matches_contiguous_with(config, |_| {});
6217    }
6218
6219    /// [`paged_matches_contiguous`] for the features that live on the
6220    /// WEIGHTS rather than in the config, and so cannot be switched on
6221    /// by handing a different `ModelConfig` in.
6222    fn paged_matches_contiguous_with(config: ModelConfig, prepare: impl FnOnce(&mut Decoder)) {
6223        let n_layers = 2;
6224        let mut decoder = Decoder::new_random_small(config, n_layers, 10);
6225        prepare(&mut decoder);
6226        let decoder = decoder;
6227
6228        let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
6229        let steps = [3usize, 5, 7, 2, 9, 1];
6230        let mut plain_logits = Vec::new();
6231        for (pos, &tok) in steps.iter().enumerate() {
6232            plain_logits.push(decoder.forward_token(tok, pos, &mut caches));
6233        }
6234
6235        let block_size = 2;
6236        let mut paged_caches: Vec<PagedKvCache> =
6237            (0..n_layers).map(|_| PagedKvCache::new()).collect();
6238        let stores = SharedPagedKv::from_stores(
6239            (0..n_layers)
6240                .map(|_| {
6241                    PagedKvStore::new(
6242                        block_size,
6243                        /* total_blocks = */ 16,
6244                        decoder.config.n_kv_heads,
6245                        decoder.config.head_dim,
6246                    )
6247                })
6248                .collect(),
6249        );
6250        let mut paged_logits = Vec::new();
6251        for (pos, &tok) in steps.iter().enumerate() {
6252            paged_logits.push(
6253                decoder
6254                    .forward_token_paged(tok, pos, &mut paged_caches, &stores)
6255                    .expect("store sized generously, must not exhaust"),
6256            );
6257        }
6258
6259        assert_eq!(plain_logits.len(), paged_logits.len());
6260        for (a, b) in plain_logits.iter().zip(paged_logits.iter()) {
6261            assert_eq!(a.len(), b.len());
6262            for (x, y) in a.iter().zip(b.iter()) {
6263                assert_eq!(
6264                    x.to_bits(),
6265                    y.to_bits(),
6266                    "paged decode must be bit-identical to contiguous decode"
6267                );
6268            }
6269        }
6270    }
6271
6272    /// The single most important correctness property of
6273    /// `forward_batch`: batching positions together for shared matmuls
6274    /// must produce EXACTLY the same result as processing them one at
6275    /// a time with `forward_token`, since causal masking guarantees
6276    /// position `i` only ever sees positions `<= i`. If this test
6277    /// fails, `forward_batch` is not a safe drop-in replacement for
6278    /// sequential decode, which would make speculative decoding built
6279    /// on top of it produce silently wrong output.
6280    /// How far a batched forward may sit from the per-token one.
6281    ///
6282    /// MEASURED rather than chosen: over these fixtures the largest
6283    /// deviation is 2.98e-8, single-ulp f32 on logits around 0.05,
6284    /// and it comes from the reduction order of a batched matmul
6285    /// against a per-token one. The bound is loose enough for that and
6286    /// for another machine's reduction order, and tight enough that a
6287    /// real drift in the batched path cannot hide under it.
6288    ///
6289    /// It was 1e-3 in four separate literals, five orders above the
6290    /// thing they were nominally checking, and two of the tests using
6291    /// it had "exactly" in their names.
6292    const BATCH_VS_SEQUENTIAL_ULP: f32 = 1e-6;
6293
6294    /// Batched and sequential agree to about one f32 ulp, NOT exactly.
6295    ///
6296    /// The name used to say "exactly" and the assertion allowed 1e-3,
6297    /// which was wrong in both directions at once: the two paths are
6298    /// not bit-identical (the sibling tests that really are say
6299    /// `bit_identical`, and mean it), and the real difference is five
6300    /// orders tighter than the bound that was nominally checking it.
6301    /// Measured over this fixture: the largest deviation is 1.5e-8,
6302    /// single-ulp on values around 0.05, arising from the reduction
6303    /// order of a batched matmul against a per-token one.
6304    ///
6305    /// The bound is 1e-6 now: loose enough for that ulp and for a
6306    /// different machine's reduction order, tight enough that an
6307    /// actual drift in the batched path cannot hide under it. This
6308    /// matters beyond hygiene -- the server verifies speculated tokens
6309    /// against logits from THIS function, so a near-tie inside this
6310    /// margin is the one case where a speculated token can disagree
6311    /// with what sequential decoding would have drawn.
6312    #[test]
6313    fn forward_batch_matches_sequential_forward_token_to_one_ulp() {
6314        let cfg = tiny_test_config();
6315        let vocab = 8;
6316        let tokens = [1usize, 3, 5, 2, 7];
6317
6318        let decoder_a = Decoder::new_random_small(cfg.clone(), 2, vocab);
6319        let mut caches_a: Vec<KvCache> = decoder_a.config.new_kv_caches();
6320        let sequential: Vec<Vec<f32>> = tokens
6321            .iter()
6322            .enumerate()
6323            .map(|(pos, &t)| decoder_a.forward_token(t, pos, &mut caches_a))
6324            .collect();
6325
6326        // A second decoder built with the same seed produces identical
6327        // weights (Decoder::new_random_small is deterministic), so
6328        // this is a fair like-for-like comparison against a fresh
6329        // cache rather than reusing decoder_a's now-mutated cache.
6330        let decoder_b = Decoder::new_random_small(cfg, 2, vocab);
6331        let mut caches_b: Vec<KvCache> = decoder_b.config.new_kv_caches();
6332        let batched = decoder_b.forward_batch(&tokens, 0, &mut caches_b);
6333
6334        assert_eq!(batched.len(), sequential.len());
6335        for (pos, (seq_logits, batch_logits)) in sequential.iter().zip(batched.iter()).enumerate() {
6336            assert_eq!(seq_logits.len(), batch_logits.len());
6337            for (i, (s, b)) in seq_logits.iter().zip(batch_logits.iter()).enumerate() {
6338                assert!(
6339                    (s - b).abs() < 1e-6,
6340                    "position {pos}, logit {i}: sequential={s} batched={b}"
6341                );
6342            }
6343        }
6344    }
6345
6346    /// `forward_batch_last` exists to skip the vocabulary projection for
6347    /// every position but the last, so the one thing that must hold is
6348    /// that the row it *does* produce is the same row `forward_batch`
6349    /// would have produced. It must also leave the KV cache in the same
6350    /// state -- prefill's whole purpose -- which is checked by decoding
6351    /// one more token from each cache and comparing.
6352    #[test]
6353    fn forward_batch_last_matches_the_final_row_of_forward_batch() {
6354        let cfg = tiny_test_config();
6355        let vocab = 16;
6356        let tokens = vec![1usize, 4, 7, 2, 9];
6357
6358        let decoder_a = Decoder::new_random_small(cfg.clone(), 2, vocab);
6359        let mut caches_a: Vec<KvCache> = decoder_a.config.new_kv_caches();
6360        let all_rows = decoder_a.forward_batch(&tokens, 0, &mut caches_a);
6361
6362        let decoder_b = Decoder::new_random_small(cfg, 2, vocab);
6363        let mut caches_b: Vec<KvCache> = decoder_b.config.new_kv_caches();
6364        let last = decoder_b.forward_batch_last(&tokens, 0, &mut caches_b);
6365
6366        let expected = all_rows.last().expect("one row per prompt token");
6367        assert_eq!(last.len(), expected.len());
6368        for (i, (a, b)) in expected.iter().zip(last.iter()).enumerate() {
6369            assert!(
6370                a == b,
6371                "logit {i}: forward_batch={a} forward_batch_last={b}"
6372            );
6373        }
6374
6375        // Same KV state: the next token's logits must agree too.
6376        let next_a = decoder_a.forward_token(3, tokens.len(), &mut caches_a);
6377        let next_b = decoder_b.forward_token(3, tokens.len(), &mut caches_b);
6378        for (i, (a, b)) in next_a.iter().zip(next_b.iter()).enumerate() {
6379            assert!(a == b, "post-prefill decode logit {i}: {a} vs {b}");
6380        }
6381
6382        // Empty prompt is the degenerate case both paths must survive.
6383        let mut caches_c: Vec<KvCache> = decoder_b.config.new_kv_caches();
6384        assert!(decoder_b
6385            .forward_batch_last(&[], 0, &mut caches_c)
6386            .is_empty());
6387    }
6388
6389    /// `forward_multi_seq`'s core correctness property: batching N
6390    /// independent sequences (different token histories, different
6391    /// current positions, different KV caches) together must produce
6392    /// EXACTLY the same per-sequence output as running each sequence
6393    /// through `forward_token` alone, one step at a time. This is what
6394    /// makes continuous batching safe -- no sequence's attention may
6395    /// ever be perturbed by another sequence sharing its batched
6396    /// matmul step.
6397    /// The PAGED batch step must equal the contiguous one, bit for bit.
6398    ///
6399    /// Continuous batching and paging are independent choices, so a
6400    /// deployment can have either, both or neither; if they disagree,
6401    /// the answer depends on two switches nobody thinks of as changing
6402    /// the model. Every sequence here is at a different position with a
6403    /// different length, which is the case the batched path exists for
6404    /// and the one where a shared-KV mistake would surface.
6405    #[test]
6406    fn a_paged_multi_seq_step_is_bit_identical_to_the_contiguous_one() {
6407        for cfg in [
6408            tiny_test_config(),
6409            {
6410                let mut c = tiny_test_config();
6411                c.sliding_window = Some(2);
6412                c.swa_layers = crate::swa_layers::SwaLayers::All;
6413                c
6414            },
6415            {
6416                let mut c = tiny_test_config();
6417                c.embedding_scale = Some(7.5);
6418                c.final_logit_softcap = Some(0.05);
6419                c.attn_logit_softcap = Some(0.05);
6420                c
6421            },
6422        ] {
6423            let n_layers = 2;
6424            let decoder = Decoder::new_random_small(cfg, n_layers, 10);
6425            let histories: [&[usize]; 3] = [&[1, 3, 5], &[2, 7], &[4, 4, 4, 6]];
6426            let next = [6usize, 1, 2];
6427
6428            // Contiguous: build each sequence's history, then one step.
6429            let mut contiguous: Vec<Vec<KvCache>> = histories
6430                .iter()
6431                .map(|h| {
6432                    let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
6433                    for (pos, &tok) in h.iter().enumerate() {
6434                        decoder.forward_token(tok, pos, &mut caches);
6435                    }
6436                    caches
6437                })
6438                .collect();
6439            let positions: Vec<usize> = histories.iter().map(|h| h.len()).collect();
6440            let want = decoder.forward_multi_seq(&next, &positions, &mut contiguous);
6441
6442            // Paged: same histories through the paged decode path, then
6443            // one batched step over the shared store.
6444            let stores = SharedPagedKv::new(
6445                n_layers,
6446                /* block_size = */ 2,
6447                /* blocks_per_layer = */ 64,
6448                decoder.config.n_kv_heads,
6449                decoder.config.head_dim,
6450            );
6451            let mut paged: Vec<Vec<PagedKvCache>> = histories
6452                .iter()
6453                .map(|h| {
6454                    let mut caches: Vec<PagedKvCache> =
6455                        (0..n_layers).map(|_| PagedKvCache::new()).collect();
6456                    for (pos, &tok) in h.iter().enumerate() {
6457                        decoder
6458                            .forward_token_paged(tok, pos, &mut caches, &stores)
6459                            .expect("sized generously");
6460                    }
6461                    caches
6462                })
6463                .collect();
6464            let got = decoder.forward_multi_seq_kv(
6465                &next,
6466                &positions,
6467                &mut MultiSeqKv::Paged {
6468                    caches: &mut paged,
6469                    stores: &stores,
6470                },
6471            );
6472
6473            assert_eq!(want.len(), got.len());
6474            for (s, (a, b)) in want.iter().zip(got.iter()).enumerate() {
6475                assert_eq!(a.len(), b.len(), "sequence {s}: logit count");
6476                for (x, y) in a.iter().zip(b.iter()) {
6477                    assert_eq!(
6478                        x.to_bits(),
6479                        y.to_bits(),
6480                        "sequence {s}: paged batching changed the answer"
6481                    );
6482                }
6483            }
6484        }
6485    }
6486
6487    #[test]
6488    fn forward_multi_seq_matches_independent_forward_token_per_sequence() {
6489        let cfg = tiny_test_config();
6490        let vocab = 8;
6491        // 3 independent sequences, deliberately different lengths/
6492        // histories/current tokens, so no two sequences are at the
6493        // same position when batched together.
6494        let seq_histories: [&[usize]; 3] = [&[1, 3, 5], &[2, 7], &[4, 4, 4, 6]];
6495
6496        let decoder_a = Decoder::new_random_small(cfg.clone(), 2, vocab);
6497        let mut independent_logits: Vec<Vec<f32>> = Vec::new();
6498        for history in seq_histories.iter() {
6499            let mut caches: Vec<KvCache> = decoder_a.config.new_kv_caches();
6500            let mut logits = Vec::new();
6501            for (pos, &tok) in history.iter().enumerate() {
6502                logits = decoder_a.forward_token(tok, pos, &mut caches);
6503            }
6504            independent_logits.push(logits);
6505        }
6506
6507        // Same seed -> identical weights, fresh caches for a fair
6508        // comparison (mirrors forward_batch_matches_sequential_forward_token_exactly).
6509        let decoder_b = Decoder::new_random_small(cfg, 2, vocab);
6510        let mut per_seq_caches: Vec<Vec<KvCache>> = seq_histories
6511            .iter()
6512            .map(|_| decoder_b.config.new_kv_caches())
6513            .collect();
6514
6515        // Feed every sequence's prefix (all but its last token)
6516        // through forward_multi_seq one shared step at a time, then
6517        // do a final batched step for the last token of every
6518        // sequence so all three arrive at their final position in
6519        // the same batched call -- exercising genuinely different
6520        // per-sequence positions/histories within one batch, not just
6521        // parallel identical-length sequences.
6522        let max_len = seq_histories.iter().map(|h| h.len()).max().unwrap();
6523        let mut batched_logits: Vec<Vec<f32>> = vec![Vec::new(); seq_histories.len()];
6524        for step in 0..max_len {
6525            let mut tokens = Vec::new();
6526            let mut positions = Vec::new();
6527            let mut active: Vec<usize> = Vec::new();
6528            for (s, history) in seq_histories.iter().enumerate() {
6529                if step < history.len() {
6530                    tokens.push(history[step]);
6531                    positions.push(step);
6532                    active.push(s);
6533                }
6534            }
6535            if tokens.is_empty() {
6536                continue;
6537            }
6538            let mut active_caches: Vec<Vec<KvCache>> = active
6539                .iter()
6540                .map(|&s| std::mem::take(&mut per_seq_caches[s]))
6541                .collect();
6542            let step_logits = decoder_b.forward_multi_seq(&tokens, &positions, &mut active_caches);
6543            for ((&s, caches), logits) in active.iter().zip(active_caches).zip(step_logits) {
6544                per_seq_caches[s] = caches;
6545                batched_logits[s] = logits;
6546            }
6547        }
6548
6549        assert_eq!(batched_logits.len(), independent_logits.len());
6550        for (s, (seq_logits, batch_logits)) in independent_logits
6551            .iter()
6552            .zip(batched_logits.iter())
6553            .enumerate()
6554        {
6555            assert_eq!(seq_logits.len(), batch_logits.len());
6556            for (i, (a, b)) in seq_logits.iter().zip(batch_logits.iter()).enumerate() {
6557                assert!(
6558                    (a - b).abs() < 1e-3,
6559                    "sequence {s}, logit {i}: independent={a} batched={b}"
6560                );
6561            }
6562        }
6563    }
6564
6565    /// The gap the decoration audit found, from the side the existing
6566    /// guard could not see.
6567    ///
6568    /// `the_paged_path_keeps_every_per_layer_feature_the_contiguous_one_applies`
6569    /// sets `attention_scale` and compares `forward_token` against
6570    /// `forward_token_paged` -- the two bodies that AGREED. It never
6571    /// compared them against `forward_hidden_batch_inner`, which applied
6572    /// the scale nowhere, so a Gemma-shaped checkpoint would answer at
6573    /// one temperature when decoded a token at a time and at another
6574    /// when its prompt was prefilled. Not an error; a plausible
6575    /// distribution from the wrong model.
6576    ///
6577    /// The first assertion is the one that makes this a guard rather
6578    /// than an assertion: 0.37 is well away from the kernel's own
6579    /// `1/sqrt(head_dim)`, so if setting it does not move the logits
6580    /// then both sides are ignoring it and the comparison below proves
6581    /// nothing.
6582    /// The Metal attention kernels infer Q/K norm style from the weight
6583    /// LENGTH; the host branches on `ModelConfig::qk_norm_style`. Two
6584    /// mechanisms for one decision, so they have to agree.
6585    ///
6586    /// They do, and not by luck: `loader.rs`'s `refined_qk_norm` DERIVES
6587    /// the enum from the same length rule, and refuses to load anything
6588    /// that matches neither width. This pins that, because the failure
6589    /// would be silent and would land on audited architectures --
6590    /// OLMoE is whole-vector, Qwen3 and Gemma-3 are per-head, and all
6591    /// three are in `AUDITED_GENERIC_GQA`, so an inference that assumed
6592    /// one style would answer wrong on the others at full speed.
6593    ///
6594    /// Raised by the decoration audit as unverifiable from the host
6595    /// side, which is exactly why it is written down here rather than
6596    /// left as a comment on one of the two sides.
6597    #[test]
6598    fn the_metal_qk_norm_length_rule_is_the_one_the_loader_derives_the_style_from() {
6599        use crate::capability::QkNormStyle;
6600        let head_dim = 8usize;
6601        let n_heads = 4usize;
6602
6603        // The rule `frink-metal/src/attn.rs` applies, transcribed.
6604        let metal_says_per_head = |len: usize| len == head_dim;
6605        // The rule `loader.rs::refined_qk_norm` applies, transcribed.
6606        let loader_style = |len: usize| -> Option<QkNormStyle> {
6607            if len == head_dim {
6608                Some(QkNormStyle::PerHead)
6609            } else if len == n_heads * head_dim {
6610                Some(QkNormStyle::WholeVector)
6611            } else {
6612                None
6613            }
6614        };
6615
6616        for len in [head_dim, n_heads * head_dim] {
6617            let style = loader_style(len).expect("both widths load");
6618            assert_eq!(
6619                metal_says_per_head(len),
6620                style == QkNormStyle::PerHead,
6621                "length {len} loads as {style:?} but Metal would infer the other style"
6622            );
6623        }
6624
6625        // A width neither side handles must be refused at load rather
6626        // than reaching a kernel that would pick a branch anyway.
6627        assert!(
6628            loader_style(head_dim + 1).is_none(),
6629            "an unrecognised norm width must be a load error, not a coin flip"
6630        );
6631
6632        // The one ambiguous case, and it is harmless: with a single
6633        // head the two widths coincide, so both rules take their PerHead
6634        // branch and per-head RMS over one head IS whole-vector RMS.
6635        let single_head = |len: usize| len == head_dim;
6636        assert!(single_head(head_dim));
6637        assert_eq!(
6638            loader_style(head_dim),
6639            Some(QkNormStyle::PerHead),
6640            "with n_heads == 1 both widths are head_dim, and both sides must land \
6641             on the same branch rather than one falling through"
6642        );
6643    }
6644
6645    #[test]
6646    fn the_batched_path_applies_attention_scale_like_the_contiguous_one() {
6647        let vocab = 8;
6648        let tokens = [1usize, 3, 5, 2, 7];
6649        let scaled = || {
6650            let mut cfg = tiny_test_config();
6651            // Far from the kernel's own 1/sqrt(head_dim) on purpose:
6652            // at this model's scale a scalar near 1 moves the logits by
6653            // ~2e-4, which is below the noise a tolerance test can see.
6654            cfg.attention_scale = Some(8.0);
6655            cfg
6656        };
6657        let fresh_caches = |d: &Decoder| -> Vec<KvCache> { d.config.new_kv_caches() };
6658
6659        // Same seed -> identical weights, so the only difference between
6660        // these three decoders is the config field under test.
6661        let seq_decoder = Decoder::new_random_small(scaled(), 2, vocab);
6662        let mut seq_caches = fresh_caches(&seq_decoder);
6663        let sequential: Vec<Vec<f32>> = tokens
6664            .iter()
6665            .enumerate()
6666            .map(|(pos, &t)| seq_decoder.forward_token(t, pos, &mut seq_caches))
6667            .collect();
6668
6669        let batch_decoder = Decoder::new_random_small(scaled(), 2, vocab);
6670        let mut batch_caches = fresh_caches(&batch_decoder);
6671        let batched = batch_decoder.forward_batch(&tokens, 0, &mut batch_caches);
6672
6673        let plain_decoder = Decoder::new_random_small(tiny_test_config(), 2, vocab);
6674        let mut plain_caches = fresh_caches(&plain_decoder);
6675        let unscaled = plain_decoder.forward_batch(&tokens, 0, &mut plain_caches);
6676        assert!(
6677            batched
6678                .iter()
6679                .zip(unscaled.iter())
6680                .any(|(s, u)| s.iter().zip(u.iter()).any(|(a, b)| (a - b).abs() > 1e-3)),
6681            "attention_scale must change the batched answer, or this test cannot fail"
6682        );
6683
6684        assert_eq!(batched.len(), sequential.len());
6685        for (pos, (seq_logits, batch_logits)) in sequential.iter().zip(batched.iter()).enumerate() {
6686            assert_eq!(seq_logits.len(), batch_logits.len());
6687            for (i, (s, b)) in seq_logits.iter().zip(batch_logits.iter()).enumerate() {
6688                assert!(
6689                    (s - b).abs() < 1e-5,
6690                    "position {pos}, logit {i}: sequential={s} batched={b}"
6691                );
6692            }
6693        }
6694    }
6695
6696    /// [`the_batched_path_applies_attention_scale_like_the_contiguous_one`]
6697    /// for the fourth host body.
6698    ///
6699    /// `forward_multi_seq_kv` did not apply `attention_scale` either, so
6700    /// a served request answered differently the moment it was batched
6701    /// with another request -- the same weights, the same position, a
6702    /// different temperature, decided by how busy the server was.
6703    #[test]
6704    fn the_multi_seq_path_applies_attention_scale_like_the_contiguous_one() {
6705        let vocab = 8;
6706        let histories: [&[usize]; 3] = [&[1, 3, 5], &[2, 7], &[4, 4, 4, 6]];
6707        let next = [6usize, 1, 2];
6708        let n_layers = 2;
6709        let scaled = || {
6710            let mut cfg = tiny_test_config();
6711            // See the batched twin: a scalar near 1 does not move this
6712            // model's logits far enough for a tolerance to see it.
6713            cfg.attention_scale = Some(8.0);
6714            cfg
6715        };
6716
6717        // Builds every sequence's history with `forward_token`, then
6718        // takes the next step either per sequence or as one batch.
6719        let run = |cfg: ModelConfig, batched: bool| -> Vec<Vec<f32>> {
6720            let decoder = Decoder::new_random_small(cfg, n_layers, vocab);
6721            let mut per_seq: Vec<Vec<KvCache>> = histories
6722                .iter()
6723                .map(|h| {
6724                    let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
6725                    for (pos, &tok) in h.iter().enumerate() {
6726                        decoder.forward_token(tok, pos, &mut caches);
6727                    }
6728                    caches
6729                })
6730                .collect();
6731            let positions: Vec<usize> = histories.iter().map(|h| h.len()).collect();
6732            if batched {
6733                decoder.forward_multi_seq(&next, &positions, &mut per_seq)
6734            } else {
6735                next.iter()
6736                    .zip(positions.iter())
6737                    .zip(per_seq.iter_mut())
6738                    .map(|((&tok, &pos), caches)| decoder.forward_token(tok, pos, caches))
6739                    .collect()
6740            }
6741        };
6742
6743        let want = run(scaled(), false);
6744        let got = run(scaled(), true);
6745        let unscaled = run(tiny_test_config(), true);
6746
6747        assert!(
6748            got.iter()
6749                .zip(unscaled.iter())
6750                .any(|(g, u)| g.iter().zip(u.iter()).any(|(a, b)| (a - b).abs() > 1e-3)),
6751            "attention_scale must change the multi-seq answer, or this test cannot fail"
6752        );
6753
6754        assert_eq!(want.len(), got.len());
6755        for (s, (a, b)) in want.iter().zip(got.iter()).enumerate() {
6756            assert_eq!(a.len(), b.len(), "sequence {s}: logit count");
6757            for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
6758                assert!(
6759                    (x - y).abs() < 1e-5,
6760                    "sequence {s}, logit {i}: independent={x} batched={y}"
6761                );
6762            }
6763        }
6764    }
6765
6766    /// The predicate that decides whether a MoE layer may be routed by
6767    /// the GPU must admit ONLY the routing the GPU actually computes.
6768    ///
6769    /// Every Metal MoE path -- `launch_moe_decode_stack`,
6770    /// `launch_moe_decode_layer_fused`, `launch_moe_prefill_q4_0` and
6771    /// the fused prefill stack -- routes with a plain top-k softmax over
6772    /// the raw router logits. `Decoder::route_for_layer` has three more
6773    /// arms: grouped routing, a per-expert router bias, and
6774    /// `expert_weights_scale`. The audit found those four call sites
6775    /// disagreeing about which of the three to refuse -- prefill checked
6776    /// all three, the fused decode layer checked two, the whole-stack
6777    /// decode checked none -- so a Softmax-gated MoE checkpoint carrying
6778    /// a router bias would have routed to different experts on Metal
6779    /// than on CPU, with no error.
6780    ///
6781    /// This asserts the invariant directly rather than the predicate's
6782    /// spelling: whenever it says yes, plain `route_top_k` and
6783    /// `route_for_layer` must return the same decision; and each of the
6784    /// three features on its own must make it say no.
6785    #[test]
6786    fn the_gpu_router_predicate_admits_only_routing_it_reproduces() {
6787        // `tiny_test_config` is GLM-shaped and so gates with sigmoid;
6788        // the GPU router implements softmax, so start from the case the
6789        // predicate is supposed to ADMIT.
6790        let mut base = tiny_test_config();
6791        base.moe.gating = frink_moe::GatingFunction::Softmax;
6792        let decoder = Decoder::new_random_small(base.clone(), 2, 8);
6793        let plain_layer = &decoder.layers[0];
6794        let n_experts = base.moe.n_experts;
6795        // Chosen so each feature really bites: the top two experts sit
6796        // in DIFFERENT groups of two (so grouped routing must reorder
6797        // them), and the runners-up are close enough behind that a
6798        // per-expert bias flips the order.
6799        assert_eq!(n_experts, 6, "the logits below are written for six experts");
6800        let logits: Vec<f32> = vec![0.90, 0.10, 0.20, 0.85, 0.30, 0.05];
6801
6802        let agrees = |layer: &LayerWeights, cfg: &ModelConfig| -> bool {
6803            let host = Decoder::route_for_layer(layer, &logits, cfg);
6804            let gpu = route_top_k(
6805                &logits,
6806                cfg.moe.n_experts_active,
6807                cfg.moe.gating,
6808                cfg.moe.norm_topk_prob,
6809            );
6810            host.expert_ids == gpu.expert_ids
6811                && host.weights.len() == gpu.weights.len()
6812                && host
6813                    .weights
6814                    .iter()
6815                    .zip(gpu.weights.iter())
6816                    .all(|(a, b)| a.to_bits() == b.to_bits())
6817        };
6818
6819        // The admitted case: the predicate says yes, and the two
6820        // routers really do agree.
6821        assert!(
6822            Decoder::gpu_router_matches_host_routing(plain_layer, &base),
6823            "a plain softmax MoE layer must stay eligible, or this test proves nothing"
6824        );
6825        assert!(agrees(plain_layer, &base));
6826
6827        // A per-expert router bias.
6828        let mut biased_decoder = Decoder::new_random_small(base.clone(), 2, 8);
6829        biased_decoder.layers[0].moe.exp_probs_bias =
6830            Some((0..n_experts).map(|e| 0.9 - 0.4 * e as f32).collect());
6831        let biased_layer = &biased_decoder.layers[0];
6832        assert!(
6833            !Decoder::gpu_router_matches_host_routing(biased_layer, &base),
6834            "exp_probs_bias must make the layer ineligible for the GPU router"
6835        );
6836        assert!(
6837            !agrees(biased_layer, &base),
6838            "the bias must actually change the routing, or the check above is vacuous"
6839        );
6840
6841        // `expert_weights_scale`.
6842        let mut scaled = base.clone();
6843        scaled.moe.expert_weights_scale = 2.5;
6844        assert!(
6845            !Decoder::gpu_router_matches_host_routing(plain_layer, &scaled),
6846            "expert_weights_scale must make the layer ineligible for the GPU router"
6847        );
6848        assert!(
6849            !agrees(plain_layer, &scaled),
6850            "the scale must actually change the routing, or the check above is vacuous"
6851        );
6852
6853        // Grouped routing.
6854        let mut grouped = base.clone();
6855        grouped.moe.expert_group_count = Some(3);
6856        grouped.moe.expert_group_used_count = Some(1);
6857        assert!(
6858            !Decoder::gpu_router_matches_host_routing(plain_layer, &grouped),
6859            "grouped routing must make the layer ineligible for the GPU router"
6860        );
6861        assert!(
6862            !agrees(plain_layer, &grouped),
6863            "the grouping must actually change the routing, or the check above is vacuous"
6864        );
6865
6866        // A non-softmax gate: the GPU kernel implements softmax only.
6867        let mut sigmoid = base.clone();
6868        sigmoid.moe.gating = frink_moe::GatingFunction::Sigmoid;
6869        assert!(
6870            !Decoder::gpu_router_matches_host_routing(plain_layer, &sigmoid),
6871            "a non-softmax gate must make the layer ineligible for the GPU router"
6872        );
6873
6874        // A router that reads the raw layer input (`smallthinker`):
6875        // every GPU router reads `normed2`, so the SAME logits routed
6876        // the same way are still the wrong experts. `agrees` cannot see
6877        // this one -- it compares two routers over one logit vector --
6878        // which is exactly why the predicate has to.
6879        let mut raw = base.clone();
6880        raw.router_input = crate::router_input::RouterInput::RawLayerInput;
6881        assert!(
6882            !Decoder::gpu_router_matches_host_routing(plain_layer, &raw),
6883            "a raw-layer-input router must make the layer ineligible for the GPU router"
6884        );
6885
6886        // A router (and experts) reading the normed layer input
6887        // (`arctic`): the same predicate, for the same reason, and the
6888        // experts would read the wrong tensor too.
6889        let mut branch = base;
6890        branch.router_input = crate::router_input::RouterInput::NormedLayerInput;
6891        assert!(
6892            !Decoder::gpu_router_matches_host_routing(plain_layer, &branch),
6893            "a normed-layer-input branch must make the layer ineligible for the GPU router"
6894        );
6895    }
6896
6897    /// OLMoE-style QK-norm (`attn_q_norm`/`attn_k_norm`, see `AttnWeights`'
6898    /// doc comment): with both set, `forward_batch` must still match
6899    /// sequential `forward_token` calls exactly -- the same consistency
6900    /// property `forward_batch_matches_sequential_forward_token_exactly`
6901    /// checks for the no-QK-norm path, now exercising the norm-applied
6902    /// per-row slicing (`q_batch.chunks_mut(q_width)`,
6903    /// `k_batch.chunks_mut(kv_width)`) instead of trusting it by
6904    /// inspection.
6905    #[test]
6906    fn forward_batch_matches_forward_token_with_qk_norm_present() {
6907        let cfg = tiny_test_config();
6908        let vocab = 8;
6909        let tokens = [1usize, 3, 5, 2, 7];
6910        let q_width = cfg.n_heads * cfg.head_dim;
6911        let kv_width = cfg.n_kv_heads * cfg.head_dim;
6912
6913        let mut decoder_a = Decoder::new_random_small(cfg.clone(), 2, vocab);
6914        for layer in &mut decoder_a.layers {
6915            layer.attn.q_norm = Some((0..q_width).map(|i| 1.0 + i as f32 * 0.1).collect());
6916            layer.attn.k_norm = Some((0..kv_width).map(|i| 0.5 + i as f32 * 0.05).collect());
6917        }
6918        let mut caches_a: Vec<KvCache> = decoder_a.config.new_kv_caches();
6919        let sequential: Vec<Vec<f32>> = tokens
6920            .iter()
6921            .enumerate()
6922            .map(|(pos, &t)| decoder_a.forward_token(t, pos, &mut caches_a))
6923            .collect();
6924
6925        let mut decoder_b = Decoder::new_random_small(cfg, 2, vocab);
6926        for layer in &mut decoder_b.layers {
6927            layer.attn.q_norm = Some((0..q_width).map(|i| 1.0 + i as f32 * 0.1).collect());
6928            layer.attn.k_norm = Some((0..kv_width).map(|i| 0.5 + i as f32 * 0.05).collect());
6929        }
6930        let mut caches_b: Vec<KvCache> = decoder_b.config.new_kv_caches();
6931        let batched = decoder_b.forward_batch(&tokens, 0, &mut caches_b);
6932
6933        assert_eq!(batched.len(), sequential.len());
6934        for (pos, (seq_logits, batch_logits)) in sequential.iter().zip(batched.iter()).enumerate() {
6935            for (i, (s, b)) in seq_logits.iter().zip(batch_logits.iter()).enumerate() {
6936                assert!(
6937                    (s - b).abs() < BATCH_VS_SEQUENTIAL_ULP,
6938                    "position {pos}, logit {i}: sequential={s} batched={b}"
6939                );
6940            }
6941        }
6942    }
6943
6944    /// QK-norm being present must actually change the output -- otherwise
6945    /// the `Some(...)` branches in `forward_token`/`forward_batch` could
6946    /// silently be dead code and this feature would ship unverified. Must
6947    /// decode at least 2 positions: at position 0 with a fresh cache,
6948    /// causal softmax has exactly one candidate (the token attending to
6949    /// itself) and always evaluates to weight 1.0 regardless of the Q*K
6950    /// dot product -- so the attention output there is Q/K-invariant by
6951    /// construction, and a single-position version of this test would
6952    /// pass even with `q_norm`/`k_norm` silently never applied.
6953    #[test]
6954    fn qk_norm_present_changes_output_versus_absent() {
6955        let cfg = tiny_test_config();
6956        let vocab = 8;
6957        let q_width = cfg.n_heads * cfg.head_dim;
6958        let kv_width = cfg.n_kv_heads * cfg.head_dim;
6959        let tokens = [3usize, 5];
6960
6961        let without_norm = Decoder::new_random_small(cfg.clone(), 1, vocab);
6962        let mut with_norm = Decoder::new_random_small(cfg, 1, vocab);
6963        for layer in &mut with_norm.layers {
6964            layer.attn.q_norm = Some(vec![2.0; q_width]);
6965            layer.attn.k_norm = Some(vec![2.0; kv_width]);
6966        }
6967
6968        let mut caches_a: Vec<KvCache> = without_norm.config.new_kv_caches();
6969        let mut caches_b: Vec<KvCache> = with_norm.config.new_kv_caches();
6970
6971        let mut out_a = Vec::new();
6972        let mut out_b = Vec::new();
6973        for (pos, &t) in tokens.iter().enumerate() {
6974            out_a = without_norm.forward_token(t, pos, &mut caches_a);
6975            out_b = with_norm.forward_token(t, pos, &mut caches_b);
6976        }
6977
6978        let differs = out_a
6979            .iter()
6980            .zip(out_b.iter())
6981            .any(|(a, b)| (a - b).abs() > 1e-4);
6982        assert!(
6983            differs,
6984            "QK-norm weights changed nothing -- forward_token likely isn't applying q_norm/k_norm"
6985        );
6986    }
6987
6988    /// Qwen2/Qwen2-MoE-family QKV attention bias (`AttnWeights::q_bias`/
6989    /// `k_bias`/`v_bias`): a real, previously-unhandled gap found by
6990    /// running frink's generic GGUF loader against a real downloaded
6991    /// Qwen1.5-MoE-A2.7B-Chat checkpoint, which produced fluent-but-wrong
6992    /// output because these real `attn_{q,k,v}.bias` tensors were
6993    /// silently never added anywhere. Same two real properties checked
6994    /// as the QK-norm tests above: (1) `forward_batch` must match
6995    /// sequential `forward_token` exactly with bias present (batched
6996    /// per-row broadcast must be correct, not just the single-token
6997    /// path), and (2) bias must actually change the output at position
6998    /// 0 or later (not silently dead code) -- checked at position 1
6999    /// specifically, since position 0's causal softmax has exactly one
7000    /// candidate and is Q/K-invariant regardless of any additive bias
7001    /// shifting Q/K, for the same reason the QK-norm test above needs
7002    /// >=2 positions.
7003    #[test]
7004    fn forward_batch_matches_forward_token_with_qkv_bias_present() {
7005        let cfg = tiny_test_config();
7006        let vocab = 8;
7007        let tokens = [1usize, 3, 5, 2, 7];
7008        let q_width = cfg.n_heads * cfg.head_dim;
7009        let kv_width = cfg.n_kv_heads * cfg.head_dim;
7010
7011        let mut decoder_a = Decoder::new_random_small(cfg.clone(), 2, vocab);
7012        for layer in &mut decoder_a.layers {
7013            layer.attn.q_bias = Some((0..q_width).map(|i| 0.3 + i as f32 * 0.02).collect());
7014            layer.attn.k_bias = Some((0..kv_width).map(|i| -0.2 + i as f32 * 0.03).collect());
7015            layer.attn.v_bias = Some((0..kv_width).map(|i| 0.1 - i as f32 * 0.01).collect());
7016        }
7017        let mut caches_a: Vec<KvCache> = decoder_a.config.new_kv_caches();
7018        let sequential: Vec<Vec<f32>> = tokens
7019            .iter()
7020            .enumerate()
7021            .map(|(pos, &t)| decoder_a.forward_token(t, pos, &mut caches_a))
7022            .collect();
7023
7024        let mut decoder_b = Decoder::new_random_small(cfg, 2, vocab);
7025        for layer in &mut decoder_b.layers {
7026            layer.attn.q_bias = Some((0..q_width).map(|i| 0.3 + i as f32 * 0.02).collect());
7027            layer.attn.k_bias = Some((0..kv_width).map(|i| -0.2 + i as f32 * 0.03).collect());
7028            layer.attn.v_bias = Some((0..kv_width).map(|i| 0.1 - i as f32 * 0.01).collect());
7029        }
7030        let mut caches_b: Vec<KvCache> = decoder_b.config.new_kv_caches();
7031        let batched = decoder_b.forward_batch(&tokens, 0, &mut caches_b);
7032
7033        assert_eq!(batched.len(), sequential.len());
7034        for (pos, (seq_logits, batch_logits)) in sequential.iter().zip(batched.iter()).enumerate() {
7035            for (i, (s, b)) in seq_logits.iter().zip(batch_logits.iter()).enumerate() {
7036                assert!(
7037                    (s - b).abs() < BATCH_VS_SEQUENTIAL_ULP,
7038                    "position {pos}, logit {i}: sequential={s} batched={b}"
7039                );
7040            }
7041        }
7042    }
7043
7044    #[test]
7045    fn qkv_bias_present_changes_output_versus_absent() {
7046        let cfg = tiny_test_config();
7047        let vocab = 8;
7048        let q_width = cfg.n_heads * cfg.head_dim;
7049        let kv_width = cfg.n_kv_heads * cfg.head_dim;
7050        let tokens = [3usize, 5];
7051
7052        let without_bias = Decoder::new_random_small(cfg.clone(), 1, vocab);
7053        let mut with_bias = Decoder::new_random_small(cfg, 1, vocab);
7054        for layer in &mut with_bias.layers {
7055            layer.attn.q_bias = Some(vec![0.5; q_width]);
7056            layer.attn.k_bias = Some(vec![0.5; kv_width]);
7057            layer.attn.v_bias = Some(vec![0.5; kv_width]);
7058        }
7059
7060        let mut caches_a: Vec<KvCache> = without_bias.config.new_kv_caches();
7061        let mut caches_b: Vec<KvCache> = with_bias.config.new_kv_caches();
7062
7063        let mut out_a = Vec::new();
7064        let mut out_b = Vec::new();
7065        for (pos, &t) in tokens.iter().enumerate() {
7066            out_a = without_bias.forward_token(t, pos, &mut caches_a);
7067            out_b = with_bias.forward_token(t, pos, &mut caches_b);
7068        }
7069
7070        let differs = out_a
7071            .iter()
7072            .zip(out_b.iter())
7073            .any(|(a, b)| (a - b).abs() > 1e-4);
7074        assert!(
7075            differs,
7076            "QKV bias changed nothing -- forward_token likely isn't applying q_bias/k_bias/v_bias"
7077        );
7078    }
7079
7080    #[test]
7081    fn forward_batch_and_forward_token_leave_kv_caches_in_the_same_state() {
7082        let cfg = tiny_test_config();
7083        let tokens = [2usize, 4, 6];
7084
7085        let decoder_a = Decoder::new_random_small(cfg.clone(), 2, 8);
7086        let mut caches_a: Vec<KvCache> = decoder_a.config.new_kv_caches();
7087        for (pos, &t) in tokens.iter().enumerate() {
7088            decoder_a.forward_token(t, pos, &mut caches_a);
7089        }
7090
7091        let decoder_b = Decoder::new_random_small(cfg, 2, 8);
7092        let mut caches_b: Vec<KvCache> = decoder_b.config.new_kv_caches();
7093        decoder_b.forward_batch(&tokens, 0, &mut caches_b);
7094
7095        for (ca, cb) in caches_a.iter().zip(caches_b.iter()) {
7096            assert_eq!(ca.positions(), cb.positions());
7097            assert_eq!(ca.k.len(), cb.k.len());
7098            for (a, b) in ca.k.iter().zip(cb.k.iter()) {
7099                assert!((a - b).abs() < 1e-4);
7100            }
7101        }
7102    }
7103
7104    /// Same architecture shape as `tiny_test_config` but genuinely
7105    /// dense (one expert, no shared experts) -- the shape every non-MoE
7106    /// model, and every DeepSeek-style leading dense layer, loads as.
7107    /// Exercises `Decoder::is_dense_layer`'s fast path.
7108    fn tiny_dense_test_config() -> ModelConfig {
7109        let mut cfg = tiny_test_config();
7110        cfg.moe.n_experts = 1;
7111        cfg.moe.n_experts_active = 1;
7112        cfg.moe.n_shared_experts = 0;
7113        cfg
7114    }
7115
7116    #[test]
7117    fn dense_layer_forward_pass_produces_finite_logits_of_correct_shape() {
7118        let vocab = 10;
7119        let decoder = Decoder::new_random_small(tiny_dense_test_config(), 2, vocab);
7120        let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
7121
7122        let logits = decoder.forward_token(3, 0, &mut caches);
7123        assert_eq!(logits.len(), vocab);
7124        assert!(
7125            logits.iter().all(|v| v.is_finite()),
7126            "logits must not contain NaN/Inf"
7127        );
7128    }
7129
7130    #[test]
7131    fn dense_layer_forward_batch_matches_sequential_forward_token_to_one_ulp() {
7132        let cfg = tiny_dense_test_config();
7133        let vocab = 8;
7134        let tokens = [1usize, 3, 5, 2, 7];
7135
7136        let decoder_a = Decoder::new_random_small(cfg.clone(), 2, vocab);
7137        let mut caches_a: Vec<KvCache> = decoder_a.config.new_kv_caches();
7138        let sequential: Vec<Vec<f32>> = tokens
7139            .iter()
7140            .enumerate()
7141            .map(|(pos, &t)| decoder_a.forward_token(t, pos, &mut caches_a))
7142            .collect();
7143
7144        let decoder_b = Decoder::new_random_small(cfg, 2, vocab);
7145        let mut caches_b: Vec<KvCache> = decoder_b.config.new_kv_caches();
7146        let batched = decoder_b.forward_batch(&tokens, 0, &mut caches_b);
7147
7148        assert_eq!(batched.len(), sequential.len());
7149        for (pos, (seq_logits, batch_logits)) in sequential.iter().zip(batched.iter()).enumerate() {
7150            for (i, (s, b)) in seq_logits.iter().zip(batch_logits.iter()).enumerate() {
7151                assert!(
7152                    (s - b).abs() < BATCH_VS_SEQUENTIAL_ULP,
7153                    "position {pos}, logit {i}: sequential={s} batched={b}"
7154                );
7155            }
7156        }
7157    }
7158
7159    #[test]
7160    fn dense_layer_fast_path_still_records_expert_zero_activations() {
7161        // The dense fast path bypasses `route_top_k` entirely, but
7162        // must still record an activation for expert 0 every step --
7163        // `MoeWeights::placement_plan` and hotness-based GPU placement
7164        // depend on this being real for every model shape, not just
7165        // genuinely-MoE ones.
7166        let decoder = Decoder::new_random_small(tiny_dense_test_config(), 1, 8);
7167        let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
7168
7169        decoder.forward_token(0, 0, &mut caches);
7170        decoder.forward_token(1, 1, &mut caches);
7171        decoder.forward_token(2, 2, &mut caches);
7172
7173        let count =
7174            decoder.layers[0].moe.activation_counts[0].load(std::sync::atomic::Ordering::Relaxed);
7175        assert_eq!(count, 3);
7176    }
7177
7178    #[test]
7179    fn forward_batch_with_empty_tokens_returns_empty() {
7180        let decoder = Decoder::new_random_small(tiny_test_config(), 2, 8);
7181        let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
7182        let out = decoder.forward_batch(&[], 0, &mut caches);
7183        assert!(out.is_empty());
7184    }
7185
7186    #[test]
7187    fn forward_batch_continues_correctly_after_prior_forward_token_calls() {
7188        // Realistic usage pattern: some tokens processed one at a time
7189        // (e.g. the first generated token), then a batch verifying
7190        // several draft tokens at once, continuing from the same
7191        // cache. The batch's positions must be numbered starting from
7192        // wherever the cache left off, not from zero.
7193        let cfg = tiny_test_config();
7194
7195        let decoder_a = Decoder::new_random_small(cfg.clone(), 2, 8);
7196        let mut caches_a: Vec<KvCache> = decoder_a.config.new_kv_caches();
7197        decoder_a.forward_token(1, 0, &mut caches_a);
7198        decoder_a.forward_token(3, 1, &mut caches_a);
7199        let seq_next = decoder_a.forward_token(5, 2, &mut caches_a);
7200
7201        let decoder_b = Decoder::new_random_small(cfg, 2, 8);
7202        let mut caches_b: Vec<KvCache> = decoder_b.config.new_kv_caches();
7203        decoder_b.forward_token(1, 0, &mut caches_b);
7204        let batch_next = decoder_b.forward_batch(&[3, 5], 1, &mut caches_b);
7205
7206        for (s, b) in seq_next.iter().zip(batch_next[1].iter()) {
7207            assert!(
7208                (s - b).abs() < BATCH_VS_SEQUENTIAL_ULP,
7209                "sequential={s} batched={b}"
7210            );
7211        }
7212    }
7213
7214    /// `PlacementPlan::from_budget` is
7215    /// real and tested in isolation, but only meaningful once it's fed
7216    /// genuinely observed per-expert activation counts rather than
7217    /// zeros. This proves the full loop: run real forward passes,
7218    /// confirm `MoeWeights::activation_counts` actually reflects what
7219    /// `route_top_k` selected, and confirm `placement_plan` prioritizes
7220    /// the expert that was genuinely hottest -- not just that the
7221    /// budget/size arithmetic works on synthetic inputs.
7222    #[test]
7223    fn placement_plan_reflects_real_observed_expert_activations() {
7224        let cfg = tiny_test_config(); // 6 experts, top-2 active/token
7225        let decoder = Decoder::new_random_small(cfg, 2, 16);
7226        let mut caches: Vec<KvCache> = decoder.config.new_kv_caches();
7227
7228        let n_calls = 20;
7229        for pos in 0..n_calls {
7230            decoder.forward_token(pos % 16, pos, &mut caches);
7231        }
7232
7233        let layer0 = &decoder.layers[0].moe;
7234        let counts: Vec<u64> = layer0
7235            .activation_counts
7236            .iter()
7237            .map(|c| c.load(std::sync::atomic::Ordering::Relaxed))
7238            .collect();
7239        let total: u64 = counts.iter().sum();
7240        assert_eq!(
7241            total,
7242            (n_calls as u64) * (decoder.config.moe.n_experts_active as u64),
7243            "total recorded activations must equal calls * experts_active_per_call"
7244        );
7245
7246        // Ties are realistic at this small a sample size; break them the
7247        // same way `PlacementPlan::from_budget` does (lowest index
7248        // wins), so this assertion can't spuriously fail on a tie that
7249        // `from_budget` resolves differently than a naive `max_by_key`
7250        // (which returns the *last* max element) would.
7251        let hottest_count = *counts.iter().max().unwrap();
7252        let hottest_idx = counts.iter().position(|&c| c == hottest_count).unwrap();
7253        assert!(hottest_count > 0);
7254
7255        // A per-expert resident size big enough for exactly one expert.
7256        let per_expert_bytes = layer0.expert_bytes(0);
7257        let plan = layer0.placement_plan(per_expert_bytes as u64);
7258
7259        assert_eq!(
7260            plan.placement_for(hottest_idx),
7261            frink_moe::ExpertPlacement::GpuDevice(0),
7262            "the genuinely hottest expert (index {hottest_idx}, {hottest_count} activations) \
7263             must be the one the plan places on GPU when only one expert fits the budget"
7264        );
7265    }
7266}
7267
7268/// The Metal side of Phi-3/Phi-4's RoPE: partial rotary and LongRoPE's
7269/// `attn_factor` used to be a refusal in `layer_supports_metal_attn`
7270/// and are now two uniforms on [`frink_metal::attn::MetalRope`].
7271#[cfg(all(test, feature = "metal"))]
7272mod metal_rope_tests {
7273    use super::*;
7274
7275    fn phi_like_config() -> ModelConfig {
7276        let mut cfg = crate::config::test_dense_fixture();
7277        cfg.head_dim = 128;
7278        cfg.rope_layout = crate::config::RopeLayout::Neox;
7279        cfg.rope_dim = Some(96);
7280        cfg.rope_attn_factor = 1.1902381;
7281        cfg
7282    }
7283
7284    /// Both values must reach the kernels, and they must be the same two
7285    /// the CPU path reads — otherwise the backends compute different
7286    /// attention for the same weights, which is the whole reason the
7287    /// model was refused Metal in the first place.
7288    #[test]
7289    fn metal_rope_carries_partial_rotary_and_mscale() {
7290        let decoder = Decoder::new_random_small(phi_like_config(), 1, 32);
7291        let rope = decoder.metal_rope();
7292        assert_eq!(rope.layout, frink_metal::attn::MetalRopeLayout::Neox);
7293        assert_eq!(rope.rot_dim, Some(96));
7294        assert_eq!(rope.attn_factor, 1.1902381);
7295    }
7296
7297    /// `rope.dimension_count == head_dim` is "the whole head rotates",
7298    /// which must reach the kernel as `None` rather than as a width —
7299    /// same graph, one code path.
7300    #[test]
7301    fn rot_dim_equal_to_head_dim_becomes_none() {
7302        let mut cfg = phi_like_config();
7303        cfg.rope_dim = Some(cfg.head_dim);
7304        let decoder = Decoder::new_random_small(cfg, 1, 32);
7305        assert_eq!(decoder.metal_rope().rot_dim, None);
7306    }
7307
7308    /// A non-unit `attn_factor` is no longer a reason to refuse Metal;
7309    /// an odd `n_rot` still is, because ggml's `ggml_rope_impl` asserts
7310    /// an even width and the split-half pairing is otherwise undefined
7311    /// for the last channel.
7312    #[test]
7313    fn odd_rot_dim_is_still_refused_but_mscale_is_not() {
7314        let supported = |cfg: ModelConfig| {
7315            let d = Decoder::new_random_small(cfg, 1, 32);
7316            d.layer_supports_metal_attn(&d.layers[0])
7317        };
7318
7319        // The control: with no rope oddity the fixture is admitted, so
7320        // the two assertions below are about the rope config and not
7321        // about the fixture failing some other check.
7322        let mut plain = phi_like_config();
7323        plain.rope_dim = None;
7324        plain.rope_attn_factor = 1.0;
7325        assert!(supported(plain), "fixture must be Metal-eligible to start");
7326
7327        assert!(
7328            supported(phi_like_config()),
7329            "partial rotary + a non-unit attn_factor must no longer refuse Metal"
7330        );
7331
7332        let mut odd = phi_like_config();
7333        odd.rope_dim = Some(95);
7334        assert!(!supported(odd), "odd n_rot must keep the model off Metal");
7335    }
7336
7337    /// A `residual_scale` keeps every fused Metal path off the model,
7338    /// through ONE predicate the four eligibility checks share.
7339    ///
7340    /// Granite multiplies both branch outputs before every residual add.
7341    /// The fused launches -- dense decode, resident MoE decode, and both
7342    /// prefill stacks -- fold the residual add in on device with no
7343    /// uniform for a multiplier, so a Granite layer served by any of
7344    /// them would be scaled by the host bodies and not by the GPU: the
7345    /// same weights answering differently depending on which backend
7346    /// took the token. That is exactly what `attention_scale` is fenced
7347    /// off for next door, and this repo has already watched the GPU
7348    /// router's eligibility check drift four ways when it was four
7349    /// spellings instead of one.
7350    ///
7351    /// Only reachable in a `--features metal` build, which is where the
7352    /// fence exists at all.
7353    #[test]
7354    fn a_residual_scale_keeps_the_model_off_every_fused_metal_path() {
7355        let mut plain = phi_like_config();
7356        plain.rope_dim = None;
7357        plain.rope_attn_factor = 1.0;
7358        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7359        assert!(
7360            d.layer_supports_metal_attn(&d.layers[0]),
7361            "the fixture must be Metal-eligible to start, or this proves nothing"
7362        );
7363
7364        let mut scaled = plain;
7365        scaled.residual_scale = Some(0.22);
7366        let d = Decoder::new_random_small(scaled.clone(), 1, 32);
7367        assert!(
7368            !d.layer_supports_metal_attn(&d.layers[0]),
7369            "a residual multiplier no Metal kernel applies must refuse the fused attention"
7370        );
7371        assert!(
7372            !Decoder::metal_prefill_dense_layer_eligible(&d.layers[0], &scaled, false),
7373            "...and the prefill dense stack"
7374        );
7375        assert!(
7376            !Decoder::metal_can_serve_model(&scaled, false),
7377            "the shared predicate is what all four read"
7378        );
7379    }
7380
7381    /// A `clamp_kqv` keeps every fused Metal path off the model too,
7382    /// through the same predicate.
7383    ///
7384    /// The fused launches apply the QKV bias inside their kernels via
7385    /// `AttnExtras` and clamp nothing, while the host bodies clamp
7386    /// after the bias (`decoder/qkv_bias.rs`). A DBRX layer served by a
7387    /// fused launch would therefore run unclamped projections -- the
7388    /// disagreement the residual-scale fence exists for, on a different
7389    /// scalar. Only reachable in a `--features metal` build.
7390    #[test]
7391    fn a_qkv_clamp_keeps_the_model_off_every_fused_metal_path() {
7392        let mut plain = phi_like_config();
7393        plain.rope_dim = None;
7394        plain.rope_attn_factor = 1.0;
7395        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7396        assert!(
7397            d.layer_supports_metal_attn(&d.layers[0]),
7398            "the fixture must be Metal-eligible to start, or this proves nothing"
7399        );
7400
7401        let mut clamped = plain;
7402        clamped.clamp_kqv = Some(8.0);
7403        let d = Decoder::new_random_small(clamped.clone(), 1, 32);
7404        assert!(
7405            !d.layer_supports_metal_attn(&d.layers[0]),
7406            "a clamp no Metal kernel applies must refuse the fused attention"
7407        );
7408        assert!(
7409            !Decoder::metal_prefill_dense_layer_eligible(&d.layers[0], &clamped, false),
7410            "...and the prefill dense stack"
7411        );
7412        assert!(!Decoder::metal_can_serve_model(&clamped, false));
7413    }
7414
7415    /// Per-layer shapes keep every fused Metal path off the model,
7416    /// through the same predicate.
7417    ///
7418    /// The fused launches take ONE `n_heads` argument and one
7419    /// `MetalKvBuffers` geometry per run of layers, and the Metal KV
7420    /// plane is sized once from the scalars. A deci or openelm layer
7421    /// served by any of them would be projected and cached at another
7422    /// layer's width -- the disagreement the two fences above exist
7423    /// for, on a shape rather than a scalar. Only reachable in a
7424    /// `--features metal` build.
7425    #[test]
7426    fn per_layer_shapes_keep_the_model_off_every_fused_metal_path() {
7427        use crate::layer_shapes::{AttnShape, LayerShape, LayerShapes};
7428        let mut plain = phi_like_config();
7429        plain.rope_dim = None;
7430        plain.rope_attn_factor = 1.0;
7431        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7432        assert!(
7433            d.layer_supports_metal_attn(&d.layers[0]),
7434            "the fixture must be Metal-eligible to start, or this proves nothing"
7435        );
7436
7437        let mut shaped = plain;
7438        shaped.layer_shapes = LayerShapes::PerLayer(vec![LayerShape {
7439            attention: AttnShape::Gqa {
7440                n_heads: shaped.n_heads,
7441                n_kv_heads: shaped.n_kv_heads,
7442            },
7443            ffn_dim: shaped.moe.expert_ffn_dim,
7444        }]);
7445        let d = Decoder::new_random_small(shaped.clone(), 1, 32);
7446        assert!(
7447            !d.layer_supports_metal_attn(&d.layers[0]),
7448            "a per-layer table, even one whose single entry agrees with the scalars, must \
7449             refuse the fused attention: the stacks hold one geometry"
7450        );
7451        assert!(
7452            !Decoder::metal_prefill_dense_layer_eligible(&d.layers[0], &shaped, false),
7453            "...and the prefill dense stack"
7454        );
7455        assert!(!Decoder::metal_can_serve_model(&shaped, false));
7456    }
7457
7458    /// A per-position attention temperature keeps every fused Metal
7459    /// path off the model, through the same predicate.
7460    ///
7461    /// No fused launch takes a per-token Q scale: the host bodies
7462    /// multiply Q by `log(floor(pos / floor) + 1) * scale + 1` after
7463    /// RoPE (`crate::attn_temperature`), and a Ministral-3 layer served
7464    /// by a fused launch would attend at temperature 1 while its
7465    /// neighbours on the host stepped with position -- the same
7466    /// disagreement the three fences above exist for. Only reachable
7467    /// in a `--features metal` build.
7468    #[test]
7469    fn a_per_position_temperature_keeps_the_model_off_every_fused_metal_path() {
7470        use crate::attn_temperature::AttnTemperature;
7471        let mut plain = phi_like_config();
7472        plain.rope_dim = None;
7473        plain.rope_attn_factor = 1.0;
7474        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7475        assert!(
7476            d.layer_supports_metal_attn(&d.layers[0]),
7477            "the fixture must be Metal-eligible to start, or this proves nothing"
7478        );
7479
7480        let mut tempered = plain;
7481        tempered.attn_temperature = Some(AttnTemperature {
7482            scale: 0.5,
7483            floor_scale: std::num::NonZeroU32::new(2).unwrap(),
7484            offset: 0.0,
7485            unrotated_layers_only: false,
7486        });
7487        let d = Decoder::new_random_small(tempered.clone(), 1, 32);
7488        assert!(
7489            !d.layer_supports_metal_attn(&d.layers[0]),
7490            "a per-position Q scale no Metal kernel applies must refuse the fused attention"
7491        );
7492        assert!(
7493            !Decoder::metal_prefill_dense_layer_eligible(&d.layers[0], &tempered, false),
7494            "...and the prefill dense stack"
7495        );
7496        assert!(!Decoder::metal_can_serve_model(&tempered, false));
7497    }
7498
7499    /// BitNet's two inner norms keep the model off every fused Metal
7500    /// path, through the same predicate, and a layer that carries the
7501    /// attention one off the per-layer fused attention through the
7502    /// exhaustive destructure in `metal_attn_view`.
7503    ///
7504    /// No fused kernel norms between the V sum and `wo` or between the
7505    /// activation and `down`; a BitNet layer served by one would skip
7506    /// both norms at full speed (`crate::sub_norms`). Only reachable in
7507    /// a `--features metal` build.
7508    /// A parallel-residual model (`crate::parallel_residual`) stays off
7509    /// every fused Metal path: each of them bakes the pre-FFN norm over
7510    /// the POST-ATTENTION residual into its kernel, and a parallel layer
7511    /// norms the layer INPUT. The model-level flag is what the
7512    /// predicate reads, because the per-layer fact lives on
7513    /// `MoeWeights` and the two config-only callers cannot see it. Only
7514    /// reachable in a `--features metal` build.
7515    /// A learned-position model (`crate::position_embd`) stays off every
7516    /// fused Metal path: the GPU embedding gather has no add, and no
7517    /// stack sees `pos` for it. Only reachable in a `--features metal`
7518    /// build.
7519    /// An ALiBi model (`crate::alibi`) stays off every fused Metal path:
7520    /// no kernel adds a per-key bias to its scores. Only reachable in a
7521    /// `--features metal` build.
7522    #[test]
7523    fn an_alibi_bias_keeps_the_model_off_every_fused_metal_path() {
7524        let mut plain = phi_like_config();
7525        plain.rope_dim = None;
7526        plain.rope_attn_factor = 1.0;
7527        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7528        assert!(
7529            d.layer_supports_metal_attn(&d.layers[0]),
7530            "the fixture must be Metal-eligible to start, or this proves nothing"
7531        );
7532        let mut biased = plain;
7533        biased.alibi_max_bias = Some(8.0);
7534        let d = Decoder::new_random_small(biased.clone(), 1, 32);
7535        assert!(d.alibi_slopes.is_some(), "derived from the config");
7536        assert!(!d.layer_supports_metal_attn(&d.layers[0]));
7537        assert!(!Decoder::metal_can_serve_model(&biased, false));
7538    }
7539
7540    #[test]
7541    fn a_learned_position_table_keeps_the_model_off_every_fused_metal_path() {
7542        let mut plain = phi_like_config();
7543        plain.rope_dim = None;
7544        plain.rope_attn_factor = 1.0;
7545        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7546        assert!(
7547            d.layer_supports_metal_attn(&d.layers[0]),
7548            "the fixture must be Metal-eligible to start, or this proves nothing"
7549        );
7550        let mut positioned = plain;
7551        positioned.learned_positions = true;
7552        let d = Decoder::new_random_small(positioned.clone(), 1, 32);
7553        assert!(!d.layer_supports_metal_attn(&d.layers[0]));
7554        assert!(!Decoder::metal_can_serve_model(&positioned, false));
7555    }
7556
7557    #[test]
7558    fn a_parallel_residual_keeps_the_model_off_every_fused_metal_path() {
7559        let mut plain = phi_like_config();
7560        plain.rope_dim = None;
7561        plain.rope_attn_factor = 1.0;
7562        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7563        assert!(
7564            d.layer_supports_metal_attn(&d.layers[0]),
7565            "the fixture must be Metal-eligible to start, or this proves nothing"
7566        );
7567        let mut parallel = plain;
7568        parallel.parallel_residual = true;
7569        let d = Decoder::new_random_small(parallel.clone(), 1, 32);
7570        assert!(!d.layer_supports_metal_attn(&d.layers[0]));
7571        assert!(!Decoder::metal_prefill_dense_layer_eligible(
7572            &d.layers[0],
7573            &parallel,
7574            false
7575        ));
7576        assert!(!Decoder::metal_can_serve_model(&parallel, false));
7577    }
7578
7579    /// Llama 4's two facts (`crate::chunked_swa`,
7580    /// `crate::weightless_qk_norm`) each keep the model off every fused
7581    /// launch: one window per layer, no weightless post-RoPE QK norm.
7582    #[test]
7583    fn a_chunked_window_and_the_weightless_qk_norm_each_keep_the_model_on_the_host() {
7584        let mut plain = phi_like_config();
7585        plain.rope_dim = None;
7586        plain.rope_attn_factor = 1.0;
7587        assert!(Decoder::metal_can_serve_model(&plain, false));
7588        let mut chunked = plain.clone();
7589        chunked.sliding_window = Some(8192);
7590        chunked.swa_chunked = true;
7591        assert!(!Decoder::metal_can_serve_model(&chunked, false));
7592        let mut normed = plain;
7593        normed.weightless_qk_norm = true;
7594        assert!(!Decoder::metal_can_serve_model(&normed, false));
7595    }
7596
7597    #[test]
7598    fn the_inner_norms_keep_the_model_off_every_fused_metal_path() {
7599        let mut plain = phi_like_config();
7600        plain.rope_dim = None;
7601        plain.rope_attn_factor = 1.0;
7602        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7603        assert!(
7604            d.layer_supports_metal_attn(&d.layers[0]),
7605            "the fixture must be Metal-eligible to start, or this proves nothing"
7606        );
7607
7608        let mut sub_normed = plain;
7609        sub_normed.block_sub_norms = true;
7610        let d = Decoder::new_random_small(sub_normed.clone(), 1, 32);
7611        assert!(
7612            !d.layer_supports_metal_attn(&d.layers[0]),
7613            "a norm between attention and `wo` that no Metal kernel applies must refuse the \
7614             fused attention"
7615        );
7616        assert!(
7617            !Decoder::metal_prefill_dense_layer_eligible(&d.layers[0], &sub_normed, false),
7618            "...and the prefill dense stack"
7619        );
7620        assert!(!Decoder::metal_can_serve_model(&sub_normed, false));
7621
7622        // Independently of the config: the tensor on the layer is enough
7623        // to lose the per-layer fused attention's view of it.
7624        let mut d = Decoder::new_random_small(plain_config_with_metal_view(), 1, 32);
7625        assert!(d.metal_attn_view(&d.layers[0]).is_some());
7626        d.layers[0].attn.attn_sub_norm = Some(vec![1.0; d.config.hidden_dim]);
7627        assert!(d.metal_attn_view(&d.layers[0]).is_none());
7628    }
7629
7630    /// A V head width that differs from K's, and a scale after `wo`,
7631    /// each keep the model off every fused Metal path through the same
7632    /// predicate: every fused launch takes ONE head width for its KV
7633    /// buffers, its attention tile and its `wo` fold, and none scales
7634    /// after the fold (`crate::kv_head_dims`, `crate::attn_value_scale`).
7635    /// Only reachable in a `--features metal` build.
7636    #[test]
7637    fn a_split_kv_head_width_or_a_value_scale_keeps_the_model_off_every_fused_metal_path() {
7638        let plain = plain_config_with_metal_view();
7639        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7640        assert!(d.layer_supports_metal_attn(&d.layers[0]), "the premise");
7641
7642        let mut split = plain.clone();
7643        split.v_head_dim = Some(plain.head_dim / 2);
7644        assert!(!Decoder::metal_can_serve_model(&split, false));
7645        assert!(!Decoder::metal_prefill_dense_layer_eligible(
7646            &d.layers[0],
7647            &split,
7648            false
7649        ));
7650
7651        let mut scaled = plain;
7652        scaled.attn_value_scale = Some(0.707);
7653        assert!(!Decoder::metal_can_serve_model(&scaled, false));
7654        assert!(!Decoder::metal_prefill_dense_layer_eligible(
7655            &d.layers[0],
7656            &scaled,
7657            false
7658        ));
7659    }
7660
7661    /// A looped model keeps off every fused Metal path: each launch
7662    /// indexes weights and KV buffers with one `l`, and a looped model's
7663    /// logical layers outnumber its weights (`crate::layer_loops`). Only
7664    /// reachable in a `--features metal` build.
7665    #[test]
7666    fn a_layer_loop_keeps_the_model_off_every_fused_metal_path() {
7667        let plain = plain_config_with_metal_view();
7668        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7669        assert!(d.layer_supports_metal_attn(&d.layers[0]), "the premise");
7670        let mut looped = plain;
7671        looped.layer_loops = Some(crate::layer_loops::LayerLoops::Repeat {
7672            n_phys: 1,
7673            n_loops: 2,
7674            skip_loop_final_norm: false,
7675        });
7676        assert!(!Decoder::metal_can_serve_model(&looped, false));
7677        assert!(!Decoder::metal_prefill_dense_layer_eligible(
7678            &d.layers[0],
7679            &looped,
7680            false
7681        ));
7682    }
7683
7684    /// Talkie's facts keep the model off every fused Metal path: the
7685    /// skip stream and the per-head scalar QK gain through the model
7686    /// predicate, and a projection gain on a layer through the
7687    /// exhaustive destructure and the dense-FFN predicate
7688    /// (`crate::skip_stream`, `crate::weight_scales`). Only reachable in
7689    /// a `--features metal` build.
7690    #[test]
7691    fn talkie_s_facts_keep_the_model_off_every_fused_metal_path() {
7692        let plain = plain_config_with_metal_view();
7693        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7694        assert!(d.layer_supports_metal_attn(&d.layers[0]), "the premise");
7695
7696        let mut skip = plain.clone();
7697        skip.skip_stream = true;
7698        assert!(!Decoder::metal_can_serve_model(&skip, false));
7699
7700        let mut scalar = plain.clone();
7701        scalar.qk_norm_style = crate::capability::QkNormStyle::PerHeadScalar;
7702        assert!(!Decoder::metal_can_serve_model(&scalar, false));
7703
7704        let mut d = Decoder::new_random_small(plain.clone(), 1, 32);
7705        assert!(d.metal_attn_view(&d.layers[0]).is_some());
7706        d.layers[0].attn.o_scale = Some(1.5);
7707        assert!(d.metal_attn_view(&d.layers[0]).is_none());
7708
7709        let mut d = Decoder::new_random_small(plain.clone(), 1, 32);
7710        assert!(Decoder::metal_prefill_dense_layer_eligible(
7711            &d.layers[0],
7712            &plain,
7713            false
7714        ));
7715        d.layers[0].moe.down_scale = Some(0.5);
7716        assert!(!Decoder::metal_prefill_dense_layer_eligible(
7717            &d.layers[0],
7718            &plain,
7719            false
7720        ));
7721        assert!(!Decoder::layer_supports_metal_dense_ffn(&d.layers[0]));
7722    }
7723
7724    fn plain_config_with_metal_view() -> ModelConfig {
7725        let mut plain = phi_like_config();
7726        plain.rope_dim = None;
7727        plain.rope_attn_factor = 1.0;
7728        plain
7729    }
7730
7731    /// An FFN activation no fused kernel spells keeps the model off
7732    /// every fused Metal path, through the same predicate.
7733    ///
7734    /// Six launch sites used to derive the kernels' `gelu: bool` as
7735    /// `!is_swiglu()`, which would have run the ungated ReLU-squared FFN
7736    /// (`arcee`) as GELU; `GluAct::fused_kernel_gelu_flag` is `None`
7737    /// there now, and this is the fence that keeps a layer from being
7738    /// half-served. Only reachable in a `--features metal` build.
7739    #[test]
7740    fn an_activation_no_kernel_spells_keeps_the_model_off_every_fused_metal_path() {
7741        let mut plain = phi_like_config();
7742        plain.rope_dim = None;
7743        plain.rope_attn_factor = 1.0;
7744        plain.ffn_activation = crate::config::FfnActivation::Swiglu;
7745        let d = Decoder::new_random_small(plain.clone(), 1, 32);
7746        assert!(d.layer_supports_metal_attn(&d.layers[0]));
7747
7748        let mut ungated = plain;
7749        ungated.ffn_activation = crate::config::FfnActivation::ReluSqr;
7750        let d = Decoder::new_random_small(ungated.clone(), 1, 32);
7751        assert!(!d.layer_supports_metal_attn(&d.layers[0]));
7752        assert!(!Decoder::metal_prefill_dense_layer_eligible(
7753            &d.layers[0],
7754            &ungated,
7755            false
7756        ));
7757        assert!(!Decoder::metal_can_serve_model(&ungated, false));
7758        assert_eq!(
7759            ungated
7760                .model_ffn_act()
7761                .and_then(GluAct::fused_kernel_gelu_flag),
7762            None
7763        );
7764
7765        // The two PARAMETERISED activations answer no whole-model
7766        // activation at all, and the two-width rotary is a fourth thing
7767        // the fused stacks cannot take; each keeps the model off alone.
7768        let mut xielu = ungated.clone();
7769        xielu.ffn_activation =
7770            crate::config::FfnActivation::Xielu(crate::act_layers::XieluLayers::new(vec![
7771                frink_moe::XieluParams::from_gguf(
7772                    0.8, 0.8, 0.5, -1e-6
7773                );
7774                xielu.n_layers
7775            ]));
7776        assert_eq!(xielu.model_ffn_act(), None);
7777        assert!(!Decoder::metal_can_serve_model(&xielu, false));
7778        let mut clamped = ungated.clone();
7779        clamped.ffn_activation =
7780            crate::config::FfnActivation::SwigluClamped(crate::act_layers::SwigluClamps::new(
7781                vec![0.0; clamped.n_layers],
7782                vec![7.0; clamped.n_layers],
7783                frink_moe::ClampForm::AfterSilu,
7784            ));
7785        assert_eq!(clamped.model_ffn_act(), None);
7786        assert!(!Decoder::metal_can_serve_model(&clamped, false));
7787        let mut two_widths = ungated;
7788        two_widths.ffn_activation = crate::config::FfnActivation::Swiglu;
7789        assert!(Decoder::metal_can_serve_model(&two_widths, false));
7790        two_widths.sliding_window = Some(4);
7791        two_widths.swa_layers = crate::swa_layers::SwaLayers::period(2, false);
7792        two_widths.n_layers = 2;
7793        two_widths.rope_dim_swa = Some(two_widths.head_dim / 2);
7794        assert!(two_widths.rope_dim_varies_by_layer());
7795        assert!(!Decoder::metal_can_serve_model(&two_widths, false));
7796    }
7797
7798    /// An attention output gate or a set of attention sinks keeps THAT
7799    /// LAYER off every fused Metal attention launch, through the one
7800    /// exhaustive destructure in `metal_attn_view`.
7801    ///
7802    /// Both sit between the softmax and `wo`, which the fused kernels
7803    /// run with no host round-trip; a launch that ignored either would
7804    /// answer differently from the host bodies for the same weights.
7805    /// The gate is `afmoe` / `laguna` (`crate::attn_gate`); the sinks
7806    /// used to be refused by the gpt-oss NAME, and this pins that they
7807    /// are refused by the TENSOR now, on a model that is not gpt-oss.
7808    /// Only reachable in a `--features metal` build.
7809    #[test]
7810    fn an_output_gate_or_attention_sinks_keep_the_layer_off_every_fused_metal_path() {
7811        let mut plain = phi_like_config();
7812        plain.rope_dim = None;
7813        plain.rope_attn_factor = 1.0;
7814        let mut d = Decoder::new_random_small(plain.clone(), 2, 32);
7815        assert!(
7816            d.layer_supports_metal_attn(&d.layers[0]),
7817            "the fixture must be Metal-eligible to start, or this proves nothing"
7818        );
7819        assert!(d.metal_attn_view(&d.layers[1]).is_some());
7820
7821        // A gate on layer 0 only.
7822        let (n_heads, hidden) = (plain.n_heads, plain.hidden_dim);
7823        d.layers[0].attn.output_gate = Some(crate::attn_gate::AttnGate {
7824            proj: WeightMatrix::F32(Tensor::new(
7825                vec![0.1; n_heads * hidden],
7826                vec![n_heads, hidden],
7827            )),
7828            act: crate::attn_gate::GateAct::Sigmoid,
7829            width: crate::attn_gate::GateWidth::PerHead,
7830        });
7831        assert!(
7832            !d.layer_supports_metal_attn(&d.layers[0]),
7833            "a gate no Metal kernel applies must refuse the fused attention"
7834        );
7835        assert!(d.metal_attn_view(&d.layers[0]).is_none());
7836        assert!(
7837            d.layer_supports_metal_attn(&d.layers[1]),
7838            "the fence is per layer: the ungated layer is still served"
7839        );
7840
7841        // Sinks on layer 1, with `gpt_oss` still `None`.
7842        d.layers[1].attn.sinks = Some(vec![0.5; n_heads]);
7843        assert!(d.gpt_oss.is_none());
7844        assert!(
7845            !d.layer_supports_metal_attn(&d.layers[1]),
7846            "sinks are refused by the tensor, not by the gpt-oss name"
7847        );
7848        assert!(d.metal_attn_view(&d.layers[1]).is_none());
7849    }
7850
7851    /// A LoRA adapter keeps the WHOLE model off every fused Metal
7852    /// launch, through the shared predicate, and an adapted matrix has
7853    /// no raw-bytes Metal descriptor at all -- so a stack that somehow
7854    /// asked for one anyway could not build it. The delta lives inside
7855    /// `WeightMatrix::Adapted` and is served by that type's methods;
7856    /// the stacks read weight bytes past those methods.
7857    /// Only reachable in a `--features metal` build.
7858    #[test]
7859    fn a_lora_adapter_keeps_the_model_off_every_fused_metal_path() {
7860        let mut plain = phi_like_config();
7861        plain.rope_dim = None;
7862        plain.rope_attn_factor = 1.0;
7863        let mut d = Decoder::new_random_small(plain.clone(), 2, 32);
7864        assert!(Decoder::metal_can_serve_model(&plain, false));
7865        assert!(
7866            d.layer_supports_metal_attn(&d.layers[0]),
7867            "the fixture must be Metal-eligible to start, or this proves nothing"
7868        );
7869        assert!(Decoder::metal_matvec_launch(&d.layers[0].attn.q_proj).is_some());
7870
7871        // The fact the predicate reads.
7872        assert!(!Decoder::metal_can_serve_model(&plain, true));
7873        assert!(!Decoder::metal_prefill_dense_layer_eligible(
7874            &d.layers[0],
7875            &plain,
7876            true
7877        ));
7878
7879        // The variant, on one matrix: no descriptor, so no launch.
7880        let (rows, cols) = (
7881            d.layers[0].attn.q_proj.rows(),
7882            d.layers[0].attn.q_proj.cols(),
7883        );
7884        let scale = frink_core::weight_matrix::LoraScale::new(1.0);
7885        d.layers[0].attn.q_proj.attach_lora(
7886            frink_core::weight_matrix::LoraDelta::new(
7887                vec![0.01; 2 * cols],
7888                vec![0.01; rows * 2],
7889                2,
7890                rows,
7891                cols,
7892                0.0,
7893                scale.clone(),
7894            )
7895            .unwrap(),
7896        );
7897        assert!(Decoder::metal_matvec_launch(&d.layers[0].attn.q_proj).is_none());
7898        assert!(d.layers[0].attn.q_proj.mul_mm_sg_launch().is_none());
7899        assert!(
7900            !d.layer_supports_metal_attn(&d.layers[0]),
7901            "a layer whose projection has no Metal descriptor is not served"
7902        );
7903
7904        // The list, on the decoder: what every eligibility check asks.
7905        d.lora_adapters.push(crate::lora_attach::LoraAttached {
7906            path: "x.gguf".into(),
7907            alpha: 0.0,
7908            task_name: String::new(),
7909            prompt_prefix: String::new(),
7910            scale,
7911            n_tensors: 1,
7912        });
7913        assert!(d.lora_attached());
7914        assert!(
7915            !d.layer_supports_metal_attn(&d.layers[1]),
7916            "the fence is the whole model, not the adapted layer alone"
7917        );
7918    }
7919
7920    /// A Gemma-3-4B-shaped config: `rope_scaling {linear, factor 8}`
7921    /// folded into the full-attention layers' divisors, nothing on the
7922    /// sliding ones, `sliding_window_pattern = 6` last-dense.
7923    fn gemma3_4b_shaped_config() -> ModelConfig {
7924        let mut cfg = crate::config::test_dense_fixture();
7925        cfg.head_dim = 8;
7926        cfg.rope_layout = crate::config::RopeLayout::Norm;
7927        cfg.rope_theta = 1_000_000.0;
7928        cfg.rope_theta_swa = Some(10_000.0);
7929        cfg.sliding_window = Some(4);
7930        cfg.swa_layers = crate::swa_layers::SwaLayers::period(6, false);
7931        cfg.rope_freqs = Some(crate::config::RopeFreqs {
7932            full: vec![8.0; 4],
7933            swa: Some(vec![1.0; 4]),
7934        });
7935        // One full period, so the run holds five sliding layers and one
7936        // full-attention layer -- Gemma-3's ratio, and the smallest one
7937        // that makes `rope_freqs_vary_by_layer` true.
7938        cfg.n_layers = 6;
7939        cfg
7940    }
7941
7942    /// What the fused Metal stacks are handed per layer must be BOTH
7943    /// halves of `ModelConfig::layer_rope`, layer by layer.
7944    ///
7945    /// `Decoder::metal_stack_needs_per_layer_rope_freqs` used to refuse
7946    /// exactly this config off the fused prefill/decode stacks, because
7947    /// those took one `freq_factors` slice for a whole run beside a
7948    /// per-layer theta -- half the answer varying and half not, which is
7949    /// this repo's dominant bug shape. `LayerRope` carries the pair, and
7950    /// this pins that the decoder fills it from the pair rather than
7951    /// re-deriving either half on its own.
7952    #[test]
7953    fn the_metal_stacks_are_handed_each_layer_s_own_rope_pair() {
7954        let cfg = gemma3_4b_shaped_config();
7955        assert!(
7956            cfg.rope_freqs_vary_by_layer(),
7957            "fixture must be the shape that used to be refused"
7958        );
7959        let decoder = Decoder::new_random_small(cfg, 6, 32);
7960
7961        for il in 0..decoder.layers.len() {
7962            let rope = decoder
7963                .config
7964                .layer_rope(il)
7965                .expect("every layer rotates here");
7966            let sent = decoder
7967                .metal_layer_rope(il)
7968                .expect("so every layer is handed a rope");
7969            assert_eq!(sent.theta, rope.theta, "layer {il} base");
7970            assert_eq!(sent.freq_factors, rope.freq_factors, "layer {il} divisors");
7971        }
7972
7973        // Not vacuous: with `swa_pattern = 6` last-dense, layers 0..=4
7974        // slide and layer 5 does not, so the run really does hold two
7975        // different answers.
7976        let sliding = decoder.metal_layer_rope(0).unwrap();
7977        let full = decoder.metal_layer_rope(5).unwrap();
7978        assert_eq!(sliding.freq_factors, Some(&[1.0f32; 4][..]));
7979        assert_eq!(full.freq_factors, Some(&[8.0f32; 4][..]));
7980        assert_ne!(
7981            sliding, full,
7982            "a run of layers that all rope alike proves nothing here"
7983        );
7984    }
7985
7986    /// The THIRD half of the answer: a layer llama.cpp does not rotate
7987    /// reaches the Metal stacks as `None`, from the same accessor the
7988    /// CPU bodies read, and `layer_needs_metal_stack` sees it through
7989    /// the same `Option` -- so the per-layer launches, which always
7990    /// rope, can never be handed such a layer.
7991    #[test]
7992    fn an_unrotated_layer_is_handed_no_rope_and_routed_to_the_stack() {
7993        let mut cfg = gemma3_4b_shaped_config();
7994        // Gemma-3's own graph rotates everything; borrow its shape and
7995        // give it EXAONE-4 32B's rule so the full-attention layer 5 is
7996        // the one that does not rotate.
7997        cfg.rope_layers = crate::rope_layers::RopeLayers::SlidingOnly;
7998        let decoder = Decoder::new_random_small(cfg, 6, 32);
7999
8000        for il in 0..5 {
8001            assert!(
8002                decoder.metal_layer_rope(il).is_some(),
8003                "sliding layer {il} rotates"
8004            );
8005        }
8006        assert_eq!(
8007            decoder.metal_layer_rope(5),
8008            None,
8009            "the full-attention layer does not"
8010        );
8011        assert!(
8012            decoder.layer_needs_metal_stack(&decoder.layers[5], 5),
8013            "an unrotated layer must go to the fused stack, which implements `None`"
8014        );
8015    }
8016}