Skip to main content

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