Skip to main content

cortiq_engine/
pipeline.rs

1//! Full inference pipeline: tokenize → embed → layers → lm_head → sample → decode.
2//!
3//! Prefill/decode contract: every token is forwarded exactly once and
4//! enters the KV cache exactly once. Logits for the next token are
5//! computed from the hidden state of the LAST forwarded token — the
6//! decode loop forwards the freshly sampled token, never re-embeds the
7//! prompt tail (v1 duplicated the last prompt token in the cache).
8
9use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11use crate::kv_cache::KvCache;
12use crate::linear_core::{
13    GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
14    gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
15    vmf_phase_pair,
16};
17use crate::pool::Pool;
18use crate::qtensor::QTensor;
19use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
20use crate::tokenizer::Tokenizer;
21use cortiq_core::mask::TaskMask;
22use cortiq_core::types::NormStyle;
23
24pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
25    std::sync::atomic::AtomicBool::new(false);
26
27/// Reusable per-pipeline forward scratch: the four norm outputs the
28/// decode paths recompute every layer (single: n1/p1; pair: all four).
29/// Plain buffers, resized once — steady-state decode reuses them.
30struct ForwardScratch {
31    n1: Vec<f32>,
32    n2: Vec<f32>,
33    p1: Vec<f32>,
34    p2: Vec<f32>,
35}
36
37impl ForwardScratch {
38    fn new(hidden: usize) -> Self {
39        Self {
40            n1: vec![0.0; hidden],
41            n2: vec![0.0; hidden],
42            p1: vec![0.0; hidden],
43            p2: vec![0.0; hidden],
44        }
45    }
46}
47
48/// Complete inference pipeline state.
49pub struct Pipeline {
50    /// In-process layer split across local GPUs: (device, first layer,
51    /// last layer) per segment, in execution order. `None` = one device.
52    /// Arc so cloning the plan out of `&mut self` does not fight the
53    /// borrow checker on the hot path.
54    gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
55    /// Arc: the server shares one tokenizer handle across request
56    /// handlers without borrowing a pipeline slot.
57    pub tokenizer: std::sync::Arc<Tokenizer>,
58    pub kv_cache: KvCache,
59    pub sampler_config: SamplerConfig,
60    pub weights: PipelineWeights,
61    pub hidden_size: usize,
62    pub intermediate_size: usize,
63    pub num_heads: usize,
64    pub num_kv_heads: usize,
65    pub head_dim: usize,
66    /// Total virtual layers (num_layers × num_loops for looped models).
67    pub num_layers: usize,
68    /// Physical layers in weights.layers (≤ num_layers for looped models).
69    pub physical_layers: usize,
70    /// Looped Transformer: apply final norm after each loop iteration.
71    pub loop_final_norm: bool,
72    pub vocab_size: usize,
73    pub rms_eps: f64,
74    pub rope_base: f32,
75    pub norm_style: NormStyle,
76    /// RoPE dims actually rotated (≤ head_dim; Qwen3.5 uses head_dim/4).
77    pub rotary_dim: usize,
78    /// Optional Q-head count override for each attention layer (Laguna).
79    pub attention_heads_per_layer: Option<Vec<usize>>,
80    /// Linear-core geometry (present when the model has linear layers).
81    pub vmf_cfg: Option<VmfPhaseCfg>,
82    /// GatedDeltaNet geometry (faithful vendor operator).
83    pub gdn_cfg: Option<GdnCfg>,
84    /// MiniCPM-class logit scale (tied lm_head → cannot fold into weights).
85    pub logit_multiplier: Option<f32>,
86    /// Cooperative cancel: set from any thread (FFI `cortiq_cancel`,
87    /// a dropped server connection); the generate loop checks it at
88    /// every prefill chunk and decode step and finishes with
89    /// `finish_reason: "cancelled"`. Auto-cleared when honoured.
90    pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
91    /// Token ids currently materialized in the KV cache (the forwarded
92    /// prompt + all generated tokens except the last, which is sampled
93    /// but not yet forwarded). Lets the next generate call prefill only
94    /// the suffix when a chat app resends the whole history.
95    pub kv_history: Vec<u32>,
96    /// KDA geometry (Kimi Linear / Kimi-K3) — shared by every Kda layer.
97    pub kda_cfg: Option<crate::linear_core::KdaCfg>,
98    /// Gemma-3n stack (AltUp/LAuReL/PLE/KV-sharing): its own forward —
99    /// weights.layers stays empty, the KV caches are the shared ones.
100    pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
101    /// DeepSeek-V4 runs its own stack too: its hidden state is `hc_mult`
102    /// copies of a vector, so no loop written for a single residual
103    /// stream can carry it.
104    pub dsv4: Option<
105        Box<(
106            crate::dsv4::Dsv4Globals,
107            Vec<crate::dsv4::Dsv4Layer>,
108            crate::dsv4::Dsv4Cfg,
109            crate::dsv4::Dsv4State,
110        )>,
111    >,
112    /// DeepSeek-V4's own speculation stack: three draft modules, each a full
113    /// layer, plus a confidence head on the last. Empty when the file has
114    /// none, which is the only signal the decode path needs.
115    pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
116    /// The draft's per-sequence state (KV rings, captured trunk hidden).
117    pub dspark: Option<crate::dsv4::DsparkState>,
118    /// Drafts awaiting their verdict: (position, proposals, still matching,
119    /// accepted so far).
120    pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
121    /// Accepted prefix length of every graded draft.
122    pub dspark_hist: Vec<usize>,
123    /// The real tokens the drafts were graded against — a degenerate,
124    /// repeating output would make any acceptance number meaningless, and
125    /// the cheapest guard against believing one is to count them.
126    pub dspark_real: Vec<u32>,
127    /// The trunk's expert picks for the last few tokens, per layer. The
128    /// union over a window of them is what a batched verify would have to
129    /// read, and the ratio to the pick count is all it could save.
130    pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
131    /// (unique, total) expert picks per draft, trunk side and draft side.
132    pub dspark_exp: Vec<(usize, usize, usize, usize)>,
133    /// Wall time spent in the deliberately out-of-core draft. Kept separate
134    /// from trunk decode so block batching can be judged without conflating
135    /// it with GPU chain variance.
136    pub dspark_draft_ns: u128,
137    /// LFM2 short-convolution geometry (present when the model has
138    /// `ShortConv` mixer layers).
139    pub short_conv_cfg: Option<ShortConvCfg>,
140    /// Multi-token-prediction head (None = absent).
141    pub mtp: Option<MtpModule>,
142    /// Speculative decode via MTP (greedy only; `CMF_MTP=0` disables).
143    pub speculative: bool,
144    rng: SplitMix64,
145    sampler_scratch: SamplerScratch,
146    /// Speculative SAMPLING state (graph_spec_step, temperature > 0): the
147    /// correction token a rejected draft produced — committed by the loop
148    /// top in place of a fresh draw — and the per-round draft
149    /// distributions / target scratch, reused so a round allocates
150    /// nothing at the vocab size.
151    spec_forced: Option<u32>,
152    spec_q: Vec<Vec<f32>>,
153    spec_p: Vec<f32>,
154    spec_res: Vec<f32>,
155    /// The same three for the sparse chain (top-k configs).
156    spec_qs: Vec<sampler::Sparse>,
157    spec_ps: sampler::Sparse,
158    spec_ress: sampler::Sparse,
159    /// Which arm the MTP draft block runs on this generation: Some(true)
160    /// = the whole-token graph (device attention, one submit a step),
161    /// Some(false) = the per-op path; None = not decided yet. Decided
162    /// on the first draft and held, because the two arms keep the MTP
163    /// KV in different places (device mirror vs the CPU cache) and a
164    /// mid-run switch would read the wrong one.
165    mtp_graph_mode: Option<bool>,
166    /// The Metal verify graph of the round in flight, between its sync
167    /// (logits read) and the commit that replays the accepted prefix.
168    #[cfg(target_os = "macos")]
169    metal_verify: Option<MetalVerifyPending>,
170    /// Precomputed RoPE inverse frequencies [head_dim/2]. Arc: the
171    /// forward path clones a handle to escape the &mut self borrow —
172    /// cloning the table itself was a per-forward allocation.
173    pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
174    /// Reusable norm buffers for the decode hot path (roadmap §3 P0:
175    /// steady-state forward should not heap-allocate). Disjoint field
176    /// from `weights`/`kv_cache`, so split borrows keep working.
177    ws: ForwardScratch,
178    /// Persistent worker pool (None = serial; see CMF_THREADS).
179    pool: Option<std::sync::Arc<Pool>>,
180    // ── Dynamic per-token skill routing (spec §9, claim 14/16) ──
181    /// Source model, retained so a skill switch can re-resolve the
182    /// touched layers' FFN tensors (Mapped = mmap pointers, cheap).
183    pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
184    /// Masks present → weights are dequantized f32 (rebuild path).
185    pub(crate) dyn_force_f32: bool,
186    /// Per-skill FFN layers actually replaced (derived from tensors, not
187    /// the meta `layers` field — ru2 replaces down_proj in 0..23 while
188    /// its meta says [20..23]). None = skill touches non-FFN tensors →
189    /// ineligible for cheap dynamic switching (honest refusal).
190    pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
191    /// Currently overlaid skill (index into model.header.skills); None =
192    /// backbone. Set at load time to the statically-overlaid skill so
193    /// `set_active_skill(None)` correctly reverts it (else a static
194    /// skill would silently persist — the union-diff assumes dyn_active
195    /// always mirrors the live overlay). Switched by `set_active_skill`.
196    pub(crate) dyn_active: Option<usize>,
197    /// Pipeline was loaded with a soft blend (materialized working
198    /// tensors, not a single skill index) → dynamic routing refuses:
199    /// there is no single index to revert the blend from.
200    pub(crate) dyn_blend_loaded: bool,
201    /// Layer whose post-residual hidden feeds the router φ (shared by
202    /// swarm skills). None = φ capture off.
203    pub(crate) dyn_phi_layer: Option<usize>,
204    /// EMA of φ at `dyn_phi_layer` over the decode window (on-policy).
205    dyn_phi_ema: Vec<f32>,
206    dyn_phi_seen: usize,
207    /// Hysteresis router driving per-token skill switches during decode
208    /// (None = static/no dynamic routing). Taken out during generation.
209    pub dyn_router: Option<crate::swarm::DynRouter>,
210    /// O(1) Nyström attention setting (CLI/env/header-hint resolved by
211    /// the caller; None = plain cache attention everywhere).
212    o1_cfg: Option<crate::nystrom::O1Cfg>,
213    /// Bumped at every o1 seal — the GPU state mirror re-uploads when it
214    /// sees a new epoch (each generate seals fresh CPU state).
215    o1_epoch: u64,
216    /// Per-layer o1 flags derived from `o1_cfg` (Full layers only).
217    o1_flags: Vec<bool>,
218    /// Emit a structured per-token trace (B4 telemetry channel). Off by
219    /// default — the runtime is silent unless observation is requested.
220    trace: bool,
221    /// Confidence-calibration temperature (B1): reported Born mass is
222    /// softmax(logits / calib_temp). 1.0 = raw. Set from header.calibration.
223    calib_temp: f32,
224    /// Process-unique id keying this pipeline's device KV mirrors.
225    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
226    graph_kv_id: u64,
227    /// Decode asks the token graph to also run final-norm + lm_head on
228    /// the device (drops the separate per-op lm_head round trip).
229    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
230    graph_want_logits: bool,
231    /// Logits the graph produced for the token just forwarded (taken by
232    /// the decode loop; None = compute on the CPU path).
233    graph_logits: Option<Vec<f32>>,
234    /// Token embeddings are multiplied by this at input (Gemma: √hidden).
235    pub embed_multiplier: f32,
236    /// Attention score scale (1/√head_dim unless the arch overrides —
237    /// Gemma's query_pre_attn_scalar).
238    pub attn_scale: f32,
239    /// Sliding-window attention: (window, every-Nth-layer-is-global
240    /// pattern) — Gemma-3.
241    pub swa: Option<(usize, usize)>,
242    /// Explicit local/global schedule for architectures that cannot be
243    /// represented by Gemma's every-Nth-global convention.
244    pub sliding_layers: Option<Vec<bool>>,
245    /// RoPE table of the sliding (local) layers, when they use their
246    /// own base frequency (Gemma-3: 10k local vs 1M global).
247    pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
248    pub rotary_dim_local: Option<usize>,
249    pub rope_scale: f32,
250    pub rope_scale_local: f32,
251    /// Gemma-4: global layers run their own geometry — (head_dim,
252    /// num_kv_heads); sliding layers keep the base fields.
253    pub global_attn: Option<(usize, usize)>,
254    /// Gemma-4: the global layers' proportional RoPE table (len
255    /// global_head_dim/2, zero-padded tail = identity rotation).
256    pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
257    /// Scale-less RMS normalization of V heads before caching (Gemma-4).
258    pub attn_v_norm: bool,
259    /// Final-logit soft-capping C: logits = C·tanh(logits/C) (Gemma-4).
260    pub final_softcap: Option<f32>,
261    /// Cortiq Embryo hierarchical head: cluster matrix [C, hidden]. The
262    /// flat logits h·Eᵀ are turned into the two-level log-probabilities
263    /// log softmax_c(h·Cᵀ)[c(v)] + log softmax_{s∈c(v)}(h·E_c(v)ᵀ)[v].
264    pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
265    /// Gemma-2 attention-logit soft-capping (0.0 = off).
266    pub attn_softcap: f32,
267    /// Compute per-token Born confidence (a full-vocab softmax each
268    /// token). On by default; `bench --core` turns it off to match
269    /// llama-bench's core timing.
270    confidence_on: bool,
271}
272
273#[cfg(target_os = "macos")]
274impl Drop for Pipeline {
275    fn drop(&mut self) {
276        crate::gpu::kv_mirror_drop(self.graph_kv_id);
277    }
278}
279
280/// Model weights. Matrices are `QTensor` (owned f32 for small models
281/// and tests — bit-identical to the historical paths — or quantized
282/// bytes zero-copy from the CMF mmap for big models). 1-D norms are
283/// always small and stay f32.
284pub struct PipelineWeights {
285    /// Embedding table: [vocab_size, hidden_size]
286    pub embed_tokens: QTensor,
287    /// Per-layer weights
288    pub layers: Vec<LayerWeights>,
289    /// LM head: [vocab_size, hidden_size]
290    pub lm_head: QTensor,
291    /// Final norm: [hidden_size]
292    pub final_norm: Vec<f32>,
293}
294
295/// One transformer layer: shared norms + MLP, attention by kind.
296pub struct LayerWeights {
297    pub input_norm: Vec<f32>,
298    /// The pre-FFN norm (`post_attention_layernorm` classically;
299    /// `pre_feedforward_layernorm` on Gemma-2/3 sandwich layers).
300    pub post_norm: Vec<f32>,
301    /// Gemma-2/3 sandwich: norm applied to the ATTENTION OUTPUT before
302    /// its residual add (`post_attention_layernorm` there).
303    pub attn_out_norm: Option<Vec<f32>>,
304    /// Gemma-4: the whole layer output is multiplied by this scalar.
305    pub layer_scale: Option<f32>,
306    /// Gemma-2/3 sandwich: norm applied to the FFN OUTPUT before its
307    /// residual add (`post_feedforward_layernorm`).
308    pub ffn_out_norm: Option<Vec<f32>>,
309    pub ffn: FfnKind,
310    pub attn: AttnKind,
311}
312
313/// FFN gate activation: SiLU (SwiGLU family) or tanh-GELU (Gemma's
314/// GeGLU). A property of the model, carried on every FFN triple.
315#[derive(Clone, Copy, PartialEq, Debug, Default)]
316pub enum Act {
317    #[default]
318    Silu,
319    GeluTanh,
320    /// Kimi-K3 SituAndMul: BOTH halves transform —
321    /// a = β·tanh(g/β)·σ(g), up' = linβ·tanh(u/linβ) (linβ>0), out = a·up'.
322    Situ {
323        beta: f32,
324        linear_beta: f32,
325    },
326}
327
328impl Act {
329    pub fn from_arch(name: &str) -> Self {
330        if name == "gelu_tanh" {
331            Self::GeluTanh
332        } else {
333            Self::Silu
334        }
335    }
336
337    /// Arch-driven constructor (activation name + situ betas).
338    pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
339        match arch.hidden_act.as_str() {
340            "situ" => Self::Situ {
341                beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
342                linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
343            },
344            other => Self::from_arch(other),
345        }
346    }
347
348    #[inline]
349    pub fn apply(self, x: f32) -> f32 {
350        match self {
351            Self::Silu => inference::silu(x),
352            Self::GeluTanh => inference::gelu_tanh(x),
353            Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
354        }
355    }
356
357    /// Gated combine — the FFN contract. Situ transforms the UP half
358    /// too, so callers must use this instead of apply(g)·u.
359    #[inline]
360    pub fn combine(self, g: f32, u: f32) -> f32 {
361        match self {
362            Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
363                self.apply(g) * (linear_beta * (u / linear_beta).tanh())
364            }
365            _ => self.apply(g) * u,
366        }
367    }
368}
369
370/// Dense gated triple — the FFN of a dense layer or of one expert.
371pub struct DenseFfn {
372    pub gate_proj: QTensor,
373    pub up_proj: QTensor,
374    pub down_proj: QTensor,
375    /// Gate activation (SiLU default; Gemma: tanh-GELU).
376    pub act: Act,
377    /// `down_proj` stored transposed (`[inter, hidden]`), when the file
378    /// carries it. Only the per-token sparse path reads it: a neuron's
379    /// down weights are a contiguous ROW there, so the token's chosen
380    /// neurons are the only bytes touched. `None` = the ordinary layout,
381    /// and the sparse path stays off.
382    pub down_t: Option<QTensor>,
383    /// Task tubes (spec: defragged task-conditional width). The three
384    /// matrices above are the CORE — the neurons every task computes;
385    /// each tube is an independently quantized slice of the SAME layer
386    /// holding the neurons only some tasks need. A tube is a normal
387    /// tensor triple, so every kernel runs it unchanged, and the bytes
388    /// of an inactive tube are never read. Empty = ordinary dense FFN.
389    pub segs: Vec<FfnSeg>,
390}
391
392/// One task tube: a contiguous slice of a layer's FFN neurons, stored
393/// as its own `[w, hidden]` / `[hidden, w]` triple. `start` is the
394/// neuron's index in the layer's FULL space (core first, then tubes in
395/// order) — the bit a task mask sets to switch this tube on.
396pub struct FfnSeg {
397    pub gate: QTensor,
398    pub up: QTensor,
399    pub down: QTensor,
400    pub start: usize,
401    pub width: usize,
402}
403
404/// FFN operator of a layer, decided by tensor presence at load time
405/// (router `mlp.gate.weight` in the directory = MoE layer).
406pub enum FfnKind {
407    Dense(DenseFfn),
408    /// Mixture-of-Experts (Qwen2-MoE / Qwen3-MoE): softmax over ALL
409    /// expert logits → top-k, optional renorm; experts stay quantized
410    /// in mmap — only the selected ones are touched per token.
411    Moe(MoeFfn),
412    /// Gemma-4 MoE: a dense MLP branch AND a routed-expert branch in
413    /// the SAME layer, each with its own norm sandwich. The dense
414    /// branch reads the pre-FFN-normed input; the expert branch (and
415    /// the router) read the RAW residual through `pre_norm_2`:
416    ///   d = post_norm_1(dense(x̂));  m = post_norm_2(Σwₑ·FFNₑ(pre_norm_2(h)))
417    ///   ffn_out = d + m   (the caller's ffn_out_norm + residual follow)
418    DenseMoe(Box<DenseMoeFfn>),
419}
420
421/// Gemma-4 dual-branch FFN (see `FfnKind::DenseMoe`).
422pub struct DenseMoeFfn {
423    pub dense: DenseFfn,
424    pub moe: MoeFfn,
425    /// post_feedforward_layernorm_1 — dense-branch output norm.
426    pub post_norm_1: Vec<f32>,
427    /// pre_feedforward_layernorm_2 — expert-branch input norm (applied
428    /// to the RAW residual, not the pre-FFN-normed activation).
429    pub pre_norm_2: Vec<f32>,
430    /// post_feedforward_layernorm_2 — expert-branch output norm.
431    pub post_norm_2: Vec<f32>,
432}
433
434pub struct MoeFfn {
435    /// Router `mlp.gate.weight` [num_experts, hidden].
436    pub router: QTensor,
437    pub experts: Vec<DenseFfn>,
438    pub top_k: usize,
439    pub norm_topk_prob: bool,
440    /// Router scores per-expert with a sigmoid (LFM2-MoE / DeepSeek-V3
441    /// `noaux_tc`) instead of a softmax over all experts (Qwen).
442    pub router_sigmoid: bool,
443    /// Per-expert selection bias `mlp.expert_bias` [num_experts]
444    /// (LFM2-MoE): added to the sigmoid scores for the top-k CHOICE only;
445    /// the gathered weights use the unbiased scores. None = no bias.
446    pub expert_bias: Option<Vec<f32>>,
447    /// Top-k weights are multiplied by this after the optional renorm
448    /// (LFM2-MoE `routed_scaling_factor`; 1.0 = off).
449    pub routed_scaling: f32,
450    /// Adaptive routing (CMF_MOE_TAU, opt-in): keep the smallest
451    /// prefix of the top-k whose renormalized mass reaches τ —
452    /// confident tokens touch 1–2 experts, flat ones keep all k.
453    /// MoE decode is memory-bound, so skipped experts are skipped
454    /// weight traffic. None = classic fixed top-k (bit-identical).
455    pub route_tau: Option<f32>,
456    /// Always-on shared expert. Qwen2-MoE carries an additional sigmoid
457    /// gate; Laguna adds the shared expert unconditionally (`None`).
458    pub shared: Option<(DenseFfn, Option<QTensor>)>,
459    /// Expert-selection counters (truncated Fisher B-field of claim 12:
460    /// routing frequency during calibration). Filled by every forward,
461    /// read by the CLI via CMF_MOE_STATS. RefCell: decode is single-threaded.
462    pub stats: std::cell::RefCell<Vec<u64>>,
463    /// Per-CHANNEL sum of squares of this FFN's input, accumulated over a
464    /// calibration run (`CMF_RMS_TRACE`). These are the RMS activation
465    /// traces AWNP needs: raw weight magnitude says every channel matters
466    /// equally, and the question AWNP asks is whether the ACTIVATIONS
467    /// disagree. Off unless the env var is set — an f64 add per channel
468    /// per token is cheap, but not free.
469    pub act_sq: std::cell::RefCell<Vec<f64>>,
470    /// Raw FFN-input rows captured for the layers named by `CMF_ACT_DUMP`
471    /// (`"9,19"`). AWNP is nullspace PROJECTION: after dropping channels the
472    /// survivors are refitted to absorb what was removed, and how much they
473    /// can absorb depends on the activation COVARIANCE, not on per-channel
474    /// RMS. Per-channel numbers can only bound the cost from above.
475    pub act_rows: std::cell::RefCell<Vec<f32>>,
476    /// Task mask over routed experts (DTG-MA over MoE, claim-12 B-field
477    /// applied): `false` experts are excluded from selection, the
478    /// softmax renormalizes over the allowed set. Built by the loader
479    /// from CMF_MOE_MASK=<stats.json> + CMF_MOE_MASK_COVER. None = all.
480    pub mask: Option<Vec<bool>>,
481    /// Gemma-4: per-expert weight scale applied AFTER the top-k renorm
482    /// (`router.per_expert_scale`). None = 1.0 everywhere.
483    pub per_expert_scale: Option<Vec<f32>>,
484    /// Gemma-4: the router reads a SCALE-LESS rms-norm of its input
485    /// (the constant gain router.scale·√hidden is folded into the
486    /// router weights at convert time).
487    pub router_input_norm: bool,
488    /// Cortiq Embryo: resonance routing (P1) — the "logits" are
489    /// bias_e − ‖(x−μ_e) − U_eᵀU_e(x−μ_e)‖², argmax = the expert whose
490    /// descriptor reconstructs the input best. `router` is a placeholder.
491    pub resonance: Option<Resonance>,
492}
493
494/// Per-expert resonance descriptors of one MoE layer (`mlp.desc.*`).
495pub struct Resonance {
496    /// [E, hidden]
497    pub mu: Vec<f32>,
498    /// [E, k, hidden] orthonormal directions (k may be 0)
499    pub u: Vec<f32>,
500    pub k: usize,
501    /// [E] selection bias (loss-free balancing, trained online)
502    pub bias: Vec<f32>,
503}
504
505impl Resonance {
506    /// Routing scores for one input row (higher = better).
507    pub fn scores(&self, x: &[f32], out: &mut [f32]) {
508        let h = x.len();
509        let ne = out.len();
510        for e in 0..ne {
511            let mu = &self.mu[e * h..(e + 1) * h];
512            let mut d2 = 0.0f32;
513            for j in 0..h {
514                let d = x[j] - mu[j];
515                d2 += d * d;
516            }
517            let mut proj = 0.0f32;
518            for i in 0..self.k {
519                let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
520                let mut p = 0.0f32;
521                for j in 0..h {
522                    p += (x[j] - mu[j]) * u[j];
523                }
524                proj += p * p;
525            }
526            out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
527        }
528    }
529}
530
531/// Attention operator of a layer. Extension point: new operators are
532/// new variants here + a forward in their own module.
533pub enum AttnKind {
534    /// GQA softmax attention (+ optional Qwen3.5 qk-norm / output gate).
535    Full {
536        wq: QTensor,
537        wk: QTensor,
538        wv: QTensor,
539        wo: QTensor,
540        q_norm: Option<Vec<f32>>,
541        k_norm: Option<Vec<f32>>,
542        output_gate: bool,
543        /// Laguna: a separate softplus projection applied to the attention
544        /// output before O. The bool means one scalar per head (broadcast
545        /// across head_dim); false means one scalar per element.
546        softplus_gate: Option<(QTensor, bool)>,
547        /// Qwen2-family projection biases (q, k, v).
548        bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
549    },
550    /// Canonical linear core (VMF phase attention).
551    Linear(VmfPhaseWeights),
552    /// Faithful vendor linear operator (Qwen3.5 GatedDeltaNet).
553    LinearGdn(GdnWeights),
554    /// LFM2 gated short-convolution mixer (no KV cache; conv ring state
555    /// lives in the layer's `linear_state`).
556    ShortConv(ShortConvWeights),
557    /// DeepSeek-V2 Multi-head Latent Attention. v1 executes it as
558    /// expand-to-MHA: the latent is projected per token, K/V expand to
559    /// every head and live in the ordinary cache (K head layout
560    /// [rope | nope] so the standard partial rotary covers the shared
561    /// rope key; V rows are zero-padded to the K head_dim and the pad
562    /// is sliced off before O). Latent-resident cache is a later
563    /// optimization, not a semantic change.
564    Mla(Box<MlaWeights>),
565    /// Kimi Delta Attention (Kimi Linear / Kimi-K3): per-channel decayed
566    /// delta rule, separate q/k/v short convs, sigmoid-gated output norm.
567    /// State lives in the layer's `linear_state` (no KV cache).
568    Kda(Box<crate::linear_core::KdaWeights>),
569}
570
571/// DeepSeek-V2 MLA projections (see `AttnKind::Mla`).
572pub struct MlaWeights {
573    /// `[nh·(rope+nope), hidden]` (or `[…, q_lora]` when compressed) —
574    /// the converter permutes each head rope-first so rotary_dim =
575    /// qk_rope works unchanged.
576    pub q_proj: QTensor,
577    /// Compressed q (K3/V3 class): x → q_a `[q_lora, hidden]` →
578    /// rms(q_a_norm) → q_proj (= q_b). None = direct q (V2-Lite).
579    pub q_a: Option<QTensor>,
580    pub q_a_norm: Option<Vec<f32>>,
581    /// `kv_a_proj_with_mqa` `[lora + rope, hidden]` (latent first).
582    pub kv_a: QTensor,
583    /// RMS-norm weights over the latent (`kv_a_layernorm`, [lora]).
584    pub kv_a_norm: Vec<f32>,
585    /// `[nh·(nope+v), lora]` — per head [k_nope | v].
586    pub kv_b: QTensor,
587    /// `[hidden, nh·v]`.
588    pub o_proj: QTensor,
589    pub nh: usize,
590    pub qk_rope: usize,
591    pub qk_nope: usize,
592    pub v_dim: usize,
593    pub lora: usize,
594    /// Softmax scale (1/√(rope+nope), YaRN-mscale-corrected at load).
595    pub scale: f32,
596    /// Kimi Linear NoPE: skip the rotary entirely (layout unchanged).
597    pub nope: bool,
598}
599
600/// Multi-token-prediction head (DeepSeek/Qwen style, spec §2.1):
601/// `x = eh_proj·[enorm(embed(next)); hnorm(hidden)]` → one transformer
602/// block over its own KV → shared lm_head. Drafts the token after next;
603/// the main model verifies, so output is exact — MTP only buys speed.
604pub struct MtpModule {
605    pub enorm: Vec<f32>,
606    pub hnorm: Vec<f32>,
607    /// [hidden, 2·hidden]
608    pub eh_proj: QTensor,
609    pub layer: LayerWeights,
610    pub final_norm: Vec<f32>,
611    pub kv: crate::kv_cache::LayerKvCache,
612}
613
614/// A Metal verify graph after its sync: what the commit needs — the
615/// graph (per-layer replay scratch), the GDN layers in encode order (their
616/// CPU states receive the replay), and the attention layers with the CPU
617/// row count they were encoded against (the accepted rows are pulled from
618/// the mirror from there).
619/// One item of the Metal rows-graph plan.
620#[cfg(target_os = "macos")]
621enum MetalRowsItem<'a> {
622    Gdn {
623        run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
624        first: usize,
625    },
626    Attn {
627        l: crate::gpu_metal::AttnGpuLayer<'a>,
628        li: usize,
629        q_norm: Option<&'a [f32]>,
630        k_norm: Option<&'a [f32]>,
631        output_gate: bool,
632    },
633}
634
635#[cfg(target_os = "macos")]
636struct MetalVerifyPending {
637    graph: crate::gpu_metal::VerifyGraph,
638    gdn_layers: Vec<usize>,
639    attn_layers: Vec<(usize, usize)>,
640}
641
642/// The speculation trial's phases (see the decode loop): four timed
643/// speculative rounds, eight timed plain tokens, then the faster arm
644/// until a re-check.
645#[derive(Clone, Copy)]
646enum SpecTrial {
647    Spec {
648        t0: std::time::Instant,
649        gen0: usize,
650        rounds: usize,
651    },
652    Plain {
653        t0: std::time::Instant,
654        gen0: usize,
655    },
656    Decided {
657        spec: bool,
658        recheck_at: usize,
659    },
660}
661
662/// The speculation monitor: exponential averages of a round's wall time
663/// and of the tokens it produced, and the plain token's wall time — the
664/// three numbers the keep/stop rule needs. A round pays when
665/// `tokens_per_round · plain_ms > round_ms · 1.03`. The one-shot trial
666/// (four rounds against eight tokens) mis-called prose: the first rounds
667/// after a prompt are formulaic and accept well, the body does not (an
668/// essay measured 39 against a plain 44.8 with the trial saying
669/// "speculate"), so the rule now runs on EVERY round and stops after four
670/// consecutive losing rounds; a stopped speculation is retried 128 tokens
671/// later.
672#[derive(Default, Clone, Copy)]
673struct SpecMon {
674    round_ms: f64,
675    tokens: f64,
676    plain_ms: f64,
677    n: u32,
678    fails: u32,
679}
680
681impl SpecMon {
682    fn round(&mut self, dt_ms: f64, produced: usize) {
683        self.n += 1;
684        if self.n == 1 {
685            return; // round 1 pays the batch scratch and the draft mirror
686        }
687        let a = if self.n == 2 { 1.0 } else { 0.3 };
688        self.round_ms += a * (dt_ms - self.round_ms);
689        self.tokens += a * (produced as f64 - self.tokens);
690    }
691    fn pays(&self) -> bool {
692        self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
693    }
694}
695
696/// Result of a generation call.
697pub struct GenerateResult {
698    pub text: String,
699    pub token_ids: Vec<u32>,
700    pub prompt_tokens: usize,
701    pub tokens_generated: usize,
702    pub finish_reason: String,
703    /// Speculative-decode stats (0/0 when MTP is absent or inactive).
704    pub mtp_drafted: usize,
705    pub mtp_accepted: usize,
706    /// Per-generated-token confidence = softmax probability of the token
707    /// that was actually emitted (Born mass on the chosen state). High =
708    /// the model was sure; low = it was guessing. Same length as the
709    /// generated slice of `token_ids`.
710    pub token_confidence: Vec<f32>,
711    /// Structured per-token telemetry (B4 channel). Empty unless
712    /// `set_trace(true)`; otherwise same length as the generated slice.
713    pub traces: Vec<TokenTrace>,
714}
715
716/// One row of the structured telemetry trace (B4): the model's internal
717/// routing state at the moment a token was emitted. Every field is a
718/// quantity the runtime already computes — nothing is inferred or
719/// estimated (anti-principle: only measured bytes).
720#[derive(Clone, Debug)]
721pub struct TokenTrace {
722    /// 0-based index within the generated slice.
723    pub t: usize,
724    /// The emitted token id.
725    pub token_id: u32,
726    /// Born mass on the emitted token (softmax prob) — how sure the model was.
727    pub confidence: f32,
728    /// Skill in force while this token was generated (None = backbone).
729    pub active_skill: Option<String>,
730    /// Recon error E = ‖r−BBᵀr‖²/‖φ‖² at the last routing eval — coherence
731    /// with the active skill's subspace (low = coherent). None = no router
732    /// or not yet evaluated.
733    pub recon: Option<f32>,
734    /// The router changed the active skill right after this token (a
735    /// domain boundary crossed under the hysteresis barrier).
736    pub switched: bool,
737}
738
739/// Calibrated softmax probability of `id` under `logits` (the Born mass on
740/// the emitted token) — the confidence signal, cheap from logits already
741/// computed for sampling. `temp` is the calibration temperature (B1):
742/// softmax(logits / temp); 1.0 = raw.
743#[cfg_attr(not(test), allow(dead_code))]
744fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
745    let t = if temp > 1e-3 { temp } else { 1.0 };
746    let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
747    let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
748    if sum > 0.0 {
749        (((logits[id as usize] - max) / t).exp()) / sum
750    } else {
751        0.0
752    }
753}
754
755/// prefill-GEMM enabled? (CMF_PREFILL=seq — emergency fallback to the
756/// sequential path.)
757fn prefill_batched() -> bool {
758    std::env::var("CMF_PREFILL")
759        .map(|v| v != "seq")
760        .unwrap_or(true)
761}
762
763/// Input to the layer-major batched span walk: token ids (embeds itself,
764/// full-stack and coordinator prefill) or ready boundary hiddens (the
765/// network worker's side of a split).
766#[derive(Clone, Copy)]
767enum PrefillIn<'a> {
768    Ids(&'a [u32]),
769    Hidden(&'a [f32]),
770}
771
772/// The batched prefill walks `weights.layers`. Architectures that load
773/// their own stack (gemma-3n's AltUp replicas, DeepSeek-V4's hyper-
774/// connections) leave that empty and must go position by position — asking
775/// otherwise indexes an empty vector, which is a panic rather than a
776/// fallback. Every call site goes through here so the next such
777/// architecture is one line, not four.
778impl Pipeline {
779    fn can_prefill_batched(&self) -> bool {
780        prefill_batched() && !self.weights.layers.is_empty()
781    }
782}
783
784/// Prefill chunk (positions per batched pass). On macOS the AMX GEMM
785/// path wants tall panels — M=48 starves the matrix units (ggml uses
786/// ubatch 512); elsewhere the historical 48 stays. CMF_PREFILL_CHUNK
787/// overrides. Pub: the network split MUST chunk identically to the
788/// local path — panel width reorders float accumulation, so a different
789/// chunk is a different (equally valid) generation.
790pub fn prefill_chunk() -> usize {
791    if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
792        .ok()
793        .and_then(|v| v.parse::<usize>().ok())
794    {
795        return n.max(1);
796    }
797    if cfg!(target_os = "macos") {
798        512
799    } else if cfg!(target_arch = "aarch64") {
800        // Mobile: big enough to feed the batched attend (gate b ≥ 32)
801        // and the blocked SDOT GEMM without the memory of 512.
802        256
803    } else {
804        48
805    }
806}
807
808/// Callback for streaming tokens. Return `false` to cancel.
809pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
810
811impl Pipeline {
812    /// Map a virtual layer index to its physical weight index.
813    /// Looped Transformer (Nanbeige 4.2): 22 physical layers × 2 loops = 44 virtual;
814    /// virtual layer 23 maps back to physical layer 1 (23 % 22 = 1).
815    #[inline]
816    pub fn phys_layer(&self, virtual_idx: usize) -> usize {
817        virtual_idx % self.physical_layers
818    }
819
820    /// True when `virtual_idx` is the last layer of a loop iteration
821    /// (used for loop_final_norm insertion).
822    #[inline]
823    pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
824        self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
825    }
826
827    /// Build a pipeline from parts (used by the loader and tests).
828    #[allow(clippy::too_many_arguments)]
829
830    /// Whole-block q1 token graph on the GPU (macOS/Metal): the run of
831    /// consecutive q1 layers — GDN *and* full attention — starting at
832    /// `start` executes as few command buffers as the CPU truly needs.
833    /// Hidden stays device-resident across every layer; the only syncs
834    /// are before each CPU attend (it needs q/k/v and owns the KV
835    /// cache) and the final hidden readback. Recurrent states
836    /// round-trip through shared memory (the CPU stays their owner, so
837    /// every other path remains coherent). Returns the first layer
838    /// index NOT covered (== `start` → refused, caller falls through
839    /// to the per-layer CPU path).
840    /// Should prefill run position-by-position through the GPU token
841    /// graph instead of the batched CPU chunk-GEMM? True for q1 GDN
842    /// hybrids on native Metal: their chunk prefill is walled by the
843    /// sequential scalar recurrence, so the graph's decode rate wins.
844    /// NOT for Looped Transformers, despite the per-chunk loop_final_norm
845    /// sync: the chunk-GEMM amortizes each weight over the whole chunk,
846    /// which the per-position graph cannot (Nanbeige 4.2 on M4, 512-token
847    /// prompt: 85 tok/s chunked vs 14 through the graph).
848    #[cfg(target_os = "macos")]
849    fn graph_prefill_preferred(&self) -> bool {
850        if !crate::gpu::enabled_here()
851            || !crate::gpu::q1_force()
852            || std::env::var("CMF_GPU_BLOCK")
853                .map(|v| v == "0")
854                .unwrap_or(false)
855            // CMF_PREFILL_GRAPH=0: the chunked prefill (GEMM projections,
856            // CPU recurrence) instead of the per-position token graph.
857            || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
858        {
859            return false;
860        }
861        self.weights
862            .layers
863            .iter()
864            .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
865    }
866
867    #[cfg(not(target_os = "macos"))]
868    fn graph_prefill_preferred(&self) -> bool {
869        // Discrete-GPU wgpu whole-token graph: GDN layers carry recurrent state
870        // (conv ring + delta-rule S) resident on the GPU. A batched CPU prefill
871        // builds that state on the CPU only, leaving the GPU buffers zeroed at
872        // decode → garbage. Route GDN-hybrid prefill through the graph one
873        // position at a time so the resident state is seeded exactly as decode
874        // will read it. Pure-attention models keep the batched CPU prefill (its
875        // KV mirror re-syncs from the CPU cache, so no seeding gap).
876        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
877        if !graph_on || !crate::gpu::enabled_here() {
878            return false;
879        }
880        // O(1) needs the CPU prefill: the q-trace that seals the Nyström
881        // skeleton is recorded there and nowhere else. The GDN half of
882        // the hybrid loses nothing — the graph's first decode creates
883        // its (ring, S) entries seeded from `cpu_state`, the same
884        // handoff every graph run relies on when the entry is fresh.
885        // Without this line the two designs collide on hybrids and o1
886        // never becomes graph-portable: prefill through the graph
887        // records no trace, so views stay None forever.
888        if self.o1_active() {
889            return false;
890        }
891        self.weights
892            .layers
893            .iter()
894            .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
895    }
896
897    #[cfg(target_os = "macos")]
898    fn q1_graph_gpu(
899        &mut self,
900        start: usize,
901        upto: Option<usize>,
902        position: usize,
903        h: &mut [f32],
904    ) -> usize {
905        let _mt0 = std::time::Instant::now(); // CMF_METAL_HOSTPROF
906        use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
907        if self.attn_softcap > 0.0 // capped scores: no graph kernel — CPU path
908            || !crate::gpu::enabled_here()
909            || !crate::gpu::q1_force()
910            || std::env::var("CMF_GPU_BLOCK")
911                .map(|v| v == "0")
912                .unwrap_or(false)
913        {
914            if std::env::var("CMF_GRAPH_DBG").is_ok() {
915                eprintln!(
916                    "block-graph: front gate (softcap={} enabled_here={} q1_force={})",
917                    self.attn_softcap > 0.0,
918                    crate::gpu::enabled_here(),
919                    crate::gpu::q1_force(),
920                );
921            }
922            return start;
923        }
924        // The graph encodes SiLU FFN, 1/√hd attention scores and
925        // full-context attend with no branch norms — Gemma-style archs
926        // (sliding window, scale override, sandwich norms, GeLU) fall
927        // back to the CPU path.
928        if self.swa.is_some()
929            || self.global_attn.is_some()
930            || self.attention_heads_per_layer.is_some()
931            || self.attn_v_norm
932            || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
933            || self.weights.layers.iter().any(|lw| {
934                lw.attn_out_norm.is_some()
935                    || lw.ffn_out_norm.is_some()
936                    || lw.layer_scale.is_some()
937                    || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
938            })
939        {
940            if std::env::var("CMF_GRAPH_DBG").is_ok() {
941                eprintln!(
942                    "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
943                    self.swa.is_some(),
944                    self.global_attn.is_some(),
945                    self.attention_heads_per_layer.is_some(),
946                    self.attn_v_norm,
947                    (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
948                );
949            }
950            return start;
951        }
952        // Looped Transformer: the graph covers ALL loop iterations;
953        // encode_loop_norm is inserted on-device at each boundary.
954        let limit = upto
955            .map(|u| u + 1)
956            .unwrap_or(self.num_layers)
957            .min(self.num_layers);
958
959        enum Item<'a> {
960            Gdn {
961                run: Vec<GdnGpuLayer<'a>>,
962                first: usize,
963            },
964            Attn {
965                l: AttnGpuLayer<'a>,
966                li: usize,
967                q_norm: Option<&'a [f32]>,
968                k_norm: Option<&'a [f32]>,
969                output_gate: bool,
970                bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
971                /// Attend on the device too (no sync): F32 KV, no
972                /// o1/bias, dims inside the kernels' contract.
973                full_gpu: bool,
974            },
975        }
976
977        // Device-attend KERNEL contract, shared by every Full layer. The
978        // hd>128 default-off POLICY is applied after the scan: it was
979        // measured on dense models, and a MoE plan inverts it — with the
980        // experts on device each CPU-attend sandwich costs a
981        // commit+wait, ~30 submits/token (W2 on M4: 14.7 tok/s
982        // sandwiched vs 27.1 device-attend vs 18.8 pure CPU).
983        let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
984        let attend_contract = attend_mode != "0"
985            && attend_mode != "off"
986            && self.head_dim % 4 == 0
987            && self.head_dim <= 256
988            && self.rotary_dim >= 2
989            && self.rotary_dim <= self.head_dim
990            && (self.rotary_dim / 2) % 32 == 0
991            && self.num_kv_heads > 0
992            && self.num_heads % self.num_kv_heads == 0;
993
994        let mut plan: Vec<Item> = Vec::new();
995        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
996        // Break-reason diagnostics ride the same env as the plan summary.
997        let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
998        let mut scan = start;
999        while scan < limit {
1000            let lw = &self.weights.layers[self.phys_layer(scan)];
1001            let ffn = match &lw.ffn {
1002                FfnKind::Dense(d) if d.segs.is_empty() => {
1003                    let (Some(g), Some(u), Some(dn)) = (
1004                        d.gate_proj.q1_parts(),
1005                        d.up_proj.q1_parts(),
1006                        d.down_proj.q1_parts(),
1007                    ) else {
1008                        if block_diag {
1009                            eprintln!(
1010                                "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1011                            );
1012                        }
1013                        break;
1014                    };
1015                    MetalFfn::Dense {
1016                        gate: g,
1017                        up: u,
1018                        down: dn,
1019                    }
1020                }
1021                FfnKind::Moe(m) => {
1022                    let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1023                        if block_diag {
1024                            eprintln!(
1025                                "block-graph: L{scan} MoE outside the graph contract — run ends"
1026                            );
1027                        }
1028                        break;
1029                    };
1030                    if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1031                        model_ref.get_or_insert_with(|| model.clone());
1032                    }
1033                    MetalFfn::Moe(moe)
1034                }
1035                _ => {
1036                    if block_diag {
1037                        eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1038                    }
1039                    break;
1040                }
1041            };
1042            match &lw.attn {
1043                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1044                    let parts = (
1045                        w.in_proj_qkv.q1_parts(),
1046                        w.in_proj_z.q1_parts(),
1047                        w.in_proj_a.f32_parts(),
1048                        w.in_proj_b.f32_parts(),
1049                        w.out_proj.q1_parts(),
1050                    );
1051                    let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1052                        if block_diag {
1053                            eprintln!(
1054                                "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1055                                w.in_proj_qkv.q1_parts().is_some(),
1056                                w.in_proj_z.q1_parts().is_some(),
1057                                w.in_proj_a.f32_parts().is_some(),
1058                                w.in_proj_b.f32_parts().is_some(),
1059                                w.out_proj.q1_parts().is_some(),
1060                            );
1061                        }
1062                        break;
1063                    };
1064                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1065                        model_ref.get_or_insert_with(|| model.clone());
1066                    }
1067                    let gl = GdnGpuLayer {
1068                        attn_norm: &lw.input_norm,
1069                        post_norm: &lw.post_norm,
1070                        qkv,
1071                        z,
1072                        a,
1073                        b,
1074                        out,
1075                        ffn,
1076                        conv1d: &w.conv1d,
1077                        a_log: &w.a_log,
1078                        dt_bias: &w.dt_bias,
1079                        gnorm: &w.norm,
1080                    };
1081                    match plan.last_mut() {
1082                        Some(Item::Gdn { run, .. }) => run.push(gl),
1083                        _ => plan.push(Item::Gdn {
1084                            run: vec![gl],
1085                            first: scan,
1086                        }),
1087                    }
1088                }
1089                AttnKind::Full {
1090                    wq,
1091                    wk,
1092                    wv,
1093                    wo,
1094                    q_norm,
1095                    k_norm,
1096                    output_gate,
1097                    softplus_gate: None,
1098                    bias,
1099                } if !self.kv_cache.layers[scan].o1_sealed()
1100                    // Sealed o1 stays plannable when the Metal o1 port
1101                    // is on: full_gpu attends through the device state,
1102                    // and any refusal falls to the sandwich, whose CPU
1103                    // core routes sealed layers through the nystrom step.
1104                    || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1105                {
1106                    let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
1107                    let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1108                        break;
1109                    };
1110                    if let QTensor::Mapped { model, .. } = wq {
1111                        model_ref.get_or_insert_with(|| model.clone());
1112                    }
1113                    let cache = &self.kv_cache.layers[scan];
1114                    // O(1) layer on Metal: the device attends through the
1115                    // sealed Nystrom state (opt-in while the port proves
1116                    // itself). Unsealed -> sandwich path = the CPU o1 step.
1117                    let o1_metal = cache.o1.is_some()
1118                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1119                        && cache.o1_views().is_some();
1120                    let full_gpu = attend_contract
1121                        && cache.mode == crate::kv_cache::KvMode::F32
1122                        && (cache.o1.is_none() || o1_metal)
1123                        && bias.is_none()
1124                        && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1125                        && pk.1 == self.num_kv_heads * self.head_dim
1126                        && pv.1 == self.num_kv_heads * self.head_dim
1127                        && po.2 == self.num_heads * self.head_dim;
1128                    plan.push(Item::Attn {
1129                        l: AttnGpuLayer {
1130                            attn_norm: &lw.input_norm,
1131                            post_norm: &lw.post_norm,
1132                            wq: pq,
1133                            wk: pk,
1134                            wv: pv,
1135                            wo: po,
1136                            ffn,
1137                        },
1138                        li: scan,
1139                        q_norm: q_norm.as_deref(),
1140                        k_norm: k_norm.as_deref(),
1141                        output_gate: *output_gate,
1142                        bias: bias
1143                            .as_ref()
1144                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1145                        full_gpu,
1146                    });
1147                }
1148                _ => break,
1149            }
1150            scan += 1;
1151        }
1152        let Some(model) = model_ref else {
1153            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1154                eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1155            }
1156            return start;
1157        };
1158        if plan.is_empty() {
1159            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1160                eprintln!("q1-graph: empty plan at layer {start}");
1161            }
1162            return start;
1163        }
1164        let has_moe = plan.iter().any(|it| match it {
1165            Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1166            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1167        });
1168        let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1169        let dev_attend = attend_contract
1170            && (self.head_dim <= 128
1171                || has_moe
1172                // A GDN hybrid attends on a quarter of its layers: the
1173                // hd>128 caution was measured on pure-dense models where
1174                // gqa_attend dominates, and on Qwen3.8-27B (hd 256, 48
1175                // GDN + 16 attn) the sandwich costs 2x the whole decode
1176                // (1.2 vs 2.21 tok/s measured before the arena fix).
1177                || (self.head_dim <= 256 && has_gdn)
1178                || attend_mode == "force"
1179                || attend_mode == "256");
1180        if !dev_attend {
1181            for it in &mut plan {
1182                if let Item::Attn { li, full_gpu, .. } = it {
1183                    // The hd>128 policy is about gqa_attend; an o1 layer
1184                    // attends through its own kernel set.
1185                    let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1186                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1187                    if !keep_o1 {
1188                        *full_gpu = false;
1189                    }
1190                }
1191            }
1192        }
1193        if std::env::var("CMF_GRAPH_DBG").is_ok() {
1194            use std::sync::atomic::{AtomicBool, Ordering};
1195            static SAID: AtomicBool = AtomicBool::new(false);
1196            if !SAID.swap(true, Ordering::Relaxed) {
1197                let fg = plan
1198                    .iter()
1199                    .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1200                    .count();
1201                let att = plan
1202                    .iter()
1203                    .filter(|it| matches!(it, Item::Attn { .. }))
1204                    .count();
1205                eprintln!(
1206                    "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1207                    plan.len(),
1208                    self.head_dim,
1209                    self.rotary_dim,
1210                    self.num_kv_heads,
1211                    self.num_heads,
1212                );
1213            }
1214        }
1215        let dims = GraphDims {
1216            hidden: self.hidden_size,
1217            eps: self.rms_eps as f32,
1218            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1219        };
1220        let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1221            return start;
1222        };
1223        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1224            nv: cfg.num_v_heads,
1225            nk: cfg.num_k_heads,
1226            dk: cfg.key_head_dim,
1227            dv: cfg.value_head_dim,
1228            kk: cfg.conv_kernel,
1229            hidden: self.hidden_size,
1230            inter: self.intermediate_size,
1231            c_dim: cfg.conv_dim(),
1232            eps: cfg.rms_eps as f32,
1233            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1234        });
1235        // Validate the whole plan BEFORE encoding anything: after the
1236        // first sync a refused layer would leave the token
1237        // half-executed, so truncate to the provably encodable prefix.
1238        let mut valid = 0usize;
1239        let mut end = start;
1240        crate::gpu::stageprof(1, _mt0.elapsed()); // конец планирования
1241        if std::env::var("CMF_PLAN_DUMP").is_ok() {
1242            static ONCE: std::sync::Once = std::sync::Once::new();
1243            ONCE.call_once(|| {
1244                for it in &plan {
1245                    match it {
1246                        Item::Gdn { first, run } => {
1247                            eprintln!("plan: Gdn first={first} len={}", run.len())
1248                        }
1249                        Item::Attn { li, full_gpu, .. } => {
1250                            eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1251                        }
1252                    }
1253                }
1254            });
1255        }
1256        for item in &plan {
1257            let ok = match item {
1258                Item::Gdn { run, .. } => gcfg
1259                    .as_ref()
1260                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1261                    .unwrap_or(false),
1262                Item::Attn { l, .. } => graph.attn_ok(l),
1263            };
1264            if !ok {
1265                if block_diag {
1266                    eprintln!(
1267                        "block-graph: plan item {} ({}) failed graph preflight",
1268                        valid,
1269                        match item {
1270                            Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1271                            Item::Attn { li, .. } => format!("Attn L{li}"),
1272                        }
1273                    );
1274                }
1275                break;
1276            }
1277            valid += 1;
1278            end += match item {
1279                Item::Gdn { run, .. } => run.len(),
1280                Item::Attn { .. } => 1,
1281            };
1282        }
1283        plan.truncate(valid);
1284        if plan.is_empty() {
1285            return start;
1286        }
1287
1288        let inv_freq = self.inv_freq.clone();
1289        let pool = self.pool.clone();
1290        let (nh, nkv, hd, hs, rd, eps) = (
1291            self.num_heads,
1292            self.num_kv_heads,
1293            self.head_dim,
1294            self.hidden_size,
1295            self.rotary_dim,
1296            self.rms_eps,
1297        );
1298        let norm_style = self.norm_style;
1299        let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1300        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1301        let kv_id = self.graph_kv_id;
1302        // GDN runs whose states await readback after the next sync
1303        // (device-attended layers add no sync, so several may stack).
1304        let mut pending: Vec<(usize, usize)> = Vec::new();
1305        // Device-attended layers: their K/V/imp are pulled from the
1306        // mirror after the final sync.
1307        let mut dev_attn: Vec<usize> = Vec::new();
1308        for item in &plan {
1309            let _xt0 = std::time::Instant::now();
1310            let _xkind: u32 = match item {
1311                Item::Gdn { .. } => 2,
1312                Item::Attn { .. } => 3,
1313            };
1314            // Looped Transformer: insert on-device norm at loop boundaries.
1315            if self.loop_final_norm {
1316                let item_start = match item {
1317                    Item::Gdn { first, .. } => *first,
1318                    Item::Attn { li, .. } => *li,
1319                };
1320                if item_start > start && self.is_loop_end(item_start - 1) {
1321                    graph.encode_loop_norm(&self.weights.final_norm);
1322                }
1323            }
1324            match item {
1325                Item::Gdn { run, first } => {
1326                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1327                        if l.linear_state.len() != want {
1328                            l.linear_state = vec![0f32; want];
1329                        }
1330                    }
1331                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1332                        .iter()
1333                        .map(|l| l.linear_state.as_slice())
1334                        .collect();
1335                    let _ig = std::time::Instant::now();
1336                    if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1337                        // Unreachable: the plan was validated above.
1338                        tracing::error!("q1 graph: GDN run refused after validation");
1339                        return start;
1340                    }
1341                    // Early commit: the GPU starts the run while the
1342                    // CPU encodes the next layer (nothing to wait on).
1343                    graph.commit_kind = 2;
1344                    graph.commit();
1345                    crate::gpu::stageprof(0, _ig.elapsed());
1346                    pending.push((*first, run.len()));
1347                }
1348                Item::Attn {
1349                    l,
1350                    li,
1351                    q_norm,
1352                    k_norm,
1353                    output_gate,
1354                    bias,
1355                    full_gpu,
1356                } => {
1357                    let _ia = std::time::Instant::now();
1358                    // ── Fully device-resident attention: no sync at all.
1359                    if *full_gpu {
1360                        let cache = &self.kv_cache.layers[*li];
1361                        let o1p = if cache.o1.is_some() {
1362                            match cache.o1_views() {
1363                                Some(views) => Some(crate::gpu::O1AttnParams {
1364                                    views,
1365                                    epoch: self.o1_epoch,
1366                                }),
1367                                // Sealed state gone mid-run: sandwich.
1368                                None => None,
1369                            }
1370                        } else {
1371                            None
1372                        };
1373                        let o1_layer = cache.o1.is_some();
1374                        if o1_layer && o1p.is_none() {
1375                            // fall to the sandwich (CPU o1 step)
1376                        }
1377                        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1378                        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1379                        let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1380                        let p = crate::gpu::AttnDeviceParams {
1381                            kv_id,
1382                            layer: *li,
1383                            nh,
1384                            nkv,
1385                            hd,
1386                            rd,
1387                            position,
1388                            eps: eps as f32,
1389                            gemma,
1390                            output_gate: *output_gate,
1391                            q_norm: *q_norm,
1392                            k_norm: *k_norm,
1393                            inv_freq: &inv_freq,
1394                            cpu_k,
1395                            cpu_v,
1396                            cpu_stored,
1397                            o1: o1p,
1398                        };
1399                        let o1_bad = o1_layer && p.o1.is_none();
1400                        if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1401                        {
1402                            // o1 layers leave no mirror row to pull.
1403                            if p.o1.is_none() {
1404                                dev_attn.push(*li);
1405                            }
1406                            graph.commit_kind = 3;
1407                            graph.commit();
1408                            // The footer below is skipped by `continue`:
1409                            // account the device-attn item here or its
1410                            // cost hides from the stage profile entirely.
1411                            crate::gpu::stageprof(_xkind, _xt0.elapsed());
1412                            continue;
1413                        }
1414                        // Mirror refused (nothing encoded) → sandwich.
1415                    }
1416                    graph.encode_attn_prefix(l);
1417                    graph.sync();
1418                    if !pending.is_empty() {
1419                        let idxs: Vec<usize> =
1420                            pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1421                        let mut outs: Vec<&mut [f32]> = self
1422                            .kv_cache
1423                            .layers
1424                            .iter_mut()
1425                            .enumerate()
1426                            .filter(|(i, _)| idxs.binary_search(i).is_ok())
1427                            .map(|(_, s)| s.linear_state.as_mut_slice())
1428                            .collect();
1429                        graph.read_states(&mut outs);
1430                    }
1431                    let mut q_raw = attention::take_buf(l.wq.1);
1432                    let mut k = attention::take_buf(l.wk.1);
1433                    let mut v = attention::take_buf(l.wv.1);
1434                    graph.read_qkv(&mut q_raw, &mut k, &mut v);
1435                    let cfg = QwenAttnCfg {
1436                        num_heads: nh,
1437                        num_kv_heads: nkv,
1438                        head_dim: hd,
1439                        hidden_size: hs,
1440                        position,
1441                        inv_freq: &inv_freq,
1442                        rotary_dim: rd,
1443                        scale: self.attn_scale,
1444                        softcap: self.attn_softcap,
1445                        window: None,
1446                        v_norm: false,
1447                        q_norm: *q_norm,
1448                        k_norm: *k_norm,
1449                        output_gate: *output_gate,
1450                        softplus_gate: None,
1451                        rope_scale: 1.0,
1452                        bias: *bias,
1453                        rms_eps: eps,
1454                        norm_style,
1455                        pool: pool.as_deref(),
1456                    };
1457                    // CMF_ATTN_ORACLE=1: diff the device attend against
1458                    // this CPU attend on identical inputs (bring-up).
1459                    let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1460                        || std::env::var("CMF_ATTN_DUMP").is_ok();
1461                    let _ = full_gpu;
1462                    let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1463                    let mut ao = attention::qwen_attention_core(
1464                        q_raw,
1465                        k,
1466                        v,
1467                        &mut self.kv_cache.layers[*li],
1468                        &cfg,
1469                    );
1470                    // CMF_ATTN_DUMP=<dir>: this token's rope'd Q and the layer's whole
1471                    // K/V cache as raw f32 (offline attention-statistics probes:
1472                    // block bounds, mass concentration). Needs CMF_GPU_ATTEND=0.
1473                    if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1474                        if let Some((qr0, k0, v0)) = oracle_in.clone() {
1475                            let (cq, _cg, _ck, _cv) = attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1476                            let cache = &self.kv_cache.layers[*li];
1477                            let n = cache.head_keys(0).len() / hd;
1478                            let mut bytes: Vec<u8> = Vec::new();
1479                            for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1480                                bytes.extend_from_slice(&v.to_le_bytes());
1481                            }
1482                            for v in &cq {
1483                                bytes.extend_from_slice(&v.to_le_bytes());
1484                            }
1485                            for g in 0..nkv {
1486                                for v in cache.head_keys(g) {
1487                                    bytes.extend_from_slice(&v.to_le_bytes());
1488                                }
1489                            }
1490                            for g in 0..nkv {
1491                                for v in cache.head_values(g) {
1492                                    bytes.extend_from_slice(&v.to_le_bytes());
1493                                }
1494                            }
1495                            let _ = std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1496                        }
1497                    }
1498                    if let Some((qr0, k0, v0)) = oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")) {
1499                        let (cq, _cg, ck, cv) = attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1500                        let mut h_now = vec![0f32; hs];
1501                        graph.read_h(&mut h_now);
1502                        let cache = &self.kv_cache.layers[*li];
1503                        let n_after = cache.head_keys(0).len() / hd;
1504                        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| &cache.head_keys(g)[..(n_after - 1) * hd]).collect();
1505                        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| &cache.head_values(g)[..(n_after - 1) * hd]).collect();
1506                        let p = crate::gpu::AttnDeviceParams {
1507                            kv_id,
1508                            layer: *li,
1509                            nh,
1510                            nkv,
1511                            hd,
1512                            rd,
1513                            position,
1514                            eps: eps as f32,
1515                            gemma,
1516                            output_gate: *output_gate,
1517                            q_norm: *q_norm,
1518                            k_norm: *k_norm,
1519                            inv_freq: &inv_freq,
1520                            cpu_k,
1521                            cpu_v,
1522                            cpu_stored: n_after - 1,
1523                            o1: None,
1524                        };
1525                        if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1526                            let md = |a: &[f32], b: &[f32]| a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
1527                            let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1528                            eprintln!(
1529                                "attn-oracle L{li} pos {position}: |q| {:.2} max|dq| {:.4} | |k| {:.2} max|dk| {:.4} | |v| {:.2} max|dv| {:.4} | |ao| {:.2} max|dao| {:.4}",
1530                                nn(&cq), md(&cq, &dq), nn(&ck), md(&ck, &dk), nn(&cv), md(&cv, &dv), nn(&ao), md(&ao, &dao)
1531                            );
1532                        } else {
1533                            eprintln!("attn-oracle L{li}: device probe declined");
1534                        }
1535                    }
1536                    graph.encode_attn_suffix(l, &ao);
1537                    // Early commit: the GPU starts O+FFN while the CPU
1538                    // encodes the following GDN run / attention prefix.
1539                    graph.commit();
1540                    attention::recycle_buf(&mut ao);
1541                }
1542            }
1543
1544            crate::gpu::stageprof(_xkind, _xt0.elapsed());
1545        }
1546        // Ride the final norm + lm_head in the same command buffer when
1547        // this run reaches the model's end and the caller wants logits:
1548        // the separate per-op lm_head submit (a full round trip) folds
1549        // into the sync that already happens here.
1550        let mut lm_rows = None;
1551        if self.graph_want_logits
1552            && upto.is_none()
1553            && end == self.num_layers
1554            && std::env::var("CMF_GPU_LMHEAD")
1555                .map(|v| v != "0")
1556                .unwrap_or(true)
1557        {
1558            if let Some(lm) = self.weights.lm_head.q1_parts() {
1559                if graph.lm_head_ok(lm) {
1560                    graph.encode_lm_head(&self.weights.final_norm, lm);
1561                    lm_rows = Some(lm.1);
1562                }
1563            }
1564        }
1565        let _sy0 = std::time::Instant::now();
1566        graph.sync();
1567        let _rs0 = std::time::Instant::now();
1568        if !pending.is_empty() {
1569            let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1570            let mut outs: Vec<&mut [f32]> = self
1571                .kv_cache
1572                .layers
1573                .iter_mut()
1574                .enumerate()
1575                .filter(|(i, _)| idxs.binary_search(i).is_ok())
1576                .map(|(_, s)| s.linear_state.as_mut_slice())
1577                .collect();
1578            graph.read_states(&mut outs);
1579        }
1580        if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1581            use std::sync::atomic::{AtomicU64, Ordering};
1582            static SY: AtomicU64 = AtomicU64::new(0);
1583            static RS: AtomicU64 = AtomicU64::new(0);
1584            static N: AtomicU64 = AtomicU64::new(0);
1585            SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1586            RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1587            let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1588            if n % 100 == 0 {
1589                eprintln!(
1590                    "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1591                    SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1592                    RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1593                );
1594            }
1595        }
1596        if let Some(rows) = lm_rows {
1597            crate::gpu::hostprof_encode_done(_mt0);
1598            let mut lg = attention::take_buf(rows.min(self.vocab_size));
1599            graph.read_logits(&mut lg);
1600            crate::gpu::hostprof_total(_mt0);
1601            lg.resize(self.vocab_size, 0.0);
1602            if let Some(c) = self.final_softcap {
1603                for l in lg.iter_mut() {
1604                    *l = c * (*l / c).tanh();
1605                }
1606            }
1607            self.graph_logits = Some(lg);
1608        }
1609        graph.finish(h);
1610        // Device-attended layers: replay the CPU bookkeeping — append
1611        // the mirror's new K/V row (rope'd on the GPU) into the owner
1612        // cache, then bank this token's Born-importance mass.
1613        for li in dev_attn {
1614            let mut krow = attention::take_buf(nkv * hd);
1615            let mut vrow = attention::take_buf(nkv * hd);
1616            if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1617                let cache = &mut self.kv_cache.layers[li];
1618                cache.append(&krow, &vrow, &[]);
1619                let n = cache.seq_len;
1620                let mut imp = attention::take_buf(n);
1621                crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1622                cache.accumulate_imp(&imp);
1623                attention::recycle_buf(&mut imp);
1624            }
1625            attention::recycle_buf(&mut krow);
1626            attention::recycle_buf(&mut vrow);
1627        }
1628        end
1629    }
1630
1631    pub fn new(
1632        tokenizer: Tokenizer,
1633        weights: PipelineWeights,
1634        hidden_size: usize,
1635        intermediate_size: usize,
1636        num_heads: usize,
1637        num_kv_heads: usize,
1638        head_dim: usize,
1639        num_layers: usize,
1640        physical_layers: usize,
1641        loop_final_norm: bool,
1642        vocab_size: usize,
1643        rms_eps: f64,
1644        rope_base: f32,
1645        norm_style: NormStyle,
1646        max_seq_len: usize,
1647        sampler_config: SamplerConfig,
1648    ) -> Self {
1649        let rng = match sampler_config.seed {
1650            Some(s) => SplitMix64::new(s),
1651            None => SplitMix64::from_entropy(),
1652        };
1653        let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1654        let pool = Pool::from_env();
1655        if let Some(p) = &pool {
1656            tracing::info!("worker pool: {} threads", p.n_workers());
1657        }
1658        Self {
1659            gpu_plan: None,
1660            tokenizer: std::sync::Arc::new(tokenizer),
1661            kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1662            sampler_config,
1663            weights,
1664            hidden_size,
1665            intermediate_size,
1666            num_heads,
1667            num_kv_heads,
1668            head_dim,
1669            num_layers,
1670            physical_layers,
1671            loop_final_norm,
1672            vocab_size,
1673            rms_eps,
1674            rope_base,
1675            norm_style,
1676            rotary_dim: head_dim,
1677            attention_heads_per_layer: None,
1678            vmf_cfg: None,
1679            gdn_cfg: None,
1680            kda_cfg: None,
1681            g3n: None,
1682            dsv4: None,
1683            dsv4_mtp: Vec::new(),
1684            dspark: None,
1685            dspark_pending: Vec::new(),
1686            dspark_hist: Vec::new(),
1687            dspark_real: Vec::new(),
1688            dspark_trunk_picks: Vec::new(),
1689            dspark_exp: Vec::new(),
1690            dspark_draft_ns: 0,
1691            logit_multiplier: None,
1692            cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1693            kv_history: Vec::new(),
1694            short_conv_cfg: None,
1695            mtp: None,
1696            speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1697            rng,
1698            sampler_scratch: SamplerScratch::default(),
1699            spec_forced: None,
1700            spec_q: Vec::new(),
1701            spec_p: Vec::new(),
1702            spec_res: Vec::new(),
1703            spec_qs: Vec::new(),
1704            spec_ps: Vec::new(),
1705            spec_ress: Vec::new(),
1706            mtp_graph_mode: None,
1707            #[cfg(target_os = "macos")]
1708            metal_verify: None,
1709            inv_freq,
1710            ws: ForwardScratch::new(hidden_size),
1711            pool,
1712            model: None,
1713            dyn_force_f32: false,
1714            dyn_skill_layers: Vec::new(),
1715            dyn_active: None,
1716            dyn_blend_loaded: false,
1717            dyn_phi_layer: None,
1718            dyn_phi_ema: Vec::new(),
1719            dyn_phi_seen: 0,
1720            dyn_router: None,
1721            o1_cfg: None,
1722            o1_epoch: 0,
1723            o1_flags: Vec::new(),
1724            trace: false,
1725            calib_temp: 1.0,
1726            confidence_on: true,
1727            embed_multiplier: 1.0,
1728            attn_scale: 1.0 / (head_dim as f32).sqrt(),
1729            swa: None,
1730            sliding_layers: None,
1731            inv_freq_local: None,
1732            rotary_dim_local: None,
1733            rope_scale: 1.0,
1734            rope_scale_local: 1.0,
1735            global_attn: None,
1736            inv_freq_global: None,
1737            attn_v_norm: false,
1738            final_softcap: None,
1739            head_clusters: None,
1740            attn_softcap: 0.0,
1741            graph_want_logits: false,
1742            graph_logits: None,
1743            graph_kv_id: {
1744                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1745                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1746            },
1747        }
1748    }
1749
1750    /// Enable/disable per-layer O(1) Nyström attention. Only Full
1751    /// layers are eligible (a linear layer keeps its own operator).
1752    /// Applies to generation (`generate*`/`forward_ids`): the prompt
1753    /// pass stays exact, the seal happens once after prefill, decode
1754    /// runs on the O(1) state. Teacher-forced scoring (`ppl_ids`)
1755    /// intentionally stays exact.
1756    pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1757        self.o1_flags = match &cfg {
1758            Some(c) => {
1759                let mut flags = c.layer_flags(self.num_layers);
1760                for (li, f) in flags.iter_mut().enumerate() {
1761                    if *f
1762                        && !matches!(
1763                            self.weights.layers[self.phys_layer(li)].attn,
1764                            AttnKind::Full { .. }
1765                        )
1766                    {
1767                        *f = false;
1768                    }
1769                }
1770                flags
1771            }
1772            None => Vec::new(),
1773        };
1774        if let Some(c) = &cfg {
1775            let n = self.o1_flags.iter().filter(|&&f| f).count();
1776            tracing::info!(
1777                "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1778                self.num_layers,
1779                c.m,
1780                c.w,
1781                c.sink,
1782                c.rect
1783            );
1784        }
1785        self.o1_cfg = cfg;
1786    }
1787
1788    /// True when at least one layer runs the O(1) kernel.
1789    pub fn o1_active(&self) -> bool {
1790        self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1791    }
1792
1793    /// Arm query collection on the o1 layers (fresh prompt pass).
1794    /// Reset the o1 layers to Collecting for a fresh sequence. Pub for the
1795    /// network split: each side runs the o1 lifecycle over ITS OWN layers
1796    /// (begin before prefill, seal at the prefill barrier).
1797    pub fn o1_begin(&mut self) {
1798        if let Some(c) = &self.o1_cfg {
1799            let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1800            for (li, &f) in self.o1_flags.iter().enumerate() {
1801                if f {
1802                    self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1803                }
1804            }
1805        }
1806    }
1807
1808    /// Freeze landmarks + skeleton state after the prompt pass and drop
1809    /// the o1 layers' full KV; decode then runs `step()` per token.
1810    /// Pub for the network split (see `o1_begin`).
1811    pub fn o1_seal(&mut self) {
1812        self.o1_epoch = self.o1_epoch.wrapping_add(1);
1813        if self.o1_cfg.is_none() {
1814            return;
1815        }
1816        for li in 0..self.num_layers {
1817            if self.o1_flags.get(li).copied().unwrap_or(false) {
1818                self.kv_cache.layers[li].o1_seal(self.num_heads);
1819            }
1820        }
1821    }
1822
1823    /// Enable/disable the structured per-token telemetry trace (B4).
1824    pub fn set_trace(&mut self, on: bool) {
1825        self.trace = on;
1826    }
1827
1828    /// Replace all request-scoped sampler options and reset the random stream.
1829    /// This is required for deterministic `seed` semantics in pooled servers.
1830    pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1831        self.rng = match config.seed {
1832            Some(seed) => SplitMix64::new(seed),
1833            None => SplitMix64::from_entropy(),
1834        };
1835        self.sampler_config = config;
1836    }
1837
1838    /// Toggle the per-token Born-confidence reduction (a full-vocab
1839    /// softmax each token). `bench --core` turns it off so the timed
1840    /// loop matches llama-bench's core contract; the result's
1841    /// `confidence` vec is empty while off.
1842    pub fn set_confidence(&mut self, on: bool) {
1843        self.confidence_on = on;
1844    }
1845
1846    /// Set the confidence-calibration temperature (B1). Values ≤0 are
1847    /// clamped to raw (1.0).
1848    pub fn set_calib_temp(&mut self, t: f32) {
1849        self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1850    }
1851
1852    /// The active calibration temperature (1.0 = raw Born mass).
1853    pub fn calib_temp(&self) -> f32 {
1854        self.calib_temp
1855    }
1856
1857    /// Partial rotary (Qwen3.5): rotate only the first `rotary_dim` dims;
1858    /// the frequency table is rebuilt over the rotary dims.
1859    pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1860        self.rotary_dim = rotary_dim.min(self.head_dim);
1861        self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1862    }
1863
1864    fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1865        QwenAttnCfg {
1866            num_heads: self.num_heads,
1867            num_kv_heads: self.num_kv_heads,
1868            head_dim: self.head_dim,
1869            hidden_size: self.hidden_size,
1870            position,
1871            inv_freq: &self.inv_freq,
1872            rotary_dim: self.rotary_dim,
1873            scale: self.attn_scale,
1874            softcap: self.attn_softcap,
1875            window: None,
1876            v_norm: false,
1877            q_norm: None,
1878            k_norm: None,
1879            output_gate: false,
1880            softplus_gate: None,
1881            rope_scale: self.rope_scale,
1882            bias: None,
1883            rms_eps: self.rms_eps,
1884            norm_style: self.norm_style,
1885            pool: self.pool.as_deref(),
1886        }
1887    }
1888
1889    /// Generate text from a plain-text prompt. Streams tokens via `on_token`.
1890    pub fn generate(
1891        &mut self,
1892        prompt: &str,
1893        max_tokens: usize,
1894        task_mask: Option<&TaskMask>,
1895        on_token: Option<TokenCallback>,
1896    ) -> Result<GenerateResult, String> {
1897        let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1898        self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1899    }
1900
1901    /// `None` when the mask forbids nothing (see `TaskMask::fully_open`).
1902    fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
1903        m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
1904    }
1905
1906    /// Generate from prepared token ids (e.g. a chat template).
1907    ///
1908    /// With an MTP head, greedy generation without a task mask takes the
1909    /// speculative path: the MTP module drafts the token after next and
1910    /// the main model verifies both in one fused two-position forward
1911    /// (weights streamed once). The output is EXACTLY the vanilla greedy
1912    /// sequence — a rejected draft is rolled back — MTP only buys speed.
1913    pub fn generate_from_ids(
1914        &mut self,
1915        input_ids: &[u32],
1916        max_tokens: usize,
1917        task_mask: Option<&TaskMask>,
1918        mut on_token: Option<TokenCallback>,
1919    ) -> Result<GenerateResult, String> {
1920        if std::env::var("CMF_TRACE_H").is_ok() {
1921            eprintln!("input_ids: {input_ids:?}");
1922        }
1923        if input_ids.is_empty() {
1924            return Err("empty prompt: nothing to generate from".to_string());
1925        }
1926        // A mask that forbids nothing still costs every fused path and
1927        // whole-token graph, all of which are gated on `is_none()`. A
1928        // narrowed file whose one segment is always on carries exactly
1929        // such a mask — drop it here rather than pay 5x for a no-op.
1930        let task_mask = self.drop_open_mask(task_mask);
1931
1932        // Cross-turn KV reuse: a chat app resends the whole history
1933        // every turn; when the new ids strictly EXTEND what the cache
1934        // already holds, prefill only the tail — turn latency stays
1935        // proportional to the new text instead of the whole session.
1936        // Extension-only (no rollback), so it is exact for every layer
1937        // kind including recurrent state; MTP/o1/task-mask runs keep
1938        // the fresh-sequence path. CMF_KV_REUSE=0 disables.
1939        let reuse_from = {
1940            let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
1941            let h = &self.kv_history;
1942            if on
1943                && task_mask.is_none()
1944                && self.mtp.is_none()
1945                && self.o1_cfg.is_none()
1946                && !h.is_empty()
1947                && h.len() < input_ids.len()
1948                && input_ids[..h.len()] == h[..]
1949            {
1950                h.len()
1951            } else {
1952                0
1953            }
1954        };
1955        if reuse_from == 0 {
1956            // Fresh sequence — the cache holds absolute positions.
1957            self.kv_cache.clear();
1958            self.kv_history.clear();
1959            crate::gpu::graph_kv_reset(self.graph_kv_id);
1960        } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
1961            eprintln!(
1962                "kv-reuse: {} of {} prompt positions already cached",
1963                reuse_from,
1964                input_ids.len()
1965            );
1966        }
1967        crate::gpu::graph_race_begin_generation();
1968        self.o1_begin();
1969
1970        // Speculative decode is off under o1: a rejected draft can't be
1971        // rolled back out of the far accumulators / ring window (the
1972        // Nyström insertion is irreversible by design).
1973        // The wgpu token graph owns a device K/V mirror that speculative
1974        // rollback would desync — the two are mutually exclusive.
1975        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
1976        // Graph speculative decode (`CMF_GRAPH_SPEC=1`): the MTP head
1977        // drafts, ONE batched graph submit verifies the whole chain.
1978        //
1979        // It now PAYS on Qwen3.6-27B / RTX 5090 — 51.1 tok/s against a
1980        // plain 49.4 at k=3, medians of three, 89% of drafts accepted,
1981        // and the greedy continuation is byte-identical to the plain
1982        // path. That took the batch matvec sharing its nibble unpack
1983        // across the batch (`CMF_MV_BK=2`); before it, the same round
1984        // measured 43.6, an 11% LOSS, which is what the earlier note
1985        // here described.
1986        //
1987        // Still opt-in. One model's win is not a default: the verify
1988        // rides `gdn_spec_restore` and a batched frame whose numerics
1989        // are the batch kernels', and that has to be shown on more than
1990        // one architecture before every greedy decode takes it.
1991        // Greedy (with or without penalties) verifies by argmax equality.
1992        // Sampling (temperature > 0) can go through speculative SAMPLING —
1993        // draft from the MTP head's own post-chain distribution, accept
1994        // with min(1, p/q), correct from max(0, p − q); the emitted stream
1995        // is distributed exactly as the plain sampler's — but it is
1996        // OPT-IN (`CMF_GRAPH_SPEC_SAMPLE=1`): measured on Qwen3.8-27B /
1997        // RTX 5090 at the instruct row (0.7 / 0.80 / 20 / presence 1.5)
1998        // it decoded 19-22 tok/s against a plain 40 — nine post-chain
1999        // distributions a round plus a lower acceptance than greedy's,
2000        // against a verify that costs 2.7 single tokens. The greedy arms
2001        // pay +10%; the sampling arm needs a cheaper verify first.
2002        let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2003            || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2004        // ON by default for greedy on the wgpu graph: with the draft on
2005        // the graph and the verify bit-exact, it measured 58.7 tok/s
2006        // against a plain 48.1 on Qwen3.8-27B q4tp / RTX 5090 (k=4) and
2007        // 51.1 against 49.4 on Qwen3.6-27B, and a round that stops
2008        // paying turns itself off below (acceptance watchdog).
2009        // `CMF_GRAPH_SPEC=0` disables; `=1` was the old opt-in spelling.
2010        // …but only where the batched verify has its register-blocked
2011        // kernel: q4tp dense FFNs (graph kind 6). q4t and q8_2f verify
2012        // through tile GEMMs today and measured a LOSS (q8_2f 22 against
2013        // 29 tok/s), the 2-bit plane the same; those stay opt-in
2014        // (`CMF_GRAPH_SPEC=1`).
2015        // …at least in nine dense FFNs of ten: a healed file carries its
2016        // last two layers at q8_2f, and two tile-GEMM verifies among 64 do
2017        // not change the arithmetic (measured: the healed q4tp file
2018        // decodes at the plain file's rate and would otherwise sit out).
2019        let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2020        for lw in &self.weights.layers {
2021            if let FfnKind::Dense(d) = &lw.ffn {
2022                dense_n += 1;
2023                if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2024                    && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2025                    && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2026                {
2027                    dense_q4tp += 1;
2028                }
2029            }
2030        }
2031        let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2032        // Penalties break the draft head's agreement with the trunk (a
2033        // 1.1 repetition penalty measured 2 of 16 accepted): not by
2034        // default there either.
2035        let penalized = self.sampler_config.repetition_penalty != 1.0
2036            || self.sampler_config.presence_penalty != 0.0
2037            || !self.sampler_config.suppress_tokens.is_empty();
2038        // …and not on wgpu-over-Metal: the batched verify graph there
2039        // returned 0 accepted drafts and garbage text on a GDN hybrid
2040        // (16.08, Qwen3.5-0.8B) while Vulkan is bit-exact; the Mac's
2041        // default backend is native Metal without a batch graph anyway.
2042        #[cfg(feature = "gpu")]
2043        let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2044        #[cfg(not(feature = "gpu"))]
2045        let metal_wgpu = false;
2046        let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2047        let spec_wanted = match spec_env.as_deref() {
2048            Some("0") => false,
2049            Some(_) => {
2050                if metal_wgpu {
2051                    tracing::warn!(
2052                        "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2053                         verified on this backend (garbage measured on Qwen3.5-0.8B)"
2054                    );
2055                }
2056                true
2057            }
2058            None => spec_default_ok && !penalized && !metal_wgpu,
2059        };
2060        // Native Metal: the b-row verify graph (`try_batch_graph_metal`)
2061        // stands where the wgpu batch graph stands on discrete cards.
2062        #[cfg(target_os = "macos")]
2063        let metal_graph = crate::gpu::q1_force()
2064            && crate::gpu::enabled_here()
2065            && std::env::var("CMF_GPU_BLOCK").map(|v| v != "0").unwrap_or(true);
2066        #[cfg(not(target_os = "macos"))]
2067        let metal_graph = false;
2068        let graph_spec = self.speculative
2069            && (graph_on || metal_graph)
2070            && self.mtp.is_some()
2071            && task_mask.is_none()
2072            && !self.o1_active()
2073            && spec_sampling_ok
2074            && spec_wanted;
2075        // GDN hybrids sit the fused-pair speculation out by default: the
2076        // recurrence is sequential, so the pair lane cannot parallelize
2077        // (the bench's own Pair line reads fused 1.28x TWO singles on the
2078        // 35B) and the draft's full-vocab head rides on top — measured 2x
2079        // SLOWER end to end (16.1 vs 32.4 tok/s on the 48-core stand).
2080        // CMF_MTP=1 forces it back for study.
2081        let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2082        let spec_active = self.speculative
2083            && self.mtp.is_some()
2084            && task_mask.is_none()
2085            && !self.o1_active()
2086            && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2087        // The MTP module is detached during generation so its mutable
2088        // state does not fight the borrow on `self`.
2089        let mut mtp = if spec_active { self.mtp.take() } else { None };
2090        if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2091            eprintln!(
2092                "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2093                mtp.is_some(),
2094                self.speculative,
2095                self.sampler_config.temperature < 1e-6,
2096            );
2097        }
2098        if let Some(m) = &mut mtp {
2099            m.kv.clear();
2100            // The MTP block's own device mirror starts over with its cache.
2101            crate::gpu::graph_kv_reset(self.mtp_kv_id());
2102            self.mtp_graph_mode = None;
2103        }
2104        // Dynamic router detached during decode (same borrow trick as MTP).
2105        // Speculative decode and dynamic routing are mutually exclusive
2106        // for now — the fused-pair path doesn't carry per-token φ.
2107        let mut router = if mtp.is_none() {
2108            self.dyn_router.take()
2109        } else {
2110            None
2111        };
2112        if let Some(r) = &mut router {
2113            r.reset(); // active=backbone, matching a fresh overlay
2114            self.dyn_phi_seen = 0; // fresh φ EMA per generation
2115            let _ = self.set_active_skill(None);
2116        }
2117
2118        let mut all_ids = input_ids.to_vec();
2119        let mut generated = 0usize;
2120        let mut finish_reason = "max_tokens".to_string();
2121        let mut drafted = 0usize;
2122        let mut accepted = 0usize;
2123        // DeepSeek-V4's draft quality is strongly content-dependent.  Two
2124        // consecutive paid rounds with no extra token put it on a bounded
2125        // cooldown; predictable text keeps batching, ordinary prose falls
2126        // back to the exact walk instead of paying a slow draft forever.
2127        // Local to one generation so one difficult request cannot poison the
2128        // next one, and deliberately automatic — this is not a user knob.
2129        let mut dsv4_spec_bad = 0usize;
2130        let mut dsv4_spec_retry_at = 0usize;
2131        let mut confidence: Vec<f32> = Vec::new();
2132        let trace_on = self.trace;
2133        let calib_temp = self.calib_temp;
2134        let mut traces: Vec<TokenTrace> = Vec::new();
2135
2136        // ── Prefill: forward each prompt token once, KEEP the last hidden.
2137        //    Dense prefill runs in fused pairs (weights streamed once per
2138        //    two positions — bit-identical to sequential, proven by the
2139        //    pair tests). With MTP: warm the draft head on
2140        //    (hidden_p, token_{p+1}) pairs.
2141        let mut hidden = vec![0.0f32; self.hidden_size];
2142        let mut pos = reuse_from;
2143        // lm_head-in-graph is only sound when the very next logits
2144        // consumer is this loop's own (MTP and skill routing interleave
2145        // other forwards / can swap lm_head between forward and sample).
2146        // CMF_GPU_LMHEAD=0 keeps lm_head off the graph: the token reads back
2147        // the 8 KB hidden instead of ~1 MB of logits, and the head runs on
2148        // the host. A probe for how much of the graph's fixed per-token cost
2149        // is the logits readback (the layer sweep puts that fixed part at
2150        // 3.88 ms of an 18.5 ms frame).
2151        let fuse_lm = mtp.is_none()
2152            && router.is_none()
2153            && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2154        self.graph_logits = None;
2155        self.graph_want_logits = false;
2156        let _tpf = std::time::Instant::now();
2157        let batch_k = std::env::var("CMF_BATCH_K")
2158            .ok()
2159            .and_then(|v| v.parse::<usize>().ok())
2160            .unwrap_or(0);
2161        // DeepSeek-V4 owns a separate hyper-connection stack. Route it
2162        // before the generic prefill choices: those correctly reject an
2163        // empty `weights.layers`, but their final per-position fallback used
2164        // to consume the whole prompt before `dsv4::forward_chunk` could see
2165        // it. The batch implementation therefore existed without a live
2166        // production entry point.
2167        //
2168        // Bounded chunks preserve cancellation responsiveness. Only the
2169        // prompt's final chunk asks for logits; every earlier head projection
2170        // would produce 129 280 values that no caller reads.
2171        while self.dsv4.is_some()
2172            && mtp.is_none()
2173            && pos < input_ids.len()
2174            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2175        {
2176            let end = (pos + prefill_chunk()).min(input_ids.len());
2177            let ids: Vec<u32> = input_ids[pos..end].to_vec();
2178            let mut lg = Vec::new();
2179            if let Some(b) = &mut self.dsv4 {
2180                let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2181                crate::dsv4::forward_chunk(
2182                    g,
2183                    layers,
2184                    &cfg,
2185                    st,
2186                    &ids,
2187                    pos,
2188                    &self.inv_freq,
2189                    self.pool.as_deref(),
2190                    &mut lg,
2191                    end == input_ids.len(),
2192                );
2193            }
2194            if end == input_ids.len() {
2195                self.graph_logits = Some(lg);
2196            }
2197            pos = end;
2198            hidden = vec![0.0; self.hidden_size];
2199        }
2200        // With dynamic routing, prefill sequentially so the φ hook fires
2201        // over the PROMPT — the router enters decode with a warm φ (the
2202        // fused-pair path skips the per-layer φ capture). o1 layers
2203        // collect their query trace in both the single and pair paths.
2204        let dyn_prefill = router.is_some();
2205        // q1 hybrids on Metal: the per-position GPU token graph beats
2206        // the CPU chunk-GEMM (whose wall is the sequential scalar GDN
2207        // recurrence), so prefill goes position-by-position through the
2208        // same graph as decode. Pure-attention models keep the batched
2209        // path — there the chunk-GEMM amortization wins.
2210        let graph_prefill = self.graph_prefill_preferred();
2211        // Native Metal, q4tp GDN hybrids: the prompt through the b-row
2212        // rows graph — projections as GEMMs over up to 512 positions, the
2213        // GDN recurrence in registers on the device, K/V rows appended by
2214        // the chunk — instead of one token-graph submit per position (the
2215        // 27B: 8 tok/s → GEMM-bound). The MTP warm-up rows come out of one
2216        // batched run of the block per chunk. Any refusal leaves the rest
2217        // of the prompt to the sequential paths below.
2218        #[cfg(target_os = "macos")]
2219        if task_mask.is_none()
2220            && !dyn_prefill
2221            && crate::gpu::q1_force()
2222            && crate::gpu::enabled_here()
2223            && self.gdn_cfg.is_some()
2224            && self.g3n.is_none()
2225            && input_ids.len() > 8
2226            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2227            && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
2228        {
2229            let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
2230                .ok()
2231                .and_then(|v| v.parse().ok())
2232                .filter(|&v| (16..=512).contains(&v))
2233                .unwrap_or(256);
2234            let hs = self.hidden_size;
2235            let _tp = std::time::Instant::now();
2236            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2237                let end = (pos + chunk).min(input_ids.len());
2238                let Some(hb) = self.prefill_batch_metal(&input_ids[pos..end], pos) else {
2239                    break;
2240                };
2241                if let Some(m) = &mut mtp {
2242                    let n_pairs = if end < input_ids.len() { end - pos } else { end - pos - 1 };
2243                    if n_pairs > 0 {
2244                        let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
2245                            .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
2246                            .collect();
2247                        if !self.mtp_warm_batch_metal(m, &pairs, pos) {
2248                            for (j, (h, t)) in pairs.iter().enumerate() {
2249                                let h = h.to_vec();
2250                                let _ = self.mtp_step(m, &h, *t, pos + j);
2251                            }
2252                        }
2253                    }
2254                }
2255                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2256                pos = end;
2257            }
2258            if std::env::var("CMF_PREFILL_PROF").is_ok() {
2259                eprintln!(
2260                    "metal-prefill: {} of {} tokens in {:.1} ms",
2261                    pos,
2262                    input_ids.len(),
2263                    _tp.elapsed().as_secs_f64() * 1e3
2264                );
2265            }
2266        }
2267        if task_mask.is_none()
2268            && !dyn_prefill
2269            && !graph_prefill
2270            && self.can_prefill_batched()
2271            && self.g3n.is_none()
2272            && input_ids.len() > 2
2273        {
2274            // Production prefill = the same chunked prefill-GEMM that
2275            // bench/PPL measure (roadmap §3 P0: generation used to warm
2276            // the prompt with the slower pair path — the published
2277            // prefill number didn't match real TTFT). MTP warm-up reads
2278            // each position's hidden straight from the chunk result.
2279            let chunk = prefill_chunk();
2280            let hs = self.hidden_size;
2281            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2282                let end = (pos + chunk).min(input_ids.len());
2283                let hb = self.prefill_batch(&input_ids[pos..end], pos);
2284                if let Some(m) = &mut mtp {
2285                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2286                        .ok()
2287                        .and_then(|v| v.parse().ok())
2288                        .unwrap_or(0);
2289                    for p in pos..end {
2290                        if p + 1 < input_ids.len() {
2291                            if probe >= 1 && p + 2 < input_ids.len() {
2292                                // Teacher-forced chain acceptance (see the
2293                                // tail loop's twin): the warm-up row stays,
2294                                // the chain's rows roll back.
2295                                let (d1, mut hx) = self.mtp_step_h(
2296                                    m,
2297                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
2298                                    input_ids[p + 1],
2299                                    p,
2300                                );
2301                                let mut ok = d1 == input_ids[p + 2];
2302                                Self::chain_probe_note(0, ok);
2303                                let mut d_prev = d1;
2304                                let mut extra = 0usize;
2305                                for j in 1..probe {
2306                                    if p + 2 + j >= input_ids.len() {
2307                                        break;
2308                                    }
2309                                    let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
2310                                    extra += 1;
2311                                    ok = ok && dj == input_ids[p + 2 + j];
2312                                    Self::chain_probe_note(j, ok);
2313                                    d_prev = dj;
2314                                    hx = hj;
2315                                }
2316                                m.kv.truncate_last(extra);
2317                            } else {
2318                                let _ = self.mtp_step(
2319                                    m,
2320                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
2321                                    input_ids[p + 1],
2322                                    p,
2323                                );
2324                            }
2325                        }
2326                    }
2327                }
2328                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2329                pos = end;
2330            }
2331        }
2332        let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
2333        if task_mask.is_none()
2334            && !dyn_prefill
2335            && !graph_prefill
2336            && !pair_off
2337            && self.pair_supported()
2338        {
2339            while pos + 1 < input_ids.len()
2340                && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2341            {
2342                let e1 = self.embed_single(input_ids[pos]);
2343                let e2 = self.embed_single(input_ids[pos + 1]);
2344                let (h1, h2) = self.forward_pair(&e1, &e2, pos);
2345                // Both prefill tokens are real → commit lane-2 states.
2346                self.commit_linear_scratch();
2347                if let Some(m) = &mut mtp {
2348                    let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
2349                    if pos + 2 < input_ids.len() {
2350                        let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2351                            .ok()
2352                            .and_then(|v| v.parse().ok())
2353                            .unwrap_or(0);
2354                        if probe >= 1 && pos + 3 < input_ids.len() {
2355                            // Same teacher-forced chain table as the tail
2356                            // loop below, fed from the pair path that owns
2357                            // most prefill positions.
2358                            let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
2359                            let mut ok = d1 == input_ids[pos + 3];
2360                            Self::chain_probe_note(0, ok);
2361                            let mut d_prev = d1;
2362                            let mut extra = 0usize;
2363                            for j in 1..probe {
2364                                if pos + 3 + j >= input_ids.len() {
2365                                    break;
2366                                }
2367                                let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
2368                                extra += 1;
2369                                ok = ok && dj == input_ids[pos + 3 + j];
2370                                Self::chain_probe_note(j, ok);
2371                                d_prev = dj;
2372                                hx = hj;
2373                            }
2374                            m.kv.truncate_last(extra);
2375                        } else {
2376                            let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
2377                        }
2378                    }
2379                }
2380                hidden = h2;
2381                pos += 2;
2382            }
2383        }
2384        // Batched GPU prefill for the wgpu decode graph (GDN hybrids): K prompt
2385        // positions per submit — projections/FFN as GEMMs (weight once per K),
2386        // attention/GDN looped inside — instead of one whole-graph submit per
2387        // position. Falls through to the per-position graph on any refusal.
2388        // Batched prefill is opt-in (CMF_BATCH_K>0). Default 0 = per-position
2389        // graph prefill. (Steady-state decode is provably identical either way —
2390        // token-graph submit and lm_head both unchanged — so this only trades
2391        // prefill wall.)
2392        if batch_k > 0
2393            && graph_prefill
2394            && task_mask.is_none()
2395            && !self.o1_active()
2396            && mtp.is_none()
2397            && !dyn_prefill
2398            && pos + 1 < input_ids.len()
2399        {
2400            let hs = self.hidden_size;
2401            let chunk = batch_k;
2402            while pos < input_ids.len() {
2403                let end = (pos + chunk).min(input_ids.len());
2404                let bk = end - pos;
2405                let mut hiddens = vec![0f32; bk * hs];
2406                for (j, &id) in input_ids[pos..end].iter().enumerate() {
2407                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2408                }
2409                let positions: Vec<usize> = (pos..end).collect();
2410                let t_chunk = std::time::Instant::now();
2411                let ok_b = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2412                if std::env::var("CMF_GRAPH_PROF").is_ok() {
2413                    let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2414                    eprintln!(
2415                        "batch-chunk: k={bk} ok={ok_b} {ms:.1} ms ({:.1} tok/s)",
2416                        bk as f64 / (ms / 1000.0)
2417                    );
2418                }
2419                {
2420                    use std::sync::atomic::{AtomicBool, Ordering};
2421                    static SAID: AtomicBool = AtomicBool::new(false);
2422                    if !SAID.swap(true, Ordering::Relaxed) {
2423                        if ok_b {
2424                            tracing::info!("batched prefill: ACTIVE (k={bk})");
2425                        } else {
2426                            tracing::warn!("batched prefill declined — per-position graph");
2427                        }
2428                    }
2429                }
2430                if ok_b {
2431                    hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
2432                    pos = end;
2433                } else {
2434                    break; // unsupported → per-position graph handles the rest
2435                }
2436            }
2437        }
2438        while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2439            self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
2440            hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
2441            if let Some(m) = &mut mtp {
2442                if pos + 1 < input_ids.len() {
2443                    // `CMF_MTP_CHAIN_PROBE=k`: teacher-forced acceptance of a
2444                    // CHAINED draft — iterate the head on its own hidden k
2445                    // deep and score every depth against the prompt's real
2446                    // continuation. The economics of a k-token speculative
2447                    // round stand or fall on this table.
2448                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2449                        .ok()
2450                        .and_then(|v| v.parse().ok())
2451                        .unwrap_or(0);
2452                    if probe >= 1 && pos + 2 < input_ids.len() {
2453                        let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
2454                        let mut ok = d1 == input_ids[pos + 2];
2455                        Self::chain_probe_note(0, ok);
2456                        let mut d_prev = d1;
2457                        let mut extra = 0usize;
2458                        for j in 1..probe {
2459                            if pos + 2 + j >= input_ids.len() {
2460                                break;
2461                            }
2462                            let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
2463                            extra += 1;
2464                            ok = ok && dj == input_ids[pos + 2 + j];
2465                            Self::chain_probe_note(j, ok);
2466                            d_prev = dj;
2467                            hx = hj;
2468                        }
2469                        // The chain's rows are speculation, not the prompt —
2470                        // keep only the warmup row the plain path would add.
2471                        m.kv.truncate_last(extra);
2472                    } else {
2473                        let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
2474                    }
2475                }
2476            }
2477            pos += 1;
2478        }
2479        if std::env::var("CMF_PREFILL_PROF").is_ok() {
2480            eprintln!(
2481                "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
2482                input_ids.len(),
2483                _tpf.elapsed().as_secs_f64() * 1000.0
2484            );
2485        }
2486        // Cancelled mid-prefill: the cache holds a partial prompt —
2487        // drop the reuse history and return an empty generation.
2488        if self
2489            .cancel
2490            .swap(false, std::sync::atomic::Ordering::Relaxed)
2491        {
2492            self.kv_history.clear();
2493            if let Some(m) = mtp {
2494                self.mtp = Some(m);
2495            }
2496            return Ok(GenerateResult {
2497                text: String::new(),
2498                token_ids: Vec::new(),
2499                prompt_tokens: input_ids.len(),
2500                tokens_generated: 0,
2501                finish_reason: "cancelled".to_string(),
2502                mtp_drafted: 0,
2503                mtp_accepted: 0,
2504                token_confidence: Vec::new(),
2505                traces: Vec::new(),
2506            });
2507        }
2508
2509        // Prompt absorbed → freeze the o1 layers' skeletons; from here
2510        // every decode step on those layers is O(W + m·dv + m²).
2511        self.o1_seal();
2512
2513        // Commit one token: push, check EOS, stream. Returns false = stop.
2514        macro_rules! commit {
2515            ($id:expr) => {{
2516                all_ids.push($id);
2517                generated += 1;
2518                if self.tokenizer.is_eos($id) {
2519                    finish_reason = "stop".to_string();
2520                    false
2521                } else {
2522                    let token_text = self.tokenizer.decode_token($id);
2523                    let mut go = true;
2524                    if let Some(ref mut cb) = on_token {
2525                        if !cb(&token_text) {
2526                            finish_reason = "cancelled".to_string();
2527                            go = false;
2528                        }
2529                    }
2530                    go
2531                }
2532            }};
2533        }
2534
2535        // Speculation is decided by MEASUREMENT, not by an acceptance
2536        // model. A k=4 round costs ~3.8 plain tokens on the 5090 (draft
2537        // 6.6 + verify 66.6 + commit 4.8 ms against a 20.6 ms token), so it
2538        // pays only when the head lands ~2.8 of 4 — predictable text (code,
2539        // structured output) does, free prose often does not, and the
2540        // ratio at which the two cross depends on the card and the context
2541        // depth. So: four speculative rounds timed, then eight plain
2542        // tokens timed, and the faster arm runs until a re-check 256
2543        // tokens later (context growth moves the balance). The trial
2544        // costs at most a few tokens of the slower arm per 256.
2545        let mut spec_trial = SpecTrial::Spec {
2546            t0: std::time::Instant::now(),
2547            gen0: generated,
2548            rounds: 0,
2549        };
2550        let mut spec_mon = SpecMon::default();
2551        let mut spec_watchdog_off = false;
2552        // ── Decode ──
2553        let mut next_pos = input_ids.len();
2554        'decode: while generated < max_tokens {
2555            if self
2556                .cancel
2557                .swap(false, std::sync::atomic::Ordering::Relaxed)
2558            {
2559                finish_reason = "cancelled".to_string();
2560                break 'decode;
2561            }
2562            // A rejected speculative draft already drew this position's
2563            // token from the residual distribution (graph_spec_step); it
2564            // is committed as-is — sampling again from the row's logits
2565            // would bias the stream toward the target's mode.
2566            let forced = self.spec_forced.take();
2567            let mut logits = match (forced, self.graph_logits.take()) {
2568                (Some(_), _) => Vec::new(),
2569                (None, Some(lg)) => lg,
2570                (None, None) => {
2571                    inference::rms_norm_into(
2572                        &hidden,
2573                        &self.weights.final_norm,
2574                        self.rms_eps,
2575                        self.norm_style,
2576                        &mut self.ws.n1,
2577                    );
2578                    self.lm_head_forward(&self.ws.n1)
2579                }
2580            };
2581            // CMF_LOGIT_DUMP=<path>: the first decode step's hidden + logits
2582            // as raw f32 (hidden first) — cross-backend numerics diffing.
2583            if generated
2584                == std::env::var("CMF_LOGIT_DUMP_STEP")
2585                    .ok()
2586                    .and_then(|v| v.parse().ok())
2587                    .unwrap_or(0)
2588            {
2589                if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
2590                    let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
2591                    for v in hidden.iter().chain(logits.iter()) {
2592                        bytes.extend_from_slice(&v.to_le_bytes());
2593                    }
2594                    let _ = std::fs::write(&path, &bytes);
2595                }
2596            }
2597            let t_next = match forced {
2598                Some(c) => c,
2599                None => sampler::sample_with_scratch_pool(
2600                    &logits,
2601                    &self.sampler_config,
2602                    &all_ids,
2603                    &mut self.rng,
2604                    &mut self.sampler_scratch,
2605                    self.pool.as_deref(),
2606                ),
2607            };
2608            if self.confidence_on {
2609                confidence.push(if logits.is_empty() {
2610                    0.0
2611                } else {
2612                    sampler::top1_prob_pool(
2613                        self.pool.as_deref(),
2614                        &mut self.sampler_scratch,
2615                        &logits,
2616                        t_next,
2617                        calib_temp,
2618                    )
2619                });
2620            }
2621            if !logits.is_empty() {
2622                attention::recycle_buf(&mut logits);
2623            }
2624            if trace_on {
2625                // active_skill = the overlay in force while this token was
2626                // generated; recon/switched are filled after the post-emit
2627                // routing eval below (freshest coherence for this token).
2628                let skill = router.as_ref().and_then(|r| r.active_id());
2629                traces.push(TokenTrace {
2630                    t: generated,
2631                    token_id: t_next,
2632                    confidence: confidence.last().copied().unwrap_or(0.0),
2633                    active_skill: skill,
2634                    recon: None,
2635                    switched: false,
2636                });
2637            }
2638            if !commit!(t_next) {
2639                break 'decode;
2640            }
2641            if generated >= max_tokens {
2642                break 'decode;
2643            }
2644
2645            if self.kv_cache.needs_eviction() {
2646                // Say it ONCE, loudly: past this point the model keeps
2647                // talking but has lost half its context, and on a GDN
2648                // hybrid the graph's device state goes stale on top. The
2649                // Qwen3.8 bring-up spent a day reading this cliff as
2650                // three different model bugs.
2651                static SAID: std::sync::Once = std::sync::Once::new();
2652                SAID.call_once(|| {
2653                    tracing::warn!(
2654                        "KV cache full at {} positions — evicting half; quality \
2655                         will degrade. Raise CMF_MAX_SEQ.",
2656                        self.kv_cache.max_seq_len,
2657                    );
2658                });
2659                let keep = (self.kv_cache.max_seq_len / 2).max(1);
2660                self.kv_cache.evict(keep);
2661            }
2662
2663            // Advance the speculation trial: plain-phase accounting and
2664            // the periodic re-check happen here, on every token.
2665            if graph_spec {
2666                match spec_trial {
2667                    SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
2668                        spec_mon.plain_ms =
2669                            t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
2670                        let keep = spec_mon.pays();
2671                        tracing::info!(
2672                            "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
2673                            spec_mon.tokens,
2674                            spec_mon.round_ms,
2675                            spec_mon.plain_ms,
2676                            if keep { "speculating" } else { "plain" }
2677                        );
2678                        spec_mon.fails = 0;
2679                        spec_trial = SpecTrial::Decided {
2680                            spec: keep,
2681                            recheck_at: if keep { usize::MAX } else { generated + 128 },
2682                        };
2683                    }
2684                    SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
2685                        spec_mon.n = 0;
2686                        spec_trial = SpecTrial::Spec {
2687                            t0: std::time::Instant::now(),
2688                            gen0: generated,
2689                            rounds: 0,
2690                        };
2691                    }
2692                    _ => {}
2693                }
2694                spec_watchdog_off = matches!(
2695                    spec_trial,
2696                    SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
2697                );
2698            }
2699            match &mut mtp {
2700                // ── Graph speculation: chain-draft, batch-verify on device ──
2701                #[cfg(feature = "gpu")]
2702                Some(m)
2703                    if graph_spec
2704                        && !spec_watchdog_off
2705                        && generated + 1 < max_tokens
2706                        && next_pos > 0 =>
2707                {
2708                    let t_round = std::time::Instant::now();
2709                    if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
2710                        m,
2711                        &hidden,
2712                        t_next,
2713                        next_pos,
2714                        &mut drafted,
2715                        &mut accepted,
2716                        &mut all_ids,
2717                    ) {
2718                        next_pos = n_pos;
2719                        hidden = new_h;
2720                        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
2721                            eprintln!(
2722                                "spec-round wall {:.1} ms → {} tokens",
2723                                t_round.elapsed().as_secs_f64() * 1e3,
2724                                extra.len() + 1
2725                            );
2726                        }
2727                        // One speculative round done: the monitor counts it
2728                        // (round 1 untimed — it pays the batch scratch and
2729                        // the draft mirror), and the trial advances.
2730                        spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
2731                        // the round's tokens land in `generated` below; the
2732                        // plain phase must start counting AFTER them
2733                        spec_trial = Self::spec_trial_round(
2734                            spec_trial,
2735                            &mut spec_mon,
2736                            generated + extra.len() + 1,
2737                        );
2738                        let mut stopped = false;
2739                        for &id in &extra {
2740                            if self.confidence_on {
2741                                confidence.push(0.0);
2742                            }
2743                            if !commit!(id) {
2744                                stopped = true;
2745                                break;
2746                            }
2747                        }
2748                        if stopped {
2749                            break 'decode;
2750                        }
2751                        continue 'decode;
2752                    }
2753                    // Declined (batch graph refused): plain forward below —
2754                    // and a round that produced one token for the trial's
2755                    // ledger, so a graph that keeps refusing is measured out
2756                    // like a head that keeps missing (it was spinning
2757                    // forever on a file whose batch graph declines).
2758                    // A declined round is not a cheap one-token round — it
2759                    // is a verify that does not exist for this file (a
2760                    // healed q8_2f tail measured 760 drafts, 0 accepted, 33
2761                    // against 48.8 tok/s while the monitor called the draft
2762                    // alone "paying"). Count it as the losing streak in one.
2763                    spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
2764                    spec_mon.tokens = 0.0;
2765                    spec_mon.fails = 3;
2766                    spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
2767                    hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
2768                    next_pos += 1;
2769                    continue 'decode;
2770                }
2771                // ── Speculative: draft t+2, verify in a fused pair ──
2772                Some(m) if !graph_spec && generated + 1 < max_tokens => {
2773                    let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
2774                    drafted += 1;
2775                    let emb1 = self.embed_single(t_next);
2776                    let emb2 = self.embed_single(draft);
2777                    let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
2778
2779                    inference::rms_norm_into(
2780                        &h1,
2781                        &self.weights.final_norm,
2782                        self.rms_eps,
2783                        self.norm_style,
2784                        &mut self.ws.n1,
2785                    );
2786                    let mut logits1 = self.lm_head_forward(&self.ws.n1);
2787                    let t_after = sampler::sample_with_scratch_pool(
2788                        &logits1,
2789                        &self.sampler_config,
2790                        &all_ids,
2791                        &mut self.rng,
2792                        &mut self.sampler_scratch,
2793                        self.pool.as_deref(),
2794                    );
2795                    if self.confidence_on {
2796                        confidence.push(sampler::top1_prob_pool(
2797                            self.pool.as_deref(),
2798                            &mut self.sampler_scratch,
2799                            &logits1,
2800                            t_after,
2801                            calib_temp,
2802                        ));
2803                    }
2804                    attention::recycle_buf(&mut logits1);
2805                    if trace_on {
2806                        // Speculative decode is mutually exclusive with
2807                        // dynamic routing (router is None here) — no skill.
2808                        traces.push(TokenTrace {
2809                            t: generated,
2810                            token_id: t_after,
2811                            confidence: confidence.last().copied().unwrap_or(0.0),
2812                            active_skill: None,
2813                            recon: None,
2814                            switched: false,
2815                        });
2816                    }
2817                    let stop = !commit!(t_after);
2818
2819                    if t_after == draft {
2820                        accepted += 1;
2821                        self.commit_linear_scratch();
2822                        let _ = self.mtp_step(m, &h1, t_after, next_pos);
2823                        hidden = h2;
2824                        next_pos += 2;
2825                    } else {
2826                        // The draft lane is wrong: roll its KV entry back.
2827                        for layer in &mut self.kv_cache.layers {
2828                            layer.truncate_last(1);
2829                        }
2830                        if !stop {
2831                            let _ = self.mtp_step(m, &h1, t_after, next_pos);
2832                            hidden = self.forward_layers(
2833                                &self.embed_single(t_after),
2834                                next_pos + 1,
2835                                None,
2836                            );
2837                        }
2838                        next_pos += 2;
2839                    }
2840                    if stop {
2841                        break 'decode;
2842                    }
2843                }
2844                // ── Vanilla: forward the sampled token ──
2845                _ => {
2846                    // ── DeepSeek-V4 speculative decode (CMF_DSV4_SPEC=1):
2847                    // draft five on the card, verify batched, commit the
2848                    // accepted prefix. Greedy only; a rejected token's state
2849                    // is restored and replayed, so output equals the walk. ──
2850                    #[cfg(feature = "gpu")]
2851                    if Self::dsv4_spec_on() && self.dsv4.is_some() {
2852                        static SAID: std::sync::Once = std::sync::Once::new();
2853                        SAID.call_once(|| {
2854                            eprintln!(
2855                                "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
2856                                !self.dsv4_mtp.is_empty(),
2857                                task_mask.is_none(),
2858                                router.is_none(),
2859                                !trace_on,
2860                                self.sampler_config.temperature < 1e-6,
2861                                self.sampler_config.repetition_penalty == 1.0,
2862                            );
2863                        });
2864                    }
2865                    #[cfg(feature = "gpu")]
2866                    if Self::dsv4_spec_on()
2867                        && self.dsv4.is_some()
2868                        && !self.dsv4_mtp.is_empty()
2869                        && task_mask.is_none()
2870                        && router.is_none()
2871                        && !trace_on
2872                        && self.sampler_config.temperature < 1e-6
2873                        && self.sampler_config.repetition_penalty == 1.0
2874                        && generated + 1 < max_tokens
2875                        && all_ids.len() >= 2
2876                        && generated >= dsv4_spec_retry_at
2877                    {
2878                        let tip_token = all_ids[all_ids.len() - 2];
2879                        let drafted0 = drafted;
2880                        let round = self.dsv4_spec_step(
2881                            tip_token,
2882                            t_next,
2883                            next_pos,
2884                            max_tokens.saturating_sub(generated),
2885                            &mut drafted,
2886                            &mut accepted,
2887                        );
2888                        if drafted > drafted0 {
2889                            let useful = round
2890                                .as_ref()
2891                                .is_some_and(|(extra, _)| !extra.is_empty());
2892                            if useful {
2893                                dsv4_spec_bad = 0;
2894                            } else {
2895                                dsv4_spec_bad += 1;
2896                                if dsv4_spec_bad >= 2 {
2897                                    dsv4_spec_bad = 0;
2898                                    dsv4_spec_retry_at = generated.saturating_add(32);
2899                                    tracing::info!(
2900                                        "dsv4: draft не окупился дважды — точный walk на 32 токена"
2901                                    );
2902                                }
2903                            }
2904                        }
2905                        if let Some((extra, n_pos)) = round {
2906                            next_pos = n_pos;
2907                            let mut stopped = false;
2908                            for &id in &extra {
2909                                if self.confidence_on {
2910                                    confidence.push(0.0);
2911                                }
2912                                if !commit!(id) {
2913                                    stopped = true;
2914                                    break;
2915                                }
2916                            }
2917                            if stopped {
2918                                break 'decode;
2919                            }
2920                            continue 'decode;
2921                        }
2922                    }
2923                    self.graph_want_logits = fuse_lm;
2924                    // Greedy burst (CMF_MULTISTEP, default 8, 1 = off): while
2925                    // nothing observes per-token state — pure argmax sampling,
2926                    // no router/trace/confidence/mask — decode k tokens per
2927                    // submit and commit them wholesale. The trailing normal
2928                    // forward leaves logits for the loop top, as always.
2929                    let mut t_fwd = t_next;
2930                    let pure_greedy = self.sampler_config.temperature < 1e-6
2931                        && self.sampler_config.repetition_penalty == 1.0
2932                        && self.sampler_config.suppress_tokens.is_empty();
2933                    // Off by default: at every k the burst measured at or
2934                    // below the plain path on this graph shape (k=1 loses
2935                    // the argmax dispatches vs a 1 MB readback, k>=8 loses
2936                    // inter-step drains vs the saved sync). Experimental.
2937                    let burst_k = std::env::var("CMF_MULTISTEP")
2938                        .ok()
2939                        .and_then(|v| v.parse::<usize>().ok())
2940                        .unwrap_or(0);
2941                    if pure_greedy
2942                        && burst_k >= 1
2943                        && fuse_lm
2944                        && task_mask.is_none()
2945                        && router.is_none()
2946                        && !trace_on
2947                        && !self.confidence_on
2948                    {
2949                        let mut stopped = false;
2950                        loop {
2951                            let room = max_tokens.saturating_sub(generated);
2952                            if room <= 2 {
2953                                break;
2954                            }
2955                            let k = burst_k.min(room - 1);
2956                            if k < 1 {
2957                                break;
2958                            }
2959                            let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
2960                                break;
2961                            };
2962                            next_pos += k;
2963                            for &id in &ids {
2964                                if !commit!(id) {
2965                                    stopped = true;
2966                                    break;
2967                                }
2968                            }
2969                            if stopped {
2970                                break;
2971                            }
2972                            t_fwd = *ids.last().unwrap();
2973                        }
2974                        if stopped {
2975                            break 'decode;
2976                        }
2977                    }
2978                    hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
2979                    next_pos += 1;
2980                    // Dynamic routing: the forward updated φ; ask the
2981                    // router whether to switch skills before the next token.
2982                    if let Some(r) = &mut router {
2983                        let phi = self.dyn_phi_ema.clone();
2984                        let decision = r.step(&phi, generated);
2985                        if let Some(new_active) = decision {
2986                            let _ = self.set_active_skill(new_active);
2987                        }
2988                        // Backfill this token's coherence + switch flag from
2989                        // the just-run eval (freshest measured values).
2990                        if trace_on {
2991                            if let Some(last) = traces.last_mut() {
2992                                let e = r.last_best_e();
2993                                last.recon = e.is_finite().then_some(e);
2994                                last.switched = decision.is_some();
2995                            }
2996                        }
2997                    }
2998                }
2999            }
3000        }
3001
3002        self.graph_want_logits = false;
3003        self.graph_logits = None;
3004        // Restore backbone overlay and re-attach the router for reuse.
3005        if router.is_some() {
3006            let _ = self.set_active_skill(None);
3007        }
3008        self.dyn_router = router.or(self.dyn_router.take());
3009        self.mtp = mtp.or(self.mtp.take());
3010
3011        let output_ids = &all_ids[input_ids.len()..];
3012        // Forwarded = prompt + all generated but the LAST sampled token
3013        // (emitted without being fed back). Exact only without MTP —
3014        // reuse is gated off when MTP is active.
3015        let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3016        self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3017        confidence.truncate(output_ids.len()); // guard against any overshoot
3018        traces.truncate(output_ids.len());
3019        Ok(GenerateResult {
3020            text: self.tokenizer.decode(output_ids),
3021            token_ids: output_ids.to_vec(),
3022            prompt_tokens: input_ids.len(),
3023            tokens_generated: generated,
3024            finish_reason,
3025            mtp_drafted: drafted,
3026            mtp_accepted: accepted,
3027            token_confidence: confidence,
3028            traces,
3029        })
3030    }
3031
3032    /// One MTP step: feed `(hidden_p, token_{p+1})` into the draft head,
3033    /// advance its KV cache at position `p`, return the drafted token
3034    /// for position `p+2`.
3035    fn mtp_step(
3036        &mut self,
3037        m: &mut MtpModule,
3038        hidden: &[f32],
3039        next_token: u32,
3040        position: usize,
3041    ) -> u32 {
3042        self.mtp_step_h(m, hidden, next_token, position).0
3043    }
3044
3045    /// Tally for `CMF_MTP_CHAIN_PROBE`: per depth, how often the CHAIN is
3046    /// still an exact prefix of the real continuation. Printed every 128
3047    /// depth-0 samples so a killed run still shows its table.
3048    fn chain_probe_note(depth: usize, prefix_ok: bool) {
3049        use std::sync::Mutex;
3050        static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
3051        let mut t = T.lock().unwrap();
3052        if t.len() <= depth {
3053            t.resize(depth + 1, (0, 0));
3054        }
3055        t[depth].0 += 1;
3056        t[depth].1 += prefix_ok as u64;
3057        if depth == 0 && t[0].0 % 128 == 0 {
3058            let line: Vec<String> = t
3059                .iter()
3060                .enumerate()
3061                .map(|(d, (n, k))| {
3062                    format!(
3063                        "d{}={:.0}%({n})",
3064                        d + 1,
3065                        100.0 * *k as f64 / (*n).max(1) as f64
3066                    )
3067                })
3068                .collect();
3069            eprintln!("mtp-chain: {}", line.join(" "));
3070        }
3071    }
3072
3073    /// `mtp_step` that also hands back the block's own output hidden — the
3074    /// state a CHAINED draft feeds the next step, the way a multi-token
3075    /// speculative round iterates the head on itself.
3076    /// One MTP block step from (trunk hidden, token): the head's LOGITS
3077    /// and the block's own hidden for chaining. The draft is argmax of the
3078    /// logits on the greedy path and a draw from their post-chain
3079    /// distribution on the sampling path.
3080    fn mtp_step_hl(
3081        &mut self,
3082        m: &mut MtpModule,
3083        hidden: &[f32],
3084        next_token: u32,
3085        position: usize,
3086    ) -> (Vec<f32>, Vec<f32>) {
3087        // The graph arm: the MTP block as a one-layer token graph with the
3088        // head fused — device attention over the block's own KV mirror,
3089        // one submit for block + head, hidden and logits back together.
3090        // Decided once per generation (see `mtp_graph_mode`).
3091        #[cfg(target_os = "macos")]
3092        if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
3093            if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
3094                self.mtp_graph_mode = Some(true);
3095                return r;
3096            }
3097            self.mtp_graph_mode = Some(false);
3098        }
3099        #[cfg(feature = "gpu")]
3100        if self.mtp_graph_mode != Some(false) {
3101            if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
3102                self.mtp_graph_mode = Some(true);
3103                return r;
3104            }
3105            if self.mtp_graph_mode == Some(true) {
3106                // The graph carried this generation's MTP KV and just
3107                // declined — the CPU cache is not current. A draft from
3108                // stale attention is still only a draft (verify decides),
3109                // but say so once.
3110                tracing::warn!("mtp graph declined mid-run — draft falls to the per-op path");
3111            }
3112            self.mtp_graph_mode = Some(false);
3113        }
3114        // fc concat order is [enorm(embed); hnorm(hidden)] — EMBEDDING
3115        // FIRST. Verified by the oracle (converter/mtp_oracle.py):
3116        // [emb;hid] → 45.8% acceptance, [hid;emb] → 0.00%.
3117        let e = self.embed_single(next_token);
3118        let mut cat = vec![0.0f32; 2 * self.hidden_size];
3119        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3120        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3121        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3122        let mut x = vec![0.0f32; self.hidden_size];
3123        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3124
3125        // One standard transformer block over the MTP's own cache.
3126        let lw = &m.layer;
3127        inference::rms_norm_into(
3128            &x,
3129            &lw.input_norm,
3130            self.rms_eps,
3131            self.norm_style,
3132            &mut self.ws.n1,
3133        );
3134        let attn = match &lw.attn {
3135            // MLA models carry no MTP head; this path cannot see them.
3136            AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
3137            AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
3138            AttnKind::Full {
3139                wq,
3140                wk,
3141                wv,
3142                wo,
3143                q_norm,
3144                k_norm,
3145                output_gate,
3146                softplus_gate,
3147                bias,
3148            } => {
3149                let mut cfg = self.attn_cfg(position);
3150                cfg.q_norm = q_norm.as_deref();
3151                cfg.k_norm = k_norm.as_deref();
3152                cfg.output_gate = *output_gate;
3153                cfg.softplus_gate = softplus_gate
3154                    .as_ref()
3155                    .map(|(gate, per_head)| (gate, *per_head));
3156                cfg.bias = bias
3157                    .as_ref()
3158                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3159                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3160            }
3161            AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
3162                unreachable!("MTP block is full attention")
3163            }
3164        };
3165        for (i, &a) in attn.iter().enumerate() {
3166            x[i] += a;
3167        }
3168        inference::rms_norm_into(
3169            &x,
3170            &lw.post_norm,
3171            self.rms_eps,
3172            self.norm_style,
3173            &mut self.ws.p1,
3174        );
3175        let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
3176        for (i, &f) in ffn.iter().enumerate() {
3177            x[i] += f;
3178        }
3179
3180        inference::rms_norm_into(
3181            &x,
3182            &m.final_norm,
3183            self.rms_eps,
3184            self.norm_style,
3185            &mut self.ws.n1,
3186        );
3187        let lg = self.lm_head_forward(&self.ws.n1);
3188        (lg, x)
3189    }
3190
3191    /// `mtp_step_hl` reduced to the greedy draft: argmax of the head.
3192    fn mtp_step_h(
3193        &mut self,
3194        m: &mut MtpModule,
3195        hidden: &[f32],
3196        next_token: u32,
3197        position: usize,
3198    ) -> (u32, Vec<f32>) {
3199        let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
3200        let draft = sampler::argmax(&lg);
3201        attention::recycle_buf(&mut lg);
3202        (draft, x)
3203    }
3204
3205    /// One speculative round for the trial: rounds 1..5 of a `Spec` phase
3206    /// advance it (the monitor already averaged this round); after five,
3207    /// the plain phase runs (once — a known plain rate decides at once);
3208    /// a decided speculation keeps re-checking the rule every round and
3209    /// stops after four losing rounds in a row.
3210    fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
3211        match trial {
3212            SpecTrial::Spec { t0, gen0, rounds } => {
3213                let rounds = rounds + 1;
3214                if rounds >= 5 {
3215                    if mon.plain_ms > 0.0 {
3216                        let keep = mon.pays();
3217                        mon.fails = 0;
3218                        tracing::info!(
3219                            "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3220                            mon.tokens,
3221                            mon.round_ms,
3222                            mon.plain_ms,
3223                            if keep { "speculating" } else { "plain" }
3224                        );
3225                        SpecTrial::Decided {
3226                            spec: keep,
3227                            recheck_at: if keep { usize::MAX } else { generated + 128 },
3228                        }
3229                    } else {
3230                        SpecTrial::Plain {
3231                            t0: std::time::Instant::now(),
3232                            gen0: generated,
3233                        }
3234                    }
3235                } else {
3236                    SpecTrial::Spec { t0, gen0, rounds }
3237                }
3238            }
3239            SpecTrial::Decided { spec: true, .. } => {
3240                if mon.pays() {
3241                    mon.fails = 0;
3242                    trial
3243                } else {
3244                    mon.fails += 1;
3245                    if mon.fails >= 4 {
3246                        tracing::info!(
3247                            "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
3248                            mon.tokens,
3249                            mon.round_ms,
3250                            mon.plain_ms
3251                        );
3252                        SpecTrial::Decided {
3253                            spec: false,
3254                            recheck_at: generated + 128,
3255                        }
3256                    } else {
3257                        trial
3258                    }
3259                }
3260            }
3261            other => other,
3262        }
3263    }
3264
3265    /// The MTP block's device-mirror id: the trunk's id with a high bit,
3266    /// so the (kv_id, layer) mirror keys never collide.
3267    fn mtp_kv_id(&self) -> u64 {
3268        self.graph_kv_id | (1u64 << 40)
3269    }
3270
3271    /// The MTP block's mirror layer index: 0 — its own kv_id keeps it
3272    /// apart from the trunk, and the BATCH graph (the warm-up path) keys
3273    /// its mirrors at layer 0 with no base of its own, so the draft's
3274    /// token graph must key the same slot.
3275    const MTP_LAYER_BASE: usize = 0;
3276
3277    /// The block's input from (trunk hidden, token): eh_proj · [enorm(e);
3278    /// hnorm(h)] — the same arithmetic the per-op path starts with.
3279    fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
3280        let e = self.embed_single(next_token);
3281        let mut cat = vec![0.0f32; 2 * self.hidden_size];
3282        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3283        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3284        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3285        let mut x = vec![0.0f32; self.hidden_size];
3286        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3287        x
3288    }
3289
3290    /// Is the MTP block graphable at all (device up, full attention
3291    /// without softplus, dense FFN)? The plan itself is built per call.
3292    #[cfg(feature = "gpu")]
3293    fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
3294        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
3295            return false;
3296        }
3297        if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
3298            || !crate::gpu::enabled_here()
3299            || self.attn_softcap > 0.0
3300            || self.attention_heads_per_layer.is_some()
3301        {
3302            return false;
3303        }
3304        matches!(
3305            &m.layer.attn,
3306            AttnKind::Full {
3307                softplus_gate: None,
3308                ..
3309            }
3310        ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
3311    }
3312
3313    /// One MTP block step on the wgpu token graph: block + fused head in
3314    /// one submit, the block hidden and the logits read back together.
3315    /// None = the graph cannot take this block (softplus gate, non-dense
3316    /// FFN, unquantized head, no device) — the caller keeps the per-op
3317    /// path for the whole generation.
3318    #[cfg(feature = "gpu")]
3319    fn mtp_step_graph(
3320        &mut self,
3321        m: &mut MtpModule,
3322        hidden: &[f32],
3323        next_token: u32,
3324        position: usize,
3325    ) -> Option<(Vec<f32>, Vec<f32>)> {
3326        if !self.mtp_graph_ok(m) {
3327            return None;
3328        }
3329        let lw = &m.layer;
3330        let AttnKind::Full {
3331            wq,
3332            wk,
3333            wv,
3334            wo,
3335            q_norm,
3336            k_norm,
3337            output_gate,
3338            softplus_gate,
3339            bias,
3340        } = &lw.attn
3341        else {
3342            return None;
3343        };
3344        if softplus_gate.is_some() {
3345            return None;
3346        }
3347        let FfnKind::Dense(d) = &lw.ffn else {
3348            return None;
3349        };
3350        if !d.segs.is_empty() {
3351            return None; // tube layers run on the segmented path
3352        }
3353        // The block's input first: it borrows `self` mutably (embed scratch,
3354        // pool), the plan below borrows the weights immutably.
3355        let mut x = self.mtp_block_input(m, hidden, next_token);
3356        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3357            let (_, i, kind, rs) = t.graph_weight()?;
3358            Some(crate::gpu::GraphW {
3359                idx: i,
3360                kind,
3361                row_scale: rs,
3362                data: &[],
3363            })
3364        }
3365        let (model, _, _, _) = wq.graph_weight()?;
3366        let model = model.clone();
3367        let (lm_gw, lm_rows) = {
3368            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3369            (
3370                crate::gpu::GraphW {
3371                    idx: i,
3372                    kind,
3373                    row_scale: rs,
3374                    data: &[],
3375                },
3376                self.weights.lm_head.rows(),
3377            )
3378        };
3379        let layer = crate::gpu::GraphLayer {
3380            input_norm: &lw.input_norm,
3381            attn: crate::gpu::GraphAttn::Full {
3382                wq: gw(wq)?,
3383                wk: gw(wk)?,
3384                wv: gw(wv)?,
3385                wo: gw(wo)?,
3386                q_norm: q_norm.as_deref(),
3387                k_norm: k_norm.as_deref(),
3388                bias: bias
3389                    .as_ref()
3390                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3391                output_gate: *output_gate,
3392                cpu_k: m.kv.k_heads(),
3393                cpu_v: m.kv.v_heads(),
3394            },
3395            post_norm: &lw.post_norm,
3396            ffn: crate::gpu::GraphFfn::Dense {
3397                gate: gw(&d.gate_proj)?,
3398                up: gw(&d.up_proj)?,
3399                down: gw(&d.down_proj)?,
3400            },
3401        };
3402        let nh = self.num_heads;
3403        let (nkv, hd, rd) = self.layer_geom(0);
3404        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3405        let mut logits = Vec::new();
3406        let ok = crate::gpu::forward_token_graph(
3407            &model,
3408            self.mtp_kv_id(),
3409            std::slice::from_ref(&layer),
3410            &[None],
3411            self.o1_epoch,
3412            &self.inv_freq,
3413            &mut x,
3414            nh,
3415            nkv,
3416            hd,
3417            rd,
3418            self.hidden_size,
3419            self.intermediate_size,
3420            position,
3421            self.kv_cache.max_seq_len,
3422            gemma,
3423            self.rms_eps as f32,
3424            Some((&lm_gw, lm_rows)),
3425            &m.final_norm,
3426            &mut logits,
3427            &[],
3428            1,
3429            None,
3430            None,
3431            None,
3432            Self::MTP_LAYER_BASE,
3433            true,
3434        );
3435        if !ok {
3436            return None;
3437        }
3438        logits.resize(self.vocab_size, 0.0);
3439        Some((logits, x))
3440    }
3441
3442    /// The warm-ups of one speculative round on the device: every accepted
3443    /// (hidden, token) pair as ONE batched graph run over the MTP block
3444    /// (no head) — its kv_append lands the pairs in the block's mirror.
3445    /// `pairs` are consecutive positions from `first_pos`. False = the
3446    /// batch graph declined; the caller warms one by one on the token
3447    /// graph (prefix mode) instead.
3448    #[cfg(feature = "gpu")]
3449    fn mtp_warm_graph(
3450        &mut self,
3451        m: &mut MtpModule,
3452        pairs: &[(&[f32], u32)],
3453        first_pos: usize,
3454    ) -> bool {
3455        if pairs.is_empty() || !self.mtp_graph_ok(m) {
3456            return pairs.is_empty();
3457        }
3458        let hs = self.hidden_size;
3459        // Block inputs for every pair (eh_proj on the per-op path, one
3460        // matvec each — the plan's own prologue).
3461        let mut hiddens = Vec::with_capacity(pairs.len() * hs);
3462        for (h, t) in pairs {
3463            hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
3464        }
3465        let lw = &m.layer;
3466        let AttnKind::Full {
3467            wq,
3468            wk,
3469            wv,
3470            wo,
3471            q_norm,
3472            k_norm,
3473            output_gate,
3474            bias,
3475            ..
3476        } = &lw.attn
3477        else {
3478            return false;
3479        };
3480        let FfnKind::Dense(d) = &lw.ffn else {
3481            return false;
3482        };
3483        if !d.segs.is_empty() {
3484            return false; // tube layers run on the segmented path
3485        }
3486        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3487            let (_, i, kind, rs) = t.graph_weight()?;
3488            Some(crate::gpu::GraphW {
3489                idx: i,
3490                kind,
3491                row_scale: rs,
3492                data: &[],
3493            })
3494        }
3495        let Some((model, _, _, _)) = wq.graph_weight() else {
3496            return false;
3497        };
3498        let model = model.clone();
3499        let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
3500            gw(wq),
3501            gw(wk),
3502            gw(wv),
3503            gw(wo),
3504            gw(&d.gate_proj),
3505            gw(&d.up_proj),
3506            gw(&d.down_proj),
3507        ) else {
3508            return false;
3509        };
3510        let layer = crate::gpu::GraphLayer {
3511            input_norm: &lw.input_norm,
3512            attn: crate::gpu::GraphAttn::Full {
3513                wq: gwq,
3514                wk: gwk,
3515                wv: gwv,
3516                wo: gwo,
3517                q_norm: q_norm.as_deref(),
3518                k_norm: k_norm.as_deref(),
3519                bias: bias
3520                    .as_ref()
3521                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3522                output_gate: *output_gate,
3523                cpu_k: m.kv.k_heads(),
3524                cpu_v: m.kv.v_heads(),
3525            },
3526            post_norm: &lw.post_norm,
3527            ffn: crate::gpu::GraphFfn::Dense {
3528                gate: gg,
3529                up: gu,
3530                down: gd,
3531            },
3532        };
3533        let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
3534        let nh = self.num_heads;
3535        let (nkv, hd, rd) = self.layer_geom(0);
3536        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3537        crate::gpu::forward_batch_graph(
3538            &model,
3539            self.mtp_kv_id(),
3540            std::slice::from_ref(&layer),
3541            &self.inv_freq,
3542            &mut hiddens,
3543            nh,
3544            nkv,
3545            hd,
3546            rd,
3547            hs,
3548            self.intermediate_size,
3549            &positions,
3550            self.kv_cache.max_seq_len,
3551            gemma,
3552            self.rms_eps as f32,
3553            pairs.len(),
3554            None,
3555        )
3556    }
3557
3558    /// The MTP block alone — advance its KV with a (hidden, token) pair the
3559    /// verify just proved, without paying the head. What keeps the draft's
3560    /// attention context warm between speculative rounds.
3561    fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
3562        let e = self.embed_single(next_token);
3563        let mut cat = vec![0.0f32; 2 * self.hidden_size];
3564        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3565        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3566        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3567        let mut x = vec![0.0f32; self.hidden_size];
3568        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3569        inference::rms_norm_into(
3570            &x,
3571            &m.layer.input_norm,
3572            self.rms_eps,
3573            self.norm_style,
3574            &mut self.ws.n1,
3575        );
3576        let attn = match &m.layer.attn {
3577            AttnKind::Full {
3578                wq,
3579                wk,
3580                wv,
3581                wo,
3582                q_norm,
3583                k_norm,
3584                output_gate,
3585                softplus_gate,
3586                bias,
3587            } => {
3588                let mut cfg = self.attn_cfg(position);
3589                cfg.q_norm = q_norm.as_deref();
3590                cfg.k_norm = k_norm.as_deref();
3591                cfg.output_gate = *output_gate;
3592                cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
3593                cfg.bias = bias
3594                    .as_ref()
3595                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3596                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3597            }
3598            _ => return,
3599        };
3600        let _ = attn;
3601    }
3602
3603    /// Speculative decode ON the wgpu whole-token graph: draft k with the
3604    /// MTP head, verify all of them plus the tip in ONE batched graph
3605    /// submit whose tail folds the head, commit the accepted prefix and
3606    /// roll the GDN state back to the last real position. Greedy only —
3607    /// output equals the plain graph's token for token, the way the DSV4
3608    /// verify equals the walk.
3609    #[cfg(feature = "gpu")]
3610    #[allow(clippy::too_many_arguments)]
3611    fn graph_spec_step(
3612        &mut self,
3613        m: &mut MtpModule,
3614        hidden: &[f32],
3615        t_next: u32,
3616        next_pos: usize,
3617        drafted: &mut usize,
3618        accepted: &mut usize,
3619        // The committed stream (prompt + generated so far, `t_next`
3620        // included): the sampler chain's penalties read it, and the
3621        // sampling arm extends it with the drafts position by position.
3622        all_ids: &mut Vec<u32>,
3623    ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
3624        // 3 is the measured optimum on Qwen3.6-27B / RTX 5090 (medians
3625        // of three, greedy): 51.1 tok/s against a plain 49.4, where k=2
3626        // gives 46.1, k=4 50.0, k=5 47.4, k=6 45.2. Acceptance is 89-91%
3627        // throughout — what turns the curve over is the verify, which
3628        // costs ~7.4 ms per extra position, and the draft ~3 ms a step.
3629        // 4 since the draft moved onto the graph (Qwen3.8-27B / 5090:
3630        // k=3 51.2, k=4 51.8 with the per-op draft; the graph draft
3631        // halves the draft cost, so the extra draft is cheaper still).
3632        // 5 with the int8 verify (the default: measured 76.5 against
3633        // k=4's 72-74 and k=6's 74 on the 5090), 4 with the f32 one.
3634        #[cfg(target_os = "macos")]
3635        let metal_native = crate::gpu::q1_force();
3636        #[cfg(not(target_os = "macos"))]
3637        let metal_native = false;
3638        #[cfg(feature = "gpu")]
3639        let k_default = if metal_native {
3640            // the Metal verify's GEMM tile is 8 rows wide and flat in b:
3641            // seven drafts + the tip fill it for free
3642            7
3643        } else if crate::gpu_wgpu::verify_i8_on() {
3644            5
3645        } else {
3646            4
3647        };
3648        #[cfg(not(feature = "gpu"))]
3649        let k_default = 4;
3650        let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
3651            .ok()
3652            .and_then(|v| v.parse().ok())
3653            .filter(|&v| (1..=8).contains(&v))
3654            .unwrap_or(k_default);
3655        if next_pos == 0 {
3656            return None;
3657        }
3658        let t_round = std::time::Instant::now();
3659        // Submissions per phase — and they say where the round's money is.
3660        // Qwen3.6-27B on an RTX 5090, k=3:
3661        //
3662        //   draft   9.3 ms / 12 submissions   (four per MTP step)
3663        //   verify 52.8 ms /  1               (the batched graph)
3664        //   commit  5.4 ms /  6               (two per warm)
3665        //
3666        // The verify is already one submit. The draft's own work is 834 MB
3667        // a step — 0.8 ms at this card's measured 1056 GB/s — against 3.1
3668        // ms measured, so ~0.58 ms of every step is round trip, not
3669        // arithmetic, and the same holds for the warms. Eighteen round
3670        // trips a round at roughly half a millisecond each is ~11 ms of a
3671        // 68 ms round: fusing the MTP block into ONE submit the way the
3672        // trunk already is projects to ~64 tok/s against today's 50.9.
3673        // That is the largest measured item left on this path.
3674        let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
3675        let sub0 = subs();
3676        // Greedy without penalties verifies by argmax equality (bit-exact
3677        // against the plain path). Anything else is speculative SAMPLING:
3678        // each draft is a DRAW from the MTP head's post-chain distribution
3679        // q_j, kept for the accept test; the verify's rows give p_j.
3680        let cfg = self.sampler_config.clone();
3681        let penalized = !(cfg.repetition_penalty == 1.0
3682            && cfg.presence_penalty == 0.0
3683            && cfg.suppress_tokens.is_empty());
3684        // Three verify regimes: plain greedy (argmax of the raw rows),
3685        // greedy WITH penalties (argmax of the penalized rows — a single
3686        // pass each, no distributions), and sampling (draw / accept /
3687        // correct on post-chain distributions).
3688        let greedy_pen = cfg.temperature < 1e-6 && penalized;
3689        let sampling = cfg.temperature >= 1e-6;
3690        // Sampling with a top-k goes through the SPARSE chain: the dense
3691        // one builds nine 248k-float distributions a round (four drafts,
3692        // five verify rows) and measured 19-22 tok/s against a plain 40 —
3693        // the host, not the card. Sparse, the same nine cost tens of
3694        // microseconds each.
3695        let sparse = sampling && sampler::sparse_ok(&cfg);
3696        let base_len = all_ids.len();
3697        if sampling && !sparse && self.spec_q.len() < k_spec {
3698            self.spec_q.resize_with(k_spec, Vec::new);
3699        }
3700        if sparse && self.spec_qs.len() < k_spec {
3701            self.spec_qs.resize_with(k_spec, Vec::new);
3702        }
3703        // Draft the chain: first from the trunk's tip hidden, then the head
3704        // iterating on itself. Rows land in the MTP KV; the chain rows past
3705        // the first are speculation over speculative state and roll back
3706        // below, replaced by verified pairs.
3707        let mut drafts = Vec::with_capacity(k_spec);
3708        let mut hx = hidden.to_vec();
3709        // CMF_SPEC_DBG=1: draft 0 through BOTH MTP arms (graph and per-op)
3710        // from the same inputs — are the arms the difference, or the inputs?
3711        let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
3712        for j in 0..k_spec {
3713            let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
3714            let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
3715            if spec_dbg {
3716                let saved = self.mtp_graph_mode;
3717                self.mtp_graph_mode = Some(false);
3718                let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
3719                self.mtp_graph_mode = saved;
3720                m.kv.truncate_last(1);
3721                dbg_ref = Some(r);
3722            }
3723            let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
3724            if let Some((lg_cpu, h_cpu)) = dbg_ref {
3725                let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
3726                let dl = lg.iter().zip(&lg_cpu).fold(0f32, |m, (a, b)| m.max((a - b).abs()));
3727                let dh = hj.iter().zip(&h_cpu).fold(0f32, |m, (a, b)| m.max((a - b).abs()));
3728                eprintln!(
3729                    "spec-dbg j={j} pos {} tok_in {tok_in}: per-op draft {} graph draft {} | max|dlogit| {dl:.3} | |h_cpu| {:.2} |h_graph| {:.2} max|dh| {dh:.3} | kv rows {}",
3730                    next_pos - 1 + j,
3731                    sampler::argmax(&lg_cpu),
3732                    sampler::argmax(&lg),
3733                    n(&h_cpu),
3734                    n(&hj),
3735                    m.kv.seq_len
3736                );
3737            }
3738            let dj = if sparse {
3739                let mut q = std::mem::take(&mut self.spec_qs[j]);
3740                let ok = sampler::sparse_distribution_into(
3741                    &lg,
3742                    &cfg,
3743                    all_ids,
3744                    &mut self.sampler_scratch,
3745                    self.pool.as_deref(),
3746                    &mut q,
3747                );
3748                let d = if ok {
3749                    sampler::draw_sparse(&q, &mut self.rng)
3750                } else {
3751                    // everything filtered: the dense chain's greedy fallback
3752                    let t = sampler::argmax(&lg);
3753                    q.clear();
3754                    q.push((t, 1.0));
3755                    t
3756                };
3757                self.spec_qs[j] = q;
3758                all_ids.push(d);
3759                d
3760            } else if sampling {
3761                let mut q = std::mem::take(&mut self.spec_q[j]);
3762                sampler::distribution_into(
3763                    &lg,
3764                    &cfg,
3765                    all_ids,
3766                    &mut self.sampler_scratch,
3767                    self.pool.as_deref(),
3768                    &mut q,
3769                );
3770                let d = sampler::draw(&q, &mut self.rng);
3771                self.spec_q[j] = q;
3772                all_ids.push(d); // the next draft's penalties see this one
3773                d
3774            } else if greedy_pen {
3775                let d = sampler::argmax_penalized(
3776                    &lg,
3777                    &cfg,
3778                    all_ids,
3779                    &mut self.sampler_scratch,
3780                    self.pool.as_deref(),
3781                );
3782                all_ids.push(d);
3783                d
3784            } else {
3785                sampler::argmax(&lg)
3786            };
3787            attention::recycle_buf(&mut lg);
3788            drafts.push(dj);
3789            hx = hj;
3790        }
3791        all_ids.truncate(base_len);
3792        *drafted += k_spec;
3793        let t_draft = t_round.elapsed();
3794        let sub_draft = subs();
3795        // Verify batch: [t_next, d1 .. d_{k-1}] at next_pos.. — every row's
3796        // logits come back from the graph's own head.
3797        let b = k_spec + 1;
3798        let mut hiddens = vec![0.0f32; b * self.hidden_size];
3799        for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
3800            let e = self.embed_single(t);
3801            hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
3802        }
3803        let positions: Vec<usize> = (next_pos..next_pos + b).collect();
3804        let (lm_gw, lm_rows) = {
3805            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3806            (
3807                crate::gpu::GraphW {
3808                    idx: i,
3809                    kind,
3810                    row_scale: rs,
3811                    data: &[],
3812                },
3813                self.weights.lm_head.rows(),
3814            )
3815        };
3816        let mut logits = Vec::new();
3817        let final_norm = self.weights.final_norm.clone();
3818        #[cfg(target_os = "macos")]
3819        let ok = if metal_native {
3820            let lm = self.weights.lm_head.q1_parts()?;
3821            self.try_batch_graph_metal(&mut hiddens, &positions, b, Some((lm, &final_norm, &mut logits)))
3822        } else {
3823            self.try_batch_graph_wgpu(
3824                &mut hiddens,
3825                &positions,
3826                b,
3827                Some(crate::gpu::SpecTail {
3828                    lm: lm_gw,
3829                    lm_rows,
3830                    final_norm: &final_norm,
3831                    logits_out: &mut logits,
3832                }),
3833            )
3834        };
3835        #[cfg(not(target_os = "macos"))]
3836        let ok = self.try_batch_graph_wgpu(
3837            &mut hiddens,
3838            &positions,
3839            b,
3840            Some(crate::gpu::SpecTail {
3841                lm: lm_gw,
3842                lm_rows,
3843                final_norm: &final_norm,
3844                logits_out: &mut logits,
3845            }),
3846        );
3847        if !ok {
3848            // Roll the draft rows back out of the MTP cache and decline —
3849            // the caller runs the plain path, nothing has changed.
3850            m.kv.truncate_last(k_spec);
3851            return None;
3852        }
3853        // `CMF_METAL_VERIFY_CHECK=1`: run the same b tokens through the
3854        // plain per-token path and compare each row's argmax + logits with
3855        // the verify's — the bring-up oracle for the batched graph. The
3856        // plain forwards mutate the CPU state; it is snapshotted and put
3857        // back, and the K/V mirrors re-pointed, before the round goes on.
3858        #[cfg(target_os = "macos")]
3859        if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
3860            let snap: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
3861            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
3862            let toks: Vec<u32> = std::iter::once(t_next).chain(drafts.iter().copied()).collect();
3863            let want_save = self.graph_want_logits;
3864            self.graph_want_logits = false;
3865            for (i, &t) in toks.iter().enumerate() {
3866                let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
3867                let _ = self.graph_logits.take();
3868                // CMF_SPEC_PLAIN_HIDDEN=1: the next round drafts from the
3869                // plain path's hidden instead of the verify's (an experiment
3870                // on the chain's sensitivity to the half-GEMM noise)
3871                if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
3872                    hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
3873                }
3874                let ref_lg = self.logits_from_hidden(&hi);
3875                let row = &logits[i * lm_rows..(i + 1) * lm_rows];
3876                let ra = sampler::argmax(&ref_lg);
3877                let va = sampler::argmax(row);
3878                let mut md = 0f32;
3879                let mut rms = 0f64;
3880                for j in 0..lm_rows.min(ref_lg.len()) {
3881                    let d = (ref_lg[j] - row[j]).abs();
3882                    md = md.max(d);
3883                    rms += (d as f64) * (d as f64);
3884                }
3885                let mut hd = 0f32;
3886                for j in 0..self.hidden_size {
3887                    hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
3888                }
3889                eprintln!(
3890                    "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
3891                    next_pos + i,
3892                    if ra == va { "OK" } else { "MISMATCH" },
3893                    (rms / lm_rows as f64).sqrt()
3894                );
3895            }
3896            self.graph_want_logits = want_save;
3897            // restore IN PLACE: the pending verify graph wraps these very
3898            // allocations (zero-copy) — replacing the Vec would strand it
3899            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
3900                if l.linear_state.len() == st.len() {
3901                    l.linear_state.copy_from_slice(&st);
3902                } else {
3903                    l.linear_state = st;
3904                }
3905            }
3906            for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
3907                let extra = l.seq_len.saturating_sub(n0);
3908                if extra > 0 {
3909                    l.truncate_last(extra);
3910                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
3911                }
3912            }
3913        }
3914        let t_verify = t_round.elapsed();
3915        let sub_verify = subs();
3916        // Acceptance. Greedy: row i's argmax is the trunk's token after
3917        // input i. Sampling: accept draft i with min(1, p_i/q_i), and on
3918        // the first rejection draw the correction from max(0, p_i − q_i)
3919        // — that token is committed by the loop top as-is (spec_forced).
3920        let mut a = 0usize;
3921        let mut forced: Option<u32> = None;
3922        let ids: Vec<u32> = if sparse {
3923            let mut p = std::mem::take(&mut self.spec_ps);
3924            let mut res = std::mem::take(&mut self.spec_ress);
3925            while a < k_spec {
3926                let ok = sampler::sparse_distribution_into(
3927                    &logits[a * lm_rows..(a + 1) * lm_rows],
3928                    &cfg,
3929                    all_ids,
3930                    &mut self.sampler_scratch,
3931                    self.pool.as_deref(),
3932                    &mut p,
3933                );
3934                if !ok {
3935                    let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
3936                    p.clear();
3937                    p.push((t, 1.0));
3938                }
3939                match sampler::spec_accept_or_correct_sparse(
3940                    &p,
3941                    &self.spec_qs[a],
3942                    drafts[a],
3943                    &mut self.rng,
3944                    &mut res,
3945                ) {
3946                    None => {
3947                        all_ids.push(drafts[a]);
3948                        a += 1;
3949                    }
3950                    Some(c) => {
3951                        forced = Some(c);
3952                        break;
3953                    }
3954                }
3955            }
3956            all_ids.truncate(base_len);
3957            self.spec_ps = p;
3958            self.spec_ress = res;
3959            drafts.clone()
3960        } else if sampling {
3961            let mut p = std::mem::take(&mut self.spec_p);
3962            let mut res = std::mem::take(&mut self.spec_res);
3963            while a < k_spec {
3964                sampler::distribution_into(
3965                    &logits[a * lm_rows..(a + 1) * lm_rows],
3966                    &cfg,
3967                    all_ids,
3968                    &mut self.sampler_scratch,
3969                    self.pool.as_deref(),
3970                    &mut p,
3971                );
3972                match sampler::spec_accept_or_correct(
3973                    &p,
3974                    &self.spec_q[a],
3975                    drafts[a],
3976                    &mut self.rng,
3977                    &mut res,
3978                    self.pool.as_deref(),
3979                ) {
3980                    None => {
3981                        all_ids.push(drafts[a]);
3982                        a += 1;
3983                    }
3984                    Some(c) => {
3985                        forced = Some(c);
3986                        break;
3987                    }
3988                }
3989            }
3990            all_ids.truncate(base_len);
3991            self.spec_p = p;
3992            self.spec_res = res;
3993            // the accepted drafts ARE the verified tokens after inputs 0..a
3994            drafts.clone()
3995        } else if greedy_pen {
3996            // Row i's penalized argmax, penalties over the stream that
3997            // includes the accepted drafts before it — the plain loop's
3998            // exact arithmetic, one pass per row, no working copy.
3999            let mut ids: Vec<u32> = Vec::with_capacity(b);
4000            for i in 0..b {
4001                let t = sampler::argmax_penalized(
4002                    &logits[i * lm_rows..(i + 1) * lm_rows],
4003                    &cfg,
4004                    all_ids,
4005                    &mut self.sampler_scratch,
4006                    self.pool.as_deref(),
4007                );
4008                ids.push(t);
4009                if i < k_spec && t == drafts[i] {
4010                    all_ids.push(t);
4011                } else {
4012                    break;
4013                }
4014            }
4015            all_ids.truncate(base_len);
4016            while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
4017                a += 1;
4018            }
4019            // rows past the first mismatch were never scored; the loop
4020            // top re-samples the last verified row itself.
4021            ids
4022        } else {
4023            let ids: Vec<u32> = (0..b)
4024                .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
4025                .collect();
4026            while a < k_spec && ids[a] == drafts[a] {
4027                a += 1;
4028            }
4029            ids
4030        };
4031        if spec_dbg {
4032            eprintln!("spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}", drafts, ids);
4033        }
4034        // CMF_METAL_VERIFY_CHECK=2: the commit oracle — plain-forward the
4035        // a+1 accepted tokens from a snapshot, then diff the replayed GDN
4036        // states and the appended K/V rows against that.
4037        #[cfg(target_os = "macos")]
4038        let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
4039            && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
4040        {
4041            let snap: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
4042            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
4043            let toks: Vec<u32> = std::iter::once(t_next).chain(drafts.iter().copied()).collect();
4044            let want_save = self.graph_want_logits;
4045            self.graph_want_logits = false;
4046            for (i, &t) in toks.iter().take(a + 1).enumerate() {
4047                let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
4048                let _ = self.graph_logits.take();
4049            }
4050            self.graph_want_logits = want_save;
4051            let plain_states: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
4052            let (nkv, hd) = (self.num_kv_heads, self.head_dim);
4053            let mut rows = Vec::new();
4054            for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens.iter()).enumerate() {
4055                let extra = l.seq_len.saturating_sub(*n0);
4056                if extra > 0 {
4057                    let mut kk = Vec::new();
4058                    let mut vv = Vec::new();
4059                    for g in 0..nkv {
4060                        kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
4061                        vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
4062                    }
4063                    rows.push((li, kk, vv));
4064                    l.truncate_last(extra);
4065                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
4066                }
4067            }
4068            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
4069                if l.linear_state.len() == st.len() {
4070                    l.linear_state.copy_from_slice(&st);
4071                } else {
4072                    l.linear_state = st;
4073                }
4074            }
4075            Some((plain_states, rows))
4076        } else {
4077            None
4078        };
4079        // a fully-accepted round needs no restore: every input was real.
4080        #[cfg(target_os = "macos")]
4081        if metal_native {
4082            // the Metal verify never wrote its states: the commit replays the
4083            // accepted prefix into the CPU owners and appends the K/V rows
4084            self.metal_verify_commit(a);
4085            if let Some((plain_states, rows)) = commit_ref {
4086                crate::gpu_metal::queue_fence();
4087                let (nkv, hd) = (self.num_kv_heads, self.head_dim);
4088                let mut worst_s = 0f32;
4089                let mut worst_li = 0usize;
4090                for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
4091                    if l.linear_state.len() != ps.len() || ps.is_empty() {
4092                        continue;
4093                    }
4094                    let d = l.linear_state.iter().zip(ps).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4095                    let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
4096                    let rel = d / n.max(1e-6);
4097                    if rel > worst_s {
4098                        worst_s = rel;
4099                        worst_li = li;
4100                    }
4101                }
4102                let mut worst_k = 0f32;
4103                for (li, kk, vv) in &rows {
4104                    let l = &self.kv_cache.layers[*li];
4105                    let n0 = l.seq_len - (kk.len() / (nkv * hd));
4106                    let mut ck = Vec::new();
4107                    let mut cv = Vec::new();
4108                    for g in 0..nkv {
4109                        ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
4110                        cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
4111                    }
4112                    if ck.len() == kk.len() {
4113                        let dk = ck.iter().zip(kk).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4114                        let dv = cv.iter().zip(vv).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4115                        worst_k = worst_k.max(dk).max(dv);
4116                    } else {
4117                        eprintln!("commit-check L{li}: kv row count mismatch {} vs {}", ck.len(), kk.len());
4118                    }
4119                }
4120                eprintln!(
4121                    "commit-check a={a}: worst GDN state rel-max diff {worst_s:.2e} (L{worst_li}) | worst K/V row abs diff {worst_k:.4}"
4122                );
4123            }
4124        } else if a + 1 < b {
4125            crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
4126        }
4127        #[cfg(not(target_os = "macos"))]
4128        if a + 1 < b {
4129            crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
4130        }
4131        *accepted += a;
4132        // MTP cache: keep the first draft row (its inputs were real), drop
4133        // the chain's, then append the verified pairs the round produced.
4134        // Each of those is a whole MTP block on the per-op path and they
4135        // cost 5.8 ms of a 69 ms round at k=3 — a third of what the
4136        // round's own draft costs. PRICED, and they earn it: skipping
4137        // them (`CMF_SPEC_WARM=0`) drops acceptance from 89% to 81% at
4138        // k=3 and 85% to 74% at k=4, and the tok/s goes nowhere at k=3
4139        // (50.3 against 50.5) and backwards at k=4 (48.1 against 50.1).
4140        // The knob stays so the next person can re-price it after the
4141        // warms are batched instead of assuming either way.
4142        m.kv.truncate_last(k_spec.saturating_sub(1));
4143        #[cfg(target_os = "macos")]
4144        if metal_native && self.mtp_graph_mode == Some(true) {
4145            // the mirror rows below the cut are the CPU rows: re-point,
4146            // no re-upload
4147            crate::gpu_metal::kv_mirror_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, m.kv.seq_len);
4148        }
4149        let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
4150        if !warm_off && a > 0 {
4151            // Graph arm: all accepted pairs in ONE batched run over the
4152            // MTP block; the token graph one by one if the batch declines.
4153            let mut warmed = false;
4154            #[cfg(target_os = "macos")]
4155            if metal_native && self.mtp_graph_mode == Some(true) {
4156                // all accepted pairs in ONE b-row graph run over the MTP
4157                // block (its input projection folded in); one by one on
4158                // the token graph if that declines
4159                let pairs: Vec<(&[f32], u32)> = (0..a)
4160                    .map(|j| (&hiddens[j * self.hidden_size..(j + 1) * self.hidden_size], ids[j]))
4161                    .collect();
4162                warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
4163                if !warmed {
4164                    warmed = true;
4165                    for j in 0..a {
4166                        let row = hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
4167                        if self.mtp_step_metal(m, &row, ids[j], next_pos + j, false).is_none() {
4168                            warmed = false;
4169                            break;
4170                        }
4171                    }
4172                }
4173            }
4174            if !warmed && self.mtp_graph_mode == Some(true) && !metal_native {
4175                let rows: Vec<Vec<f32>> = (0..a)
4176                    .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
4177                    .collect();
4178                let pairs: Vec<(&[f32], u32)> = rows
4179                    .iter()
4180                    .zip(ids.iter())
4181                    .map(|(r, &t)| (r.as_slice(), t))
4182                    .collect();
4183                warmed = self.mtp_warm_graph(m, &pairs, next_pos);
4184                if !warmed {
4185                    // Prefix-mode token graph per pair (kv_append inside).
4186                    warmed = true;
4187                    for j in 0..a {
4188                        if self
4189                            .mtp_step_graph(m, &rows[j], ids[j], next_pos + j)
4190                            .is_none()
4191                        {
4192                            warmed = false;
4193                            break;
4194                        }
4195                    }
4196                }
4197            }
4198            if !warmed {
4199                for j in 0..a {
4200                    let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
4201                    let row = row.to_vec();
4202                    self.mtp_warm(m, &row, ids[j], next_pos + j);
4203                }
4204            }
4205        }
4206        // The sampler's contract: logits of the LAST verified position —
4207        // unless a rejected draft already drew the correction, in which
4208        // case the loop top commits that token and samples nothing.
4209        if let Some(c) = forced {
4210            self.spec_forced = Some(c);
4211            self.graph_logits = None;
4212        } else {
4213            let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
4214            row.resize(self.vocab_size, 0.0);
4215            if let Some(c) = self.final_softcap {
4216                for l in row.iter_mut() {
4217                    *l = c * (*l / c).tanh();
4218                }
4219            }
4220            self.graph_logits = Some(row);
4221        }
4222        let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
4223        // Three phases, not two. The round's wall clock was 4 ms longer
4224        // than draft+verify and the difference had nowhere to be seen:
4225        // the accepted prefix re-runs the MTP block once per token to
4226        // keep the draft head's attention cache warm, and the GDN state
4227        // rolls back on any rejection. Both live here, after the verify.
4228        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
4229            let end = subs();
4230            eprintln!(
4231                "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
4232                 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
4233                t_draft.as_secs_f64() * 1e3,
4234                sub_draft - sub0,
4235                (t_verify - t_draft).as_secs_f64() * 1e3,
4236                sub_verify - sub_draft,
4237                (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
4238                end - sub_verify,
4239            );
4240        }
4241        Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
4242    }
4243
4244    /// Micro-benchmark: two single-position forwards vs one fused pair
4245    /// from the current cache state (KV rewound after each probe).
4246    /// Returns (two_singles_ms, fused_pair_ms) per probe, or the (0, 0)
4247    /// sentinel when this model has no pair path to measure — the same
4248    /// answer the o1 arm gives, and the bench prints it the same way.
4249    /// (An architecture that loads its own layers leaves `weights.layers`
4250    /// empty; walking it here was an index panic, found by `bench` on
4251    /// deepseek_v4.)
4252    pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
4253        if !self.pair_supported() {
4254            return (0.0, 0.0);
4255        }
4256        let emb1 = self.embed_single(1);
4257        let emb2 = self.embed_single(2);
4258        let pos = self.kv_cache.seq_len();
4259
4260        let t0 = std::time::Instant::now();
4261        for _ in 0..iters {
4262            let _ = self.forward_layers(&emb1, pos, None);
4263            let _ = self.forward_layers(&emb2, pos + 1, None);
4264            for l in &mut self.kv_cache.layers {
4265                l.truncate_last(2);
4266            }
4267        }
4268        let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
4269
4270        let t1 = std::time::Instant::now();
4271        for _ in 0..iters {
4272            let _ = self.forward_pair(&emb1, &emb2, pos);
4273            for l in &mut self.kv_cache.layers {
4274                l.truncate_last(2);
4275            }
4276        }
4277        let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
4278        (singles_ms, pair_ms)
4279    }
4280
4281    /// Fused two-position forward: weight rows are streamed from memory
4282    /// once per layer for both positions. Full layers → fused GQA pair;
4283    /// linear layers → vmf_phase pair (lane 2 state is tentative in the
4284    /// per-layer scratch until the draft is accepted).
4285    /// Whether the fused two-position path covers every layer kind in
4286    /// this model. MLA and KDA run per position (their pair arms are
4287    /// unreachable); the seq prefill falls back to singles for them.
4288    fn pair_supported(&self) -> bool {
4289        // An EMPTY layer stack means the architecture loaded its own and
4290        // this path has nothing to walk. Checking that directly, rather
4291        // than naming each such architecture, is what makes the guard hold
4292        // for the next one: `any()` over no layers is false, so a
4293        // feature-by-feature test says "supported" for a model that has no
4294        // layers here at all.
4295        !self.weights.layers.is_empty()
4296            && self.g3n.is_none()
4297            && !self
4298                .weights
4299                .layers
4300                .iter()
4301                .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
4302    }
4303
4304    fn forward_pair(
4305        &mut self,
4306        emb1: &[f32],
4307        emb2: &[f32],
4308        position: usize,
4309    ) -> (Vec<f32>, Vec<f32>) {
4310        let mut h1 = emb1.to_vec();
4311        let mut h2 = emb2.to_vec();
4312        let (_nkv, _hd, hs, _rd, eps) = (
4313            self.num_kv_heads,
4314            self.head_dim,
4315            self.hidden_size,
4316            self.rotary_dim,
4317            self.rms_eps,
4318        );
4319        let pool = self.pool.clone();
4320
4321        for li in 0..self.num_layers {
4322            let lw = &self.weights.layers[self.phys_layer(li)];
4323            // Norms into pipeline scratch (4 allocs/layer on the MTP
4324            // decode hot path before this).
4325            inference::rms_norm_into(
4326                &h1,
4327                &lw.input_norm,
4328                self.rms_eps,
4329                self.norm_style,
4330                &mut self.ws.n1,
4331            );
4332            inference::rms_norm_into(
4333                &h2,
4334                &lw.input_norm,
4335                self.rms_eps,
4336                self.norm_style,
4337                &mut self.ws.n2,
4338            );
4339
4340            let (a1, a2) = match &lw.attn {
4341                AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4342                AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4343                AttnKind::Linear(w) => {
4344                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
4345                    let layer = &mut self.kv_cache.layers[li];
4346                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4347                    vmf_phase_pair(
4348                        &self.ws.n1,
4349                        &self.ws.n2,
4350                        w,
4351                        &cfg,
4352                        state,
4353                        scratch,
4354                        self.pool.as_deref(),
4355                    )
4356                }
4357                AttnKind::LinearGdn(w) => {
4358                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
4359                    let layer = &mut self.kv_cache.layers[li];
4360                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4361                    gdn_pair(
4362                        &self.ws.n1,
4363                        &self.ws.n2,
4364                        w,
4365                        &cfg,
4366                        state,
4367                        scratch,
4368                        self.pool.as_deref(),
4369                    )
4370                }
4371                AttnKind::ShortConv(w) => {
4372                    let cfg = self
4373                        .short_conv_cfg
4374                        .expect("short-conv layer without short_conv_cfg");
4375                    let layer = &mut self.kv_cache.layers[li];
4376                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4377                    short_conv_pair(
4378                        &self.ws.n1,
4379                        &self.ws.n2,
4380                        w,
4381                        &cfg,
4382                        state,
4383                        scratch,
4384                        self.pool.as_deref(),
4385                    )
4386                }
4387                AttnKind::Full {
4388                    wq,
4389                    wk,
4390                    wv,
4391                    wo,
4392                    q_norm,
4393                    k_norm,
4394                    output_gate,
4395                    softplus_gate,
4396                    bias,
4397                } => {
4398                    let inv_freq_l = self.layer_inv_freq(li);
4399                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
4400                    let cfg = QwenAttnCfg {
4401                        num_heads: self.layer_num_heads(li),
4402                        num_kv_heads: nkv_l,
4403                        head_dim: hd_l,
4404                        hidden_size: hs,
4405                        position,
4406                        inv_freq: &inv_freq_l,
4407                        rotary_dim: rd_l,
4408                        scale: self.attn_scale,
4409                        softcap: self.attn_softcap,
4410                        window: self.layer_window(li),
4411                        v_norm: self.attn_v_norm,
4412                        q_norm: q_norm.as_deref(),
4413                        k_norm: k_norm.as_deref(),
4414                        output_gate: *output_gate,
4415                        softplus_gate: softplus_gate
4416                            .as_ref()
4417                            .map(|(gate, per_head)| (gate, *per_head)),
4418                        rope_scale: self.layer_rope_scale(li),
4419                        bias: bias
4420                            .as_ref()
4421                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4422                        rms_eps: eps,
4423                        norm_style: self.norm_style,
4424                        pool: pool.as_deref(),
4425                    };
4426                    attention::qwen_attention_pair(
4427                        &self.ws.n1,
4428                        &self.ws.n2,
4429                        wq,
4430                        wk,
4431                        wv,
4432                        wo,
4433                        &mut self.kv_cache.layers[li],
4434                        &cfg,
4435                    )
4436                }
4437            };
4438            let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
4439                Some(w) => (
4440                    inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
4441                    inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
4442                ),
4443                None => (a1, a2),
4444            };
4445            for i in 0..self.hidden_size {
4446                h1[i] += a1[i];
4447                h2[i] += a2[i];
4448            }
4449            let (mut a1, mut a2) = (a1, a2);
4450            attention::recycle_buf(&mut a1);
4451            attention::recycle_buf(&mut a2);
4452
4453            let lw = &self.weights.layers[self.phys_layer(li)];
4454            inference::rms_norm_into(
4455                &h1,
4456                &lw.post_norm,
4457                self.rms_eps,
4458                self.norm_style,
4459                &mut self.ws.p1,
4460            );
4461            inference::rms_norm_into(
4462                &h2,
4463                &lw.post_norm,
4464                self.rms_eps,
4465                self.norm_style,
4466                &mut self.ws.p2,
4467            );
4468            let (f1, f2) = match &lw.ffn {
4469                // Dual-branch layers need the raw residuals — run the
4470                // two positions through the same fn decode uses.
4471                FfnKind::DenseMoe(dm) => (
4472                    dense_moe_ffn(
4473                        dm,
4474                        &self.ws.p1,
4475                        &h1,
4476                        self.rms_eps,
4477                        self.norm_style,
4478                        self.pool.as_deref(),
4479                    ),
4480                    dense_moe_ffn(
4481                        dm,
4482                        &self.ws.p2,
4483                        &h2,
4484                        self.rms_eps,
4485                        self.norm_style,
4486                        self.pool.as_deref(),
4487                    ),
4488                ),
4489                _ => ffn_forward_pair(
4490                    &lw.ffn,
4491                    &self.ws.p1,
4492                    &self.ws.p2,
4493                    self.pool.as_deref(),
4494                    None,
4495                ),
4496            };
4497            let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
4498                Some(w) => (
4499                    inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
4500                    inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
4501                ),
4502                None => (f1, f2),
4503            };
4504            for i in 0..self.hidden_size {
4505                h1[i] += f1[i];
4506                h2[i] += f2[i];
4507            }
4508            let (mut f1, mut f2) = (f1, f2);
4509            attention::recycle_buf(&mut f1);
4510            attention::recycle_buf(&mut f2);
4511            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
4512                for i in 0..self.hidden_size {
4513                    h1[i] *= sc;
4514                    h2[i] *= sc;
4515                }
4516            }
4517            // Looped Transformer: apply final norm at the end of each loop iteration.
4518            if self.is_loop_end(li) && li + 1 < self.num_layers {
4519                h1 = inference::rms_norm(
4520                    &h1,
4521                    &self.weights.final_norm,
4522                    self.rms_eps,
4523                    self.norm_style,
4524                );
4525                h2 = inference::rms_norm(
4526                    &h2,
4527                    &self.weights.final_norm,
4528                    self.rms_eps,
4529                    self.norm_style,
4530                );
4531            }
4532        }
4533        (h1, h2)
4534    }
4535
4536    /// Commit lane-2 linear states after an accepted draft.
4537    fn commit_linear_scratch(&mut self) {
4538        for layer in &mut self.kv_cache.layers {
4539            if !layer.linear_scratch.is_empty() {
4540                std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
4541                layer.linear_scratch.clear();
4542            }
4543        }
4544    }
4545
4546    /// Forward a full id sequence from a fresh cache and return the
4547    /// logits after the last position (golden-parity harness, bench).
4548    pub fn forward_ids(
4549        &mut self,
4550        ids: &[u32],
4551        task_mask: Option<&TaskMask>,
4552    ) -> Result<Vec<f32>, String> {
4553        if ids.is_empty() {
4554            return Err("empty id sequence".to_string());
4555        }
4556        self.kv_cache.clear();
4557        self.kv_history.clear();
4558        self.o1_begin();
4559        let mut hidden = vec![0.0f32; self.hidden_size];
4560        let mut pos = 0usize;
4561        // Same routing predicate generation uses. Two reasons it must be
4562        // the same one: (1) a GDN hybrid's recurrent state is GPU-
4563        // resident, and a batched CPU prefill would build it on the host
4564        // only — decode then reads buffers the prefill never wrote;
4565        // (2) bench times THIS function and calls the result "prefill",
4566        // so a different path here reports a number production never
4567        // sees (W2 on 2×5090: 8.7 tok/s reported against 125 real).
4568        if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
4569            // prefill-GEMM in chunks; only the last position's hidden is
4570            // needed. (o1-compatible: the batch path attends per position
4571            // through qwen_attention, which carries the collection hook.)
4572            let chunk = prefill_chunk();
4573            let hs = self.hidden_size;
4574            while pos < ids.len() {
4575                let end = (pos + chunk).min(ids.len());
4576                let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4577                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4578                pos = end;
4579            }
4580        }
4581        // Same guards as generation's prefill — INCLUDING the graph one.
4582        // The CPU pair walk was intercepting positions that the resident
4583        // token graph would have run itself: on a GDN hybrid over wgpu
4584        // that is 89 ms of host forward against 7 ms of device submit,
4585        // and it made prefill look 12× slower than it is (W2 on an RTX
4586        // 5090, ctx 512: 11.2 tok/s with the walk, 136.6 without).
4587        // CMF_PAIR=0 opts out; a model whose layers live outside
4588        // `weights.layers` has no pair walk to take.
4589        if task_mask.is_none()
4590            && !self.graph_prefill_preferred()
4591            && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
4592            && self.pair_supported()
4593        {
4594            while pos + 1 < ids.len() {
4595                let e1 = self.embed_single(ids[pos]);
4596                let e2 = self.embed_single(ids[pos + 1]);
4597                let (_, h2) = self.forward_pair(&e1, &e2, pos);
4598                self.commit_linear_scratch();
4599                hidden = h2;
4600                pos += 2;
4601            }
4602        }
4603        while pos < ids.len() {
4604            hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4605            pos += 1;
4606        }
4607        // Harness contract: after forward_ids the cache is decode-ready —
4608        // under o1 that means sealed (bench measures the seal as part of
4609        // prefill, honestly).
4610        self.o1_seal();
4611        let normed = inference::rms_norm(
4612            &hidden,
4613            &self.weights.final_norm,
4614            self.rms_eps,
4615            self.norm_style,
4616        );
4617        Ok(self.lm_head_forward(&normed))
4618    }
4619
4620    /// Teacher-forced perplexity over a token sequence (phase-C gate:
4621    /// honest quant comparisons instead of prompt vibes).
4622    ///
4623    /// Attention is EXACT even on a model whose layers are flagged for
4624    /// the O(1) kernel — scoring the backbone is the default on purpose
4625    /// (it is the yardstick). `nll_ids_o1` scores the CONVERTED model.
4626    pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
4627        let (nll, cnt) = self.nll_ids_from(ids, 0);
4628        (nll / cnt.max(1) as f64).exp()
4629    }
4630
4631    /// DTG-MA calibration pass (Patent 2): run `ids` through the model
4632    /// (CPU path, per position) and return each layer's per-neuron
4633    /// activation mass Σ|silu(gate)·up| — the statistic the task-guided
4634    /// FFN mask is derived from.
4635    pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
4636        self.kv_cache.clear();
4637        self.kv_history.clear();
4638        FFN_PROBE.with(|p| {
4639            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
4640        });
4641        crate::gpu::cpu_scope(|| {
4642            for (pos, &id) in ids.iter().enumerate() {
4643                let emb = self.embed_single(id);
4644                let _ = self.forward_layers(&emb, pos, None);
4645            }
4646        });
4647        self.kv_cache.clear();
4648        self.kv_history.clear();
4649        FFN_PROBE
4650            .with(|p| p.borrow_mut().take())
4651            .unwrap_or_default()
4652    }
4653
4654    /// `probe_ffn_mass` over the BATCHED prefill: same accumulator, one
4655    /// sweep instead of one forward per token. What makes the statistic
4656    /// affordable on a 27B.
4657    pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
4658        self.kv_cache.clear();
4659        self.kv_history.clear();
4660        FFN_PROBE.with(|p| {
4661            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
4662        });
4663        for chunk in ids.chunks(256) {
4664            if chunk.len() < 2 {
4665                continue;
4666            }
4667            let _ = self.nll_ids_masked(chunk, 0, None);
4668        }
4669        self.kv_cache.clear();
4670        self.kv_history.clear();
4671        FFN_PROBE
4672            .with(|p| p.borrow_mut().take())
4673            .unwrap_or_default()
4674    }
4675
4676    /// Teacher-forced PPL with a task mask active (sparse execution) —
4677    /// the quality gate for a DTG-MA-masked skill. Sequential per
4678    /// position: the batched prefill path is dense-only.
4679    pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
4680        self.kv_cache.clear();
4681        self.kv_history.clear();
4682        let mut nll = 0f64;
4683        let mut cnt = 0usize;
4684        let mut hidden = vec![0f32; self.hidden_size];
4685        for (pos, &id) in ids.iter().enumerate() {
4686            if pos > 0 {
4687                inference::rms_norm_into(
4688                    &hidden,
4689                    &self.weights.final_norm,
4690                    self.rms_eps,
4691                    self.norm_style,
4692                    &mut self.ws.n1,
4693                );
4694                let mut logits = self.lm_head_forward(&self.ws.n1);
4695                let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4696                let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
4697                let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
4698                nll -= p.max(1e-300).ln();
4699                cnt += 1;
4700                attention::recycle_buf(&mut logits);
4701            }
4702            let emb = self.embed_single(id);
4703            hidden = self.forward_layers(&emb, pos, Some(mask));
4704        }
4705        self.kv_cache.clear();
4706        self.kv_history.clear();
4707        (nll / cnt.max(1) as f64).exp()
4708    }
4709
4710    /// Teacher-forced NLL sum + scored-token count over positions
4711    /// `start..len-1`, attention EXACT. Positions below `start` still
4712    /// run — they are the context — they are just not scored, so this
4713    /// pairs with `nll_ids_o1(ids, start)` over the very same tokens.
4714    ///
4715    /// Returning (nll, cnt) rather than a ppl is what lets a windowed
4716    /// caller combine windows before the exp, so every scored token
4717    /// weighs the same regardless of how the windows are cut.
4718    /// `nll_ids_from` with a task mask held active at every position.
4719    ///
4720    /// The batched prefill path does not thread masks, so this walks the
4721    /// per-position forward — slower, but it scores the file exactly the
4722    /// way `run --task` will serve it, which is the point of the gate
4723    /// that calls it. With `None` it defers to the fast path.
4724    /// Masked scoring rides the SAME batched sweep as unmasked scoring —
4725    /// the masked-inference fast path: `prefill_batch_masked` lands the
4726    /// per-visit FFN rows on the activations inside the fused arms. The
4727    /// per-position loop below remains only as the no-batch fallback.
4728    pub fn nll_ids_masked(
4729        &mut self,
4730        ids: &[u32],
4731        start: usize,
4732        task_mask: Option<&TaskMask>,
4733    ) -> (f64, usize) {
4734        let task_mask = self.drop_open_mask(task_mask);
4735        self.nll_ids_inner(ids, start, task_mask)
4736    }
4737
4738    pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
4739        self.nll_ids_inner(ids, start, None)
4740    }
4741
4742    fn nll_ids_inner(
4743        &mut self,
4744        ids: &[u32],
4745        start: usize,
4746        task_mask: Option<&TaskMask>,
4747    ) -> (f64, usize) {
4748        self.kv_cache.clear();
4749        self.kv_history.clear();
4750        let mut nll = 0f64;
4751        let mut cnt = 0usize;
4752        if self.can_prefill_batched() {
4753            // prefill-GEMM: layer-major position chunks, lm_head batched
4754            // (254MB lm_head read once per chunk, not per position).
4755            // The layer chunk is large (grouping positions by MoE experts
4756            // wins with size), lm_head in sub-blocks (logit buffer
4757            // 32×vocab ≈ 32MB instead of 128×).
4758            const CHUNK: usize = 128;
4759            const LM_SUB: usize = 32;
4760            let n = ids.len().saturating_sub(1);
4761            let hs = self.hidden_size;
4762            let rows = self.weights.lm_head.rows();
4763            let mut pos = 0usize;
4764            while pos < n {
4765                let end = (pos + CHUNK).min(n);
4766                let bsz = end - pos;
4767                let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4768                let mut k0 = 0usize;
4769                while k0 < bsz {
4770                    let k1 = (k0 + LM_SUB).min(bsz);
4771                    let sb = k1 - k0;
4772                    // Sub-block entirely below the scored range: the KV
4773                    // it just built is all this pass needed from it.
4774                    if pos + k1 <= start {
4775                        k0 = k1;
4776                        continue;
4777                    }
4778                    let mut normed = vec![0.0f32; sb * hs];
4779                    for k in 0..sb {
4780                        let r = inference::rms_norm(
4781                            &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
4782                            &self.weights.final_norm,
4783                            self.rms_eps,
4784                            self.norm_style,
4785                        );
4786                        normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
4787                    }
4788                    let mut logits = vec![0.0f32; sb * rows];
4789                    self.weights
4790                        .lm_head
4791                        .matmat(&normed, sb, &mut logits, self.pool.as_deref());
4792                    for k in 0..sb {
4793                        if pos + k0 + k < start {
4794                            continue;
4795                        }
4796                        let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
4797                        if let Some(mu) = self.logit_multiplier {
4798                            for v in lg.iter_mut() {
4799                                *v *= mu;
4800                            }
4801                        }
4802                        // Gemma-class final-logit soft-capping: the
4803                        // decode paths apply it; scoring must too, or
4804                        // the uncapped softmax misprices every token.
4805                        if let Some(c) = self.final_softcap {
4806                            for v in lg.iter_mut() {
4807                                *v = c * (*v / c).tanh();
4808                            }
4809                        }
4810                        // Cortiq Embryo hierarchical head: same correction
4811                        // the decode path applies (lm_head_forward).
4812                        if let Some(cm) = self.head_clusters.clone() {
4813                            self.hierarchical_head_logprobs(&normed[k * hs..(k + 1) * hs], &cm, lg);
4814                        }
4815                        let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
4816                        let target = ids[pos + k0 + k + 1] as usize;
4817                        let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4818                        let lse: f64 = lg
4819                            .iter()
4820                            .map(|&v| ((v - max) as f64).exp())
4821                            .sum::<f64>()
4822                            .ln()
4823                            + max as f64;
4824                        nll += lse - lg[target] as f64;
4825                        cnt += 1;
4826                        if std::env::var("CMF_PPL_TRACE").is_ok() {
4827                            let top = lg
4828                                .iter()
4829                                .enumerate()
4830                                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4831                                .map(|(i, _)| i)
4832                                .unwrap_or(0);
4833                            eprintln!(
4834                                "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
4835                                pos + k0 + k,
4836                                target,
4837                                lse - lg[target] as f64,
4838                                top,
4839                                lg[target],
4840                                lg[top]
4841                            );
4842                        }
4843                    }
4844                    k0 = k1;
4845                }
4846                pos = end;
4847            }
4848            self.kv_cache.clear();
4849            self.kv_history.clear();
4850            return (nll, cnt);
4851        }
4852        for pos in 0..ids.len().saturating_sub(1) {
4853            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4854            // Architectures whose head lives inside their own stack return
4855            // the logits out of band and a zero hidden — DeepSeek-V4 folds
4856            // its hyper-connection copies between the last layer and the
4857            // norm, so it cannot hand back a vector this loop could use.
4858            // Scoring the zeros gave a perplexity of exactly the vocabulary
4859            // size, which is a uniform distribution reported as a
4860            // measurement. `generate` already reads this channel.
4861            let out_of_band = self.graph_logits.take();
4862            if pos < start {
4863                continue;
4864            }
4865            let logits = match out_of_band {
4866                Some(lg) => lg,
4867                None => {
4868                    let normed = inference::rms_norm(
4869                        &hidden,
4870                        &self.weights.final_norm,
4871                        self.rms_eps,
4872                        self.norm_style,
4873                    );
4874                    // lm_head_forward applies the final-logit softcap itself
4875                    // — capping again here double-squashed gemma-class
4876                    // logits (tanh∘tanh) and reported a flattered ppl.
4877                    self.lm_head_forward(&normed)
4878                }
4879            };
4880            let target = ids[pos + 1] as usize;
4881            let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4882            let lse: f64 = logits
4883                .iter()
4884                .map(|&v| ((v - max) as f64).exp())
4885                .sum::<f64>()
4886                .ln()
4887                + max as f64;
4888            let tok_nll = lse - logits[target] as f64;
4889            if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4890                let top = logits
4891                    .iter()
4892                    .enumerate()
4893                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4894                    .map(|(i, _)| i)
4895                    .unwrap_or(0);
4896                eprintln!(
4897                    "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4898                    logits[target], logits[top]
4899                );
4900            }
4901            nll += tok_nll;
4902            cnt += 1;
4903        }
4904        self.kv_cache.clear();
4905        self.kv_history.clear();
4906        (nll, cnt)
4907    }
4908
4909    /// Teacher-forced NLL of the CONVERTED model: the O(1) Nyström path
4910    /// is ACTIVE over the scored positions. Returns (nll sum, scored
4911    /// count) over `prefill..len-1`.
4912    ///
4913    /// Runtime discipline, deliberately NOT the matrix probe's: the
4914    /// first `prefill` tokens run the exact prompt pass — that pass is
4915    /// what freezes the landmarks and M — and every scored position then
4916    /// goes through `NystromState::step()`, the same code decode runs.
4917    /// So the landmarks are PREFILL-frozen (what ships), not
4918    /// full-sequence oracles (what the published probe measured), and
4919    /// every scored row carries a real far field rather than sitting
4920    /// inside the exact window.
4921    ///
4922    /// Pair with `nll_ids_from(ids, prefill)` for the exact baseline
4923    /// over the identical token set — that ratio is the honest one.
4924    pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
4925        self.kv_cache.clear();
4926        self.kv_history.clear();
4927        self.o1_begin();
4928        let n = ids.len().saturating_sub(1);
4929        let p = prefill.min(n);
4930        // Exact prompt pass over ids[..p]: the seal consumes its q/k/v.
4931        let mut pos = 0usize;
4932        if self.can_prefill_batched() {
4933            const CHUNK: usize = 128;
4934            while pos < p {
4935                let end = (pos + CHUNK).min(p);
4936                let _ = self.prefill_batch(&ids[pos..end], pos);
4937                pos = end;
4938            }
4939        } else {
4940            while pos < p {
4941                let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4942                pos += 1;
4943            }
4944        }
4945        self.o1_seal();
4946
4947        let mut nll = 0f64;
4948        let mut cnt = 0usize;
4949        for pos in p..n {
4950            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4951            let normed = inference::rms_norm(
4952                &hidden,
4953                &self.weights.final_norm,
4954                self.rms_eps,
4955                self.norm_style,
4956            );
4957            // lm_head_forward applies the final-logit softcap itself —
4958            // capping again here double-squashed gemma-class logits
4959            // (tanh∘tanh) and reported a flattered ppl.
4960            let logits = self.lm_head_forward(&normed);
4961            let target = ids[pos + 1] as usize;
4962            let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4963            let lse: f64 = logits
4964                .iter()
4965                .map(|&v| ((v - max) as f64).exp())
4966                .sum::<f64>()
4967                .ln()
4968                + max as f64;
4969            let tok_nll = lse - logits[target] as f64;
4970            if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4971                let top = logits
4972                    .iter()
4973                    .enumerate()
4974                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4975                    .map(|(i, _)| i)
4976                    .unwrap_or(0);
4977                eprintln!(
4978                    "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4979                    logits[target], logits[top]
4980                );
4981            }
4982            nll += tok_nll;
4983            cnt += 1;
4984        }
4985        self.kv_cache.clear();
4986        self.kv_history.clear();
4987        (nll, cnt)
4988    }
4989
4990    /// Teacher-forced calibration data (B1): for each position, whether the
4991    /// argmax equals the actual next token, and the top-1 softmax prob
4992    /// (Born mass) under EACH temperature in `temps` — all from ONE forward
4993    /// pass (argmax/correctness are temperature-invariant; only p_max
4994    /// reshapes). Feeds `cortiq calibrate` (reliability/ECE + temperature
4995    /// fit): is the model's confidence a true property, or does it need a
4996    /// measured scaling?
4997    pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
4998        self.kv_cache.clear();
4999        self.kv_history.clear();
5000        let n = ids.len().saturating_sub(1);
5001        let mut correct = Vec::with_capacity(n);
5002        let mut pmax = Vec::with_capacity(n);
5003        for pos in 0..n {
5004            let emb = self.embed_single(ids[pos]);
5005            let hidden = self.forward_layers(&emb, pos, None);
5006            let normed = inference::rms_norm(
5007                &hidden,
5008                &self.weights.final_norm,
5009                self.rms_eps,
5010                self.norm_style,
5011            );
5012            // lm_head_forward applies the final-logit softcap itself —
5013            // capping again here double-squashed gemma-class logits
5014            // (tanh∘tanh) and reported a flattered ppl.
5015            let logits = self.lm_head_forward(&normed);
5016            let target = ids[pos + 1] as usize;
5017            let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
5018            for (i, &v) in logits.iter().enumerate() {
5019                if v > mval {
5020                    mval = v;
5021                    amax = i;
5022                }
5023            }
5024            correct.push(amax == target);
5025            let row: Vec<f32> = temps
5026                .iter()
5027                .map(|&t| {
5028                    let tt = t.max(1e-3);
5029                    let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
5030                    1.0 / s.max(1e-12) // numerator at the max is exp(0)=1
5031                })
5032                .collect();
5033            pmax.push(row);
5034        }
5035        self.kv_cache.clear();
5036        self.kv_history.clear();
5037        (correct, pmax)
5038    }
5039
5040    /// Teacher-forced PPL with the dynamic router driving per-window
5041    /// skill switches (VMF experiment №2 measurement). Sequential (φ
5042    /// must update per token), returns (ppl, switch_count). The router
5043    /// must be enabled (`enable_dynamic_routing`); else this equals
5044    /// plain `ppl_ids`. The active skill when scoring token t shapes the
5045    /// logits for t+1 — on-policy over the held-out text itself.
5046    pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
5047        let mut router = match self.dyn_router.take() {
5048            Some(r) => r,
5049            None => return (self.ppl_ids(ids), 0),
5050        };
5051        router.reset();
5052        self.dyn_phi_seen = 0;
5053        let _ = self.set_active_skill(None);
5054
5055        self.kv_cache.clear();
5056
5057        self.kv_history.clear();
5058        let mut nll = 0f64;
5059        let mut cnt = 0usize;
5060        for pos in 0..ids.len().saturating_sub(1) {
5061            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
5062            let normed = inference::rms_norm(
5063                &hidden,
5064                &self.weights.final_norm,
5065                self.rms_eps,
5066                self.norm_style,
5067            );
5068            // lm_head_forward applies the final-logit softcap itself —
5069            // capping again here double-squashed gemma-class logits
5070            // (tanh∘tanh) and reported a flattered ppl.
5071            let logits = self.lm_head_forward(&normed);
5072            let target = ids[pos + 1] as usize;
5073            let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
5074            let lse: f64 = logits
5075                .iter()
5076                .map(|&v| ((v - max) as f64).exp())
5077                .sum::<f64>()
5078                .ln()
5079                + max as f64;
5080            let tok_nll = lse - logits[target] as f64;
5081            if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
5082                let top = logits
5083                    .iter()
5084                    .enumerate()
5085                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
5086                    .map(|(i, _)| i)
5087                    .unwrap_or(0);
5088                eprintln!(
5089                    "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
5090                    logits[target], logits[top]
5091                );
5092            }
5093            nll += tok_nll;
5094            cnt += 1;
5095            // Route on the evolving φ (drives the NEXT token's skill).
5096            let phi = self.dyn_phi_ema.clone();
5097            if let Some(new_active) = router.step(&phi, pos) {
5098                let _ = self.set_active_skill(new_active);
5099            }
5100        }
5101        let switches = router.switches.len();
5102        let _ = self.set_active_skill(None);
5103        self.dyn_router = Some(router);
5104        self.kv_cache.clear();
5105        self.kv_history.clear();
5106        ((nll / cnt.max(1) as f64).exp(), switches)
5107    }
5108
5109    /// Routing probe φ (spec §9): mean-pooled hidden after `layer`.
5110    pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
5111        self.kv_cache.clear();
5112        self.kv_history.clear();
5113        let mut acc = vec![0f32; self.hidden_size];
5114        for (pos, &id) in ids.iter().enumerate() {
5115            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
5116            for (a, v) in acc.iter_mut().zip(&h) {
5117                *a += v;
5118            }
5119        }
5120        let n = ids.len().max(1) as f32;
5121        for a in acc.iter_mut() {
5122            *a /= n;
5123        }
5124        self.kv_cache.clear();
5125        self.kv_history.clear();
5126        acc
5127    }
5128
5129    /// Layer-major batched prefill (prefill-GEMM): full-attention —
5130    /// per-position with the existing operators (KV grows naturally,
5131    /// causality preserved), GDN projections / FFN / MoE — batched
5132    /// (a weight row is read from DRAM once per chunk, not per
5133    /// position). Returns the hidden of all positions [b × hidden].
5134    fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
5135        self.prefill_batch_masked(ids, start_pos, None)
5136    }
5137
5138    /// `prefill_batch` with a task mask honored on the dense-FFN panels
5139    /// (the masked-inference fast path: full fused compute, mask lands on
5140    /// the activations). The whole-chunk GPU graph is skipped for masked
5141    /// layers by the callers' arms; the per-GEMM device paths stay in
5142    /// play because the zeroing happens on the host between them.
5143    fn prefill_batch_masked(
5144        &mut self,
5145        ids: &[u32],
5146        start_pos: usize,
5147        task_mask: Option<&TaskMask>,
5148    ) -> Vec<f32> {
5149        self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
5150    }
5151
5152    /// The layer-major batched walk over a layer span [from..upto_excl):
5153    /// the whole prefill machinery (chunk graph, batched attends, GEMM
5154    /// panels) for a PARTIAL stack — the network split's prefill rides
5155    /// the same canon as the local one. Input is token ids (embeds
5156    /// itself, coordinator side) or ready boundary hiddens (worker side).
5157    fn prefill_batch_span(
5158        &mut self,
5159        input: PrefillIn<'_>,
5160        start_pos: usize,
5161        task_mask: Option<&TaskMask>,
5162        from: usize,
5163        upto_excl: usize,
5164    ) -> Vec<f32> {
5165        let hs = self.hidden_size;
5166        let b = match input {
5167            PrefillIn::Ids(ids) => ids.len(),
5168            PrefillIn::Hidden(hb) => hb.len() / hs,
5169        };
5170        let upto_excl = upto_excl.min(self.num_layers);
5171        // The CPU embed is deferred: when the chunk graph takes the run
5172        // from layer 0 it gathers the embeddings on the device instead.
5173        // A hidden input is ready by definition.
5174        let mut h: Vec<f32>;
5175        let mut h_ready;
5176        match input {
5177            PrefillIn::Ids(_) => {
5178                h = vec![0.0; b * hs];
5179                h_ready = false;
5180            }
5181            PrefillIn::Hidden(hb) => {
5182                h = hb.to_vec();
5183                h_ready = true;
5184            }
5185        }
5186        let fill_h = |h: &mut Vec<f32>, me: &Self| {
5187            if let PrefillIn::Ids(ids) = input {
5188                for (bi, &id) in ids.iter().enumerate() {
5189                    let e = me.embed_single(id);
5190                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
5191                }
5192                if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5193                    if let Ok(t) = tp.parse::<usize>() {
5194                        if t >= start_pos && t < start_pos + ids.len() {
5195                            let bi = t - start_pos;
5196                            let row = &h[bi * hs..(bi + 1) * hs];
5197                            let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
5198                            eprintln!(
5199                                "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
5200                                ids[bi], row[0], row[1], ids.len(), &ids[..ids.len().min(8)]
5201                            );
5202                        }
5203                    }
5204                }
5205            }
5206        };
5207        let (_nkv, _hd, _rd, eps) = (
5208            self.num_kv_heads,
5209            self.head_dim,
5210            self.rotary_dim,
5211            self.rms_eps,
5212        );
5213        let pool = self.pool.clone();
5214        let norm_style = self.norm_style;
5215
5216        #[cfg(target_os = "macos")]
5217        let mut chunk_skip_until = 0usize;
5218        for li in from..upto_excl {
5219            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU
5220            // GPU chunk graph (default-on under CMF_GPU=1): a run of
5221            // consecutive eligible layers for the whole chunk in ONE
5222            // Metal submission — norm, QKV, RoPE with fused mirror
5223            // append, causal attend, O, FFN, hidden device-resident
5224            // across the run. Any refusal falls through to the CPU path.
5225            #[cfg(target_os = "macos")]
5226            if task_mask.is_none() {
5227                if li < chunk_skip_until {
5228                    continue;
5229                }
5230                // Device-side embedding needs a q8_row embedding matrix;
5231                // with any other layout the CPU fills `h` first and the
5232                // graph starts from a ready hidden (refusing the whole
5233                // run over the embedding alone kept q4t models — the
5234                // whole Nanbeige/Bonsai class — on the CPU prefill).
5235                if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
5236                    fill_h(&mut h, self);
5237                    h_ready = true;
5238                }
5239                let ids_for_embed = match input {
5240                    PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
5241                    PrefillIn::Hidden(_) => None,
5242                };
5243                let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
5244                if end > li {
5245                    h_ready = true;
5246                    chunk_skip_until = end;
5247                    // Looped Transformer: the graph stopped at a loop
5248                    // boundary — apply final norm before the next iteration.
5249                    if self.is_loop_end(end - 1) && end < self.num_layers {
5250                        for bi in 0..b {
5251                            let normed = inference::rms_norm(
5252                                &h[bi * hs..(bi + 1) * hs],
5253                                &self.weights.final_norm,
5254                                eps,
5255                                norm_style,
5256                            );
5257                            h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
5258                        }
5259                    }
5260                    continue;
5261                }
5262            }
5263            if !h_ready {
5264                fill_h(&mut h, self);
5265                h_ready = true;
5266            }
5267            let lw = &self.weights.layers[self.phys_layer(li)];
5268            // ── attention ──
5269            match &lw.attn {
5270                AttnKind::Kda(w) => {
5271                    // Projections batched, recurrence sequential.
5272                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
5273                    let mut normed = vec![0.0f32; b * hs];
5274                    for bi in 0..b {
5275                        inference::rms_norm_into(
5276                            &h[bi * hs..(bi + 1) * hs],
5277                            &lw.input_norm,
5278                            eps,
5279                            norm_style,
5280                            &mut normed[bi * hs..(bi + 1) * hs],
5281                        );
5282                    }
5283                    let attn = crate::linear_core::kda_forward_batch(
5284                        &normed,
5285                        b,
5286                        w,
5287                        &cfg,
5288                        &mut self.kv_cache.layers[li].linear_state,
5289                        pool.as_deref(),
5290                    );
5291                    for (dst, &a) in h.iter_mut().zip(&attn) {
5292                        *dst += a;
5293                    }
5294                }
5295                AttnKind::LinearGdn(w) => {
5296                    // Projections batched, recurrence sequential.
5297                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5298                    let mut normed = vec![0.0f32; b * hs];
5299                    for bi in 0..b {
5300                        let r = inference::rms_norm(
5301                            &h[bi * hs..(bi + 1) * hs],
5302                            &lw.input_norm,
5303                            eps,
5304                            norm_style,
5305                        );
5306                        normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5307                    }
5308                    let attn = crate::linear_core::gdn_forward_batch(
5309                        &normed,
5310                        b,
5311                        w,
5312                        &cfg,
5313                        &mut self.kv_cache.layers[li].linear_state,
5314                        pool.as_deref(),
5315                    );
5316                    for (dst, &a) in h.iter_mut().zip(&attn) {
5317                        *dst += a;
5318                    }
5319                }
5320                AttnKind::ShortConv(w) => {
5321                    // Projections batched over the chunk; the conv walks the
5322                    // contiguous positions in order (same ring as decode).
5323                    let cfg = self
5324                        .short_conv_cfg
5325                        .expect("short-conv layer without short_conv_cfg");
5326                    let mut normed = vec![0.0f32; b * hs];
5327                    for bi in 0..b {
5328                        inference::rms_norm_into(
5329                            &h[bi * hs..(bi + 1) * hs],
5330                            &lw.input_norm,
5331                            eps,
5332                            norm_style,
5333                            &mut normed[bi * hs..(bi + 1) * hs],
5334                        );
5335                    }
5336                    let attn = short_conv_forward_batch(
5337                        &normed,
5338                        b,
5339                        w,
5340                        &cfg,
5341                        &mut self.kv_cache.layers[li].linear_state,
5342                        pool.as_deref(),
5343                    );
5344                    for (dst, &a) in h.iter_mut().zip(&attn) {
5345                        *dst += a;
5346                    }
5347                }
5348                AttnKind::Mla(w) => {
5349                    // Per-position prefill (correctness first; latent
5350                    // batching is a later optimization).
5351                    let inv_freq_l = self.layer_inv_freq(li);
5352                    let rs = self.layer_rope_scale(li);
5353                    let mut normed = vec![0.0f32; hs];
5354                    for bi in 0..b {
5355                        inference::rms_norm_into(
5356                            &h[bi * hs..(bi + 1) * hs],
5357                            &lw.input_norm,
5358                            eps,
5359                            norm_style,
5360                            &mut normed,
5361                        );
5362                        let ao = mla_attention(
5363                            w,
5364                            &normed,
5365                            &mut self.kv_cache.layers[li],
5366                            start_pos + bi,
5367                            &inv_freq_l,
5368                            rs,
5369                            eps,
5370                            pool.as_deref(),
5371                        );
5372                        for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
5373                            *dst += a;
5374                        }
5375                    }
5376                }
5377                AttnKind::Full {
5378                    wq,
5379                    wk,
5380                    wv,
5381                    wo,
5382                    q_norm,
5383                    k_norm,
5384                    output_gate,
5385                    softplus_gate,
5386                    bias,
5387                } => {
5388                    // Chunk-GEMM QKV/O; per-position causal attention
5389                    // inside (roadmap §3 P0 — full-attention prefill no
5390                    // longer re-reads the projection weights b times).
5391                    let mut normed = vec![0.0f32; b * hs];
5392                    for bi in 0..b {
5393                        inference::rms_norm_into(
5394                            &h[bi * hs..(bi + 1) * hs],
5395                            &lw.input_norm,
5396                            eps,
5397                            norm_style,
5398                            &mut normed[bi * hs..(bi + 1) * hs],
5399                        );
5400                    }
5401                    let inv_freq_l = self.layer_inv_freq(li);
5402                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5403                    let cfg = QwenAttnCfg {
5404                        num_heads: self.layer_num_heads(li),
5405                        num_kv_heads: nkv_l,
5406                        head_dim: hd_l,
5407                        hidden_size: hs,
5408                        position: start_pos,
5409                        inv_freq: &inv_freq_l,
5410                        rotary_dim: rd_l,
5411                        scale: self.attn_scale,
5412                        softcap: self.attn_softcap,
5413                        window: self.layer_window(li),
5414                        v_norm: self.attn_v_norm,
5415                        q_norm: q_norm.as_deref(),
5416                        k_norm: k_norm.as_deref(),
5417                        output_gate: *output_gate,
5418                        softplus_gate: softplus_gate
5419                            .as_ref()
5420                            .map(|(gate, per_head)| (gate, *per_head)),
5421                        rope_scale: self.layer_rope_scale(li),
5422                        bias: bias
5423                            .as_ref()
5424                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5425                        rms_eps: eps,
5426                        norm_style,
5427                        pool: pool.as_deref(),
5428                    };
5429                    let mut attn = attention::qwen_attention_batch(
5430                        &normed,
5431                        b,
5432                        wq,
5433                        wk,
5434                        wv,
5435                        wo,
5436                        &mut self.kv_cache.layers[li],
5437                        &cfg,
5438                    );
5439                    if let Some(w) = &lw.attn_out_norm {
5440                        for bi in 0..b {
5441                            inference::rms_norm_into(
5442                                &attn[bi * hs..(bi + 1) * hs],
5443                                w,
5444                                eps,
5445                                norm_style,
5446                                &mut normed[bi * hs..(bi + 1) * hs],
5447                            );
5448                        }
5449                        attn.copy_from_slice(&normed);
5450                    }
5451                    for (dst, &a) in h.iter_mut().zip(&attn) {
5452                        *dst += a;
5453                    }
5454                }
5455                AttnKind::Linear(w) => {
5456                    for bi in 0..b {
5457                        let normed = inference::rms_norm(
5458                            &h[bi * hs..(bi + 1) * hs],
5459                            &lw.input_norm,
5460                            eps,
5461                            norm_style,
5462                        );
5463                        vmf_phase_forward(
5464                            &normed,
5465                            w,
5466                            &self.vmf_cfg.expect("linear layer without vmf_cfg"),
5467                            &mut self.kv_cache.layers[li].linear_state,
5468                            pool.as_deref(),
5469                        )
5470                        .iter()
5471                        .enumerate()
5472                        .for_each(|(i, &a)| h[bi * hs + i] += a);
5473                    }
5474                }
5475            }
5476
5477            // ── FFN batched ──
5478            let lw = &self.weights.layers[self.phys_layer(li)];
5479            let mut post = vec![0.0f32; b * hs];
5480            for bi in 0..b {
5481                let r =
5482                    inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
5483                post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5484            }
5485            // A restrictive per-visit FFN row lands on the activations
5486            // inside the dense arm; an all-open row costs nothing.
5487            let mask_row = task_mask
5488                .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
5489                .and_then(|m| m.ffn_masks.get(li))
5490                .map(|v| v.as_slice());
5491            let mut ffn = match &lw.ffn {
5492                FfnKind::Dense(d) if !d.segs.is_empty() => {
5493                    tube_ffn(d, &post, b, pool.as_deref(), mask_row)
5494                }
5495                FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
5496                FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
5497                // Dual-branch layers run per position (the expert branch
5498                // reads the raw residual — nothing to batch yet).
5499                FfnKind::DenseMoe(dm) => {
5500                    let mut out = vec![0.0f32; b * hs];
5501                    for bi in 0..b {
5502                        let r = dense_moe_ffn(
5503                            dm,
5504                            &post[bi * hs..(bi + 1) * hs],
5505                            &h[bi * hs..(bi + 1) * hs],
5506                            eps,
5507                            norm_style,
5508                            pool.as_deref(),
5509                        );
5510                        out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5511                    }
5512                    out
5513                }
5514            };
5515            if let Some(w) = &lw.ffn_out_norm {
5516                for bi in 0..b {
5517                    inference::rms_norm_into(
5518                        &ffn[bi * hs..(bi + 1) * hs],
5519                        w,
5520                        eps,
5521                        norm_style,
5522                        &mut post[bi * hs..(bi + 1) * hs],
5523                    );
5524                }
5525                ffn.copy_from_slice(&post);
5526            }
5527            for (dst, &f) in h.iter_mut().zip(&ffn) {
5528                *dst += f;
5529            }
5530            if let Some(sc) = lw.layer_scale {
5531                for v in h.iter_mut() {
5532                    *v *= sc;
5533                }
5534            }
5535            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5536                if let Ok(t) = tp.parse::<usize>() {
5537                    if t >= start_pos && t < start_pos + b {
5538                        let bi = t - start_pos;
5539                        let row = &h[bi * hs..(bi + 1) * hs];
5540                        let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
5541                        eprintln!(
5542                            "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
5543                            row[0], row[1]
5544                        );
5545                    }
5546                }
5547            }
5548            // CMF_DEBUG_LAYERS=1: per-layer hidden-state health of the
5549            // LAST prompt position — the knife for "which layer type
5550            // breaks first" on a new architecture.
5551            if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
5552                let row = &h[(b - 1) * hs..b * hs];
5553                let rms =
5554                    (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
5555                let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
5556                eprintln!(
5557                    "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
5558                    match &self.weights.layers[self.phys_layer(li)].attn {
5559                        AttnKind::LinearGdn(_) => "gdn",
5560                        AttnKind::Linear(_) => "vmf",
5561                        AttnKind::ShortConv(_) => "conv",
5562                        _ => "attn",
5563                    },
5564                    match &lw.ffn {
5565                        FfnKind::Moe(_) => "moe",
5566                        FfnKind::Dense(_) => "dense",
5567                        FfnKind::DenseMoe(_) => "dense+moe",
5568                    },
5569                );
5570            }
5571            // Looped Transformer: apply final norm at the end of each loop iteration.
5572            if self.is_loop_end(li) && li + 1 < self.num_layers {
5573                for bi in 0..b {
5574                    let normed = inference::rms_norm(
5575                        &h[bi * hs..(bi + 1) * hs],
5576                        &self.weights.final_norm,
5577                        eps,
5578                        norm_style,
5579                    );
5580                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
5581                }
5582            }
5583            if std::env::var("CMF_TRACE_H").is_ok() {
5584                let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
5585                let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
5586                eprintln!(
5587                    "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
5588                    lw.layer_scale
5589                );
5590            }
5591        }
5592        crate::gpu::set_layer(-1); // lm_head/final ops outside layer-split
5593        h
5594    }
5595
5596    /// Embed a single token.
5597    fn embed_single(&self, id: u32) -> Vec<f32> {
5598        let mut out = vec![0.0f32; self.hidden_size];
5599        if (id as usize) < self.weights.embed_tokens.rows() {
5600            self.weights.embed_tokens.row_f32(id as usize, &mut out);
5601        }
5602        if self.embed_multiplier != 1.0 {
5603            for v in out.iter_mut() {
5604                *v *= self.embed_multiplier;
5605            }
5606        }
5607        // DeepSeek-V4's hash layers route by TOKEN ID, so the id has to
5608        // reach the forward. It rides in slot 0 (the forward re-reads the
5609        // real embedding itself from the table).
5610        if self.dsv4.is_some() {
5611            let mut v = vec![0.0f32; self.hidden_size.max(1)];
5612            v[0] = id as f32;
5613            return v;
5614        }
5615        // Gemma-3n: the per-layer-embedding half needs the token ID, so
5616        // it rides appended to the embedding; the g3n forward splits it.
5617        if let Some(b) = &self.g3n {
5618            return b.0.extend_embedding(id, &out, self.pool.as_deref());
5619        }
5620        out
5621    }
5622
5623    /// A run of consecutive prefill layers on the GPU for the whole
5624    /// chunk (default-on under CMF_GPU=1; CMF_GPU_CHUNK=0 disables).
5625    /// Eligibility per layer: q8_row weights, plain full attention
5626    /// (no output gate), F32 KV, no o1/masks/gemma extras. Returns the
5627    /// first layer index NOT processed (== `li0` when the run is empty).
5628    #[cfg(target_os = "macos")]
5629    fn chunk_run_gpu(
5630        &mut self,
5631        li0: usize,
5632        h: &mut [f32],
5633        b: usize,
5634        pos0: usize,
5635        embed_ids: Option<&[u32]>,
5636        cap: usize,
5637    ) -> usize {
5638        // (The old streaming attend needed a depth bound at ~1k; the
5639        // GEMM attention scales like the CPU path and lifted it.)
5640        // CMF_GPU_CHUNK=0 disables the graph.
5641        if !crate::gpu::enabled_here()
5642            || std::env::var("CMF_GPU_CHUNK")
5643                .map(|v| v == "0")
5644                .unwrap_or(false)
5645            || b < 32
5646            || self.swa.is_some()
5647            || self.global_attn.is_some()
5648            || self.attn_v_norm
5649            || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
5650        {
5651            return li0;
5652        }
5653        let Some(model) = self.model.clone() else {
5654            return li0;
5655        };
5656        let inv_freq = self.inv_freq.clone();
5657        let (nh, nkv, hd, hs) = (
5658            self.num_heads,
5659            self.num_kv_heads,
5660            self.head_dim,
5661            self.hidden_size,
5662        );
5663        // Collect the longest run of consecutive eligible layers.
5664        // Looped Transformer: stop at the loop boundary so the CPU can
5665        // apply loop_final_norm between iterations.
5666        let loop_end = if self.loop_final_norm {
5667            ((li0 / self.physical_layers) + 1) * self.physical_layers
5668        } else {
5669            self.num_layers
5670        };
5671        let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
5672        let mut stored_at: Vec<usize> = Vec::new();
5673        for li in li0..self.num_layers.min(loop_end).min(cap) {
5674            let lw = &self.weights.layers[self.phys_layer(li)];
5675            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
5676                break;
5677            }
5678            let AttnKind::Full {
5679                wq,
5680                wk,
5681                wv,
5682                wo,
5683                q_norm,
5684                k_norm,
5685                output_gate: false,
5686                softplus_gate: None,
5687                bias,
5688            } = &lw.attn
5689            else {
5690                break;
5691            };
5692            let FfnKind::Dense(d) = &lw.ffn else { break };
5693            if d.act != Act::Silu || !d.segs.is_empty() {
5694                break;
5695            }
5696            // q8_row (row_scale populated), or q4_tiled / q4tp (row_scale
5697            // empty — their scales are in the payload). Mixing across the
5698            // seven projections of one layer is fine; the encoder branches
5699            // per weight on the tensor's dtype. Anything else refuses.
5700            fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
5701                t.q8_row_parts()
5702                    .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5703                    .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5704            }
5705            let parts = (
5706                cw(wq),
5707                cw(wk),
5708                cw(wv),
5709                cw(wo),
5710                cw(&d.gate_proj),
5711                cw(&d.up_proj),
5712                cw(&d.down_proj),
5713            );
5714            let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
5715            else {
5716                break;
5717            };
5718            let layer = &self.kv_cache.layers[li];
5719            if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
5720                break;
5721            }
5722            stored_at.push(layer.head_len(0));
5723            layers.push(crate::gpu_metal::ChunkLayer {
5724                model: &model,
5725                kv_id: self.graph_kv_id,
5726                layer: li,
5727                wq: pq,
5728                wk: pk,
5729                wv: pv,
5730                wo: po,
5731                gate: pg,
5732                up: pu,
5733                down: pd,
5734                input_norm: &lw.input_norm,
5735                post_norm: &lw.post_norm,
5736                bias: bias
5737                    .as_ref()
5738                    .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
5739                q_norm: q_norm.as_deref(),
5740                k_norm: k_norm.as_deref(),
5741                inv_freq: &inv_freq,
5742                rd: self.rotary_dim,
5743                nh,
5744                nkv,
5745                hd,
5746                hs,
5747                inter: d.gate_proj.rows(),
5748                gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
5749                eps: self.rms_eps as f32,
5750            });
5751        }
5752        if layers.is_empty() {
5753            return li0;
5754        }
5755        let row = nkv * hd;
5756        let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
5757            .iter()
5758            .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
5759            .collect();
5760        let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
5761        for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
5762            let li = layers[i].layer;
5763            let layer = &self.kv_cache.layers[li];
5764            io.push(crate::gpu_metal::ChunkIo {
5765                cpu_stored: stored_at[i],
5766                cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
5767                cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
5768                out_k: ok,
5769                out_v: ov,
5770                imp: oi,
5771            });
5772        }
5773        let n_run = layers.len();
5774        let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
5775        // Device-side embedding when the run starts the model and the
5776        // embedding matrix is q8_row-mapped.
5777        let ep = embed_ids.and_then(|ids| {
5778            self.weights
5779                .embed_tokens
5780                .q8_row_parts()
5781                .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
5782                    idx,
5783                    rows,
5784                    row_scale: rs,
5785                    ids,
5786                    mult: self.embed_multiplier,
5787                })
5788        });
5789        if embed_ids.is_some() && ep.is_none() {
5790            return li0;
5791        }
5792        if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
5793            return li0;
5794        }
5795        drop(io);
5796        drop(layers);
5797        // CPU caches stay the owners of record: append the chunk rows
5798        // and bank the importance masses per layer.
5799        for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
5800            let li = li0 + i;
5801            let layer = &mut self.kv_cache.layers[li];
5802            for bi in 0..b {
5803                layer.append(
5804                    &ok[bi * row..(bi + 1) * row],
5805                    &ov[bi * row..(bi + 1) * row],
5806                    &[],
5807                );
5808            }
5809            layer.accumulate_imp(oi);
5810        }
5811        last
5812    }
5813
5814    /// Is layer `li` a sliding-window (local-RoPE) layer? Gemma-3:
5815    /// every `pattern`-th layer is global, the rest are local.
5816    fn layer_is_local(&self, li: usize) -> bool {
5817        if let Some(layers) = &self.sliding_layers {
5818            return layers.get(li).copied().unwrap_or(false);
5819        }
5820        match self.swa {
5821            Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
5822            None => false,
5823        }
5824    }
5825
5826    /// The RoPE table for layer `li` (local layers may have their own;
5827    /// Gemma-4 global layers use the proportional padded table).
5828    fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
5829        if self.layer_is_local(li) {
5830            if let Some(f) = &self.inv_freq_local {
5831                return f.clone();
5832            }
5833        } else if let Some(f) = &self.inv_freq_global {
5834            return f.clone();
5835        }
5836        self.inv_freq.clone()
5837    }
5838
5839    /// The attend window for layer `li` (None = full context).
5840    fn layer_window(&self, li: usize) -> Option<usize> {
5841        self.swa
5842            .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
5843    }
5844
5845    fn layer_num_heads(&self, li: usize) -> usize {
5846        self.attention_heads_per_layer
5847            .as_ref()
5848            .and_then(|v| v.get(li).copied())
5849            .unwrap_or(self.num_heads)
5850    }
5851
5852    fn layer_rope_scale(&self, li: usize) -> f32 {
5853        if self.layer_is_local(li) {
5854            self.rope_scale_local
5855        } else {
5856            self.rope_scale
5857        }
5858    }
5859
5860    /// Attention geometry of layer `li`: (num_kv_heads, head_dim,
5861    /// rotary_dim). Gemma-4 global layers override all three.
5862    fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
5863        if !self.layer_is_local(li) {
5864            if let Some((ghd, gkv)) = self.global_attn {
5865                return (gkv, ghd, ghd);
5866            }
5867        }
5868        (
5869            self.num_kv_heads,
5870            self.head_dim,
5871            if self.layer_is_local(li) {
5872                self.rotary_dim_local.unwrap_or(self.rotary_dim)
5873            } else {
5874                self.rotary_dim
5875            },
5876        )
5877    }
5878
5879    /// Forward one position through all layers (hybrid dispatch).
5880    fn forward_layers(
5881        &mut self,
5882        hidden: &[f32],
5883        position: usize,
5884        task_mask: Option<&TaskMask>,
5885    ) -> Vec<f32> {
5886        self.forward_layers_upto(hidden, position, task_mask, None)
5887    }
5888
5889    // ── Network pipeline-split building blocks (coordinator/worker) ──
5890    // A remote worker owns layers [from ..= upto] and their KV; the
5891    // coordinator owns the rest plus embed / final norm / head. Attention
5892    // causality is per-layer, so a whole prompt's boundary hiddens ship
5893    // as one batch and decode ships one vector per token.
5894
5895    /// Embed one token id (embed multiplier applied).
5896    pub fn embed_id(&self, id: u32) -> Vec<f32> {
5897        self.embed_single(id)
5898    }
5899
5900    /// Refuse the archs/modes whose forward cannot be cut at a layer
5901    /// boundary. Loud by design: a split that silently changed the math
5902    /// would be a chimera.
5903    pub fn split_supported(&self) -> Result<(), String> {
5904        if self.dsv4.is_some() {
5905            return Err(
5906                "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
5907            );
5908        }
5909        if self.g3n.is_some() {
5910            return Err(
5911                "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
5912            );
5913        }
5914        Ok(())
5915    }
5916
5917    /// Forward `hidden` through layers [from ..= upto] at `position`,
5918    /// appending those layers' KV/state. Both split sides call this
5919    /// over their own range; a task mask applies to the span's own
5920    /// layers (each side masks what it runs).
5921    pub fn forward_span(
5922        &mut self,
5923        hidden: &[f32],
5924        position: usize,
5925        from: usize,
5926        upto: usize,
5927        task_mask: Option<&TaskMask>,
5928    ) -> Result<Vec<f32>, String> {
5929        self.split_supported()?;
5930        if from > upto || upto >= self.num_layers {
5931            return Err(format!(
5932                "forward_span: layer range {from}..={upto} outside 0..{}",
5933                self.num_layers
5934            ));
5935        }
5936        if hidden.len() != self.hidden_size {
5937            return Err(format!(
5938                "forward_span: hidden len {} ≠ hidden_size {}",
5939                hidden.len(),
5940                self.hidden_size
5941            ));
5942        }
5943        Ok(self.forward_layers_span(hidden, position, task_mask, from, Some(upto)))
5944    }
5945
5946    /// Final norm + lm_head over a boundary hidden (the final-logit
5947    /// softcap is applied by lm_head_forward itself).
5948    pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
5949        let normed = inference::rms_norm(
5950            hidden,
5951            &self.weights.final_norm,
5952            self.rms_eps,
5953            self.norm_style,
5954        );
5955        self.lm_head_forward(&normed)
5956    }
5957
5958    /// Sample the next token with this pipeline's sampler state.
5959    pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
5960        sampler::sample_with_scratch(
5961            logits,
5962            &self.sampler_config,
5963            past_tokens,
5964            &mut self.rng,
5965            &mut self.sampler_scratch,
5966        )
5967    }
5968
5969    /// Fresh sequence: clear KV, reuse history and device mirrors.
5970    pub fn reset_session(&mut self) {
5971        self.kv_cache.clear();
5972        self.kv_history.clear();
5973        crate::gpu::graph_kv_reset(self.graph_kv_id);
5974    }
5975
5976    /// Batched span prefill from token ids (coordinator side): embed +
5977    /// layers [0 ..= upto]; returns the boundary hiddens of ALL positions
5978    /// (ids.len() × hidden). Rides the same layer-major machinery as the
5979    /// local prefill; falls back to the per-position walk under
5980    /// CMF_PREFILL=seq.
5981    pub fn prefill_span_ids(
5982        &mut self,
5983        ids: &[u32],
5984        start_pos: usize,
5985        upto: usize,
5986        task_mask: Option<&TaskMask>,
5987    ) -> Result<Vec<f32>, String> {
5988        self.split_supported()?;
5989        if upto >= self.num_layers {
5990            return Err(format!(
5991                "prefill_span_ids: upto {upto} outside 0..{}",
5992                self.num_layers
5993            ));
5994        }
5995        // Same predicate as the whole-stack prefill: a span whose GDN
5996        // state lives on the device must walk positions through the
5997        // graph, not through the batched CPU span.
5998        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
5999            Ok(self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1))
6000        } else {
6001            let hs = self.hidden_size;
6002            let mut out = Vec::with_capacity(ids.len() * hs);
6003            for (i, &id) in ids.iter().enumerate() {
6004                let emb = self.embed_id(id);
6005                out.extend_from_slice(&self.forward_span(
6006                    &emb,
6007                    start_pos + i,
6008                    0,
6009                    upto,
6010                    task_mask,
6011                )?);
6012            }
6013            Ok(out)
6014        }
6015    }
6016
6017    /// Batched span prefill from boundary hiddens (worker side): layers
6018    /// [from ..= upto] for every position in the batch; returns the batch.
6019    pub fn prefill_span_hidden(
6020        &mut self,
6021        hidden: &[f32],
6022        start_pos: usize,
6023        from: usize,
6024        upto: usize,
6025        task_mask: Option<&TaskMask>,
6026    ) -> Result<Vec<f32>, String> {
6027        self.split_supported()?;
6028        let hs = self.hidden_size;
6029        if hidden.is_empty() || hidden.len() % hs != 0 {
6030            return Err(format!(
6031                "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
6032                hidden.len()
6033            ));
6034        }
6035        if from > upto || upto >= self.num_layers {
6036            return Err(format!(
6037                "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
6038                self.num_layers
6039            ));
6040        }
6041        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
6042            Ok(self.prefill_batch_span(
6043                PrefillIn::Hidden(hidden),
6044                start_pos,
6045                task_mask,
6046                from,
6047                upto + 1,
6048            ))
6049        } else {
6050            let b = hidden.len() / hs;
6051            let mut out = Vec::with_capacity(hidden.len());
6052            for i in 0..b {
6053                let h = self.forward_span(
6054                    &hidden[i * hs..(i + 1) * hs],
6055                    start_pos + i,
6056                    from,
6057                    upto,
6058                    task_mask,
6059                )?;
6060                out.extend_from_slice(&h);
6061            }
6062            Ok(out)
6063        }
6064    }
6065
6066    /// Build the whole-token wgpu graph for a pure-attention q1 model (every
6067    /// layer Full q1 + dense q1 FFN, no gate/bias). Returns the post-stack
6068    /// hidden (caller does final norm + lm_head), or None to fall back.
6069    fn try_token_graph_wgpu(
6070        &self,
6071        hidden: &[f32],
6072        position: usize,
6073        logits_out: &mut Vec<f32>,
6074        layers_run: &mut usize,
6075    ) -> Option<Vec<f32>> {
6076        self.try_token_graph_wgpu_steps(
6077            hidden,
6078            position,
6079            logits_out,
6080            1,
6081            None,
6082            Some(layers_run),
6083            0,
6084            self.num_layers,
6085        )
6086    }
6087
6088    /// The span twin (network split): the graph covers [from..upto_excl)
6089    /// — one submit per SEGMENT per token. lm_head folds in only when
6090    /// the span reaches the last layer.
6091    fn try_token_graph_wgpu_span(
6092        &self,
6093        hidden: &[f32],
6094        position: usize,
6095        logits_out: &mut Vec<f32>,
6096        from: usize,
6097        upto_excl: usize,
6098        layers_run: &mut usize,
6099    ) -> Option<Vec<f32>> {
6100        self.try_token_graph_wgpu_steps(
6101            hidden,
6102            position,
6103            logits_out,
6104            1,
6105            None,
6106            Some(layers_run),
6107            from,
6108            upto_excl,
6109        )
6110    }
6111
6112    /// Greedy burst: forward `t_next` and let the device pick + re-embed
6113    /// the next k−1 tokens — k frames, ONE submit, k ids back. The ZML
6114    /// trade, on wgpu. None ⇒ caller keeps the per-token path.
6115    fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
6116        if self.o1_active() || self.attn_softcap > 0.0 {
6117            return None;
6118        }
6119        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
6120        if !graph_on || crate::gpu::graph_unsupported() {
6121            // Same memo as the decode site: this path builds the very
6122            // same graph, so a model it cannot build for must not be
6123            // walked again here either. Missing this guard was worth
6124            // 2.5x on an Adreno — 0.361 tok/s against 0.905 — because
6125            // the burst retried per token what decode had already given
6126            // up on.
6127            return None;
6128        }
6129        let emb = self.embed_single(t_next);
6130        let mut lg = Vec::new();
6131        let mut ids = Vec::new();
6132        self.try_token_graph_wgpu_steps(
6133            &emb,
6134            position,
6135            &mut lg,
6136            k,
6137            Some(&mut ids),
6138            None,
6139            0,
6140            self.num_layers,
6141        )?;
6142        (ids.len() == k).then_some(ids)
6143    }
6144
6145    /// Multi-step greedy: k whole frames in ONE submit, argmax and re-embed
6146    /// on the device. `ids_out` receives the k winner ids; the hidden/logits
6147    /// outputs are NOT produced in that mode.
6148    fn try_token_graph_wgpu_steps(
6149        &self,
6150        hidden: &[f32],
6151        position: usize,
6152        logits_out: &mut Vec<f32>,
6153        steps: usize,
6154        ids_out: Option<&mut Vec<u32>>,
6155        layers_run: Option<&mut usize>,
6156        from: usize,
6157        upto_excl: usize,
6158    ) -> Option<Vec<f32>> {
6159        // O(1) Nyström decode runs off the sealed state, not the KV cache the
6160        // graph mirrors — never take the graph while o1 is active.
6161        let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
6162        if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
6163            // Softcapped scores have no graph kernel yet — CPU owns them.
6164            // o1 rides the graph only behind CMF_O1_GPU=1 while the port
6165            // proves itself; without it the CPU path owns o1 as before.
6166            return None;
6167        }
6168        // Per-layer sealed o1 state for the graph. During prefill the
6169        // state is still Collecting -> views are None -> the graph
6170        // refuses below and the CPU prefill records the q trace and
6171        // seals, exactly as the o1 design requires.
6172        let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
6173            .map(|li| {
6174                if !o1_gpu {
6175                    return None;
6176                }
6177                self.kv_cache.layers[self.phys_layer(li)].o1_views()
6178            })
6179            .collect();
6180        if self.o1_active() && o1_gpu {
6181            // Any o1 layer not sealed (or degenerate exact-only) keeps the
6182            // whole token on the CPU: half-graph forwards would desync.
6183            let want: usize = (from..upto_excl)
6184                .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
6185                .count();
6186            let have = o1_views.iter().filter(|v| v.is_some()).count();
6187            if want == 0 || have != want {
6188                // The silent twin of the gpu-side o1 gates, found the
6189                // same way: a 15x decode drop with an empty log. Views
6190                // stay None until the layer's state SEALS, so `have`
6191                // lagging `want` early in a run is the o1 design working
6192                // — but it must say so, or the next reader spends a
6193                // night proving the kernels innocent.
6194                // On CHANGE, not once: the first decline is the legal
6195                // unsealed prefill, and a once-print buries the state
6196                // that matters — what the count reads AFTER the seal.
6197                use std::sync::atomic::{AtomicUsize, Ordering};
6198                static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
6199                let code = have * 1000 + want;
6200                if LAST.swap(code, Ordering::Relaxed) != code {
6201                    tracing::warn!(
6202                        "o1 graph: {have} of {want} layers sealed — per-op until all seal"
6203                    );
6204                }
6205                return None;
6206            }
6207        }
6208        let nh = self.num_heads;
6209        let (nkv, hd, rd) = self.layer_geom(0);
6210        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6211        let mut layers = Vec::with_capacity(upto_excl - from);
6212        let mut model = None;
6213        let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
6214        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6215            if let Some((_, i, kind, rs)) = t.graph_weight() {
6216                return Some(crate::gpu::GraphW {
6217                    idx: i,
6218                    kind,
6219                    row_scale: rs,
6220                    data: &[],
6221                });
6222            }
6223            // Small unquantized projections (GDN in_proj_a/b) stay f32.
6224            t.as_f32().map(|d| crate::gpu::GraphW {
6225                idx: 0,
6226                kind: 4,
6227                row_scale: &[],
6228                data: d,
6229            })
6230        }
6231        for li in from..upto_excl {
6232            let lw = &self.weights.layers[self.phys_layer(li)];
6233            if dbg {
6234                let ak = match &lw.attn {
6235                    AttnKind::Mla(_) => "Mla".into(),
6236                    AttnKind::Full {
6237                        output_gate, bias, ..
6238                    } => format!("Full gate={output_gate} bias={}", bias.is_some()),
6239                    AttnKind::LinearGdn(_) => "LinearGdn".into(),
6240                    AttnKind::Kda(_) => "Kda".into(),
6241                    AttnKind::Linear(_) => "Linear".into(),
6242                    AttnKind::ShortConv(_) => "ShortConv".into(),
6243                };
6244                let fk = match &lw.ffn {
6245                    FfnKind::Dense(_) => "Dense",
6246                    FfnKind::Moe(_) => "Moe",
6247                    FfnKind::DenseMoe(_) => "DenseMoe",
6248                };
6249                eprintln!("graph L{li}: attn={ak} ffn={fk}");
6250            }
6251            let gffn = match &lw.ffn {
6252                FfnKind::DenseMoe(_) => return None, // dual branch: CPU path
6253                // A tube layer is several matrices, not one — the
6254                // whole-layer graph has no shape for it yet.
6255                FfnKind::Dense(d) if !d.segs.is_empty() => return None,
6256                FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
6257                    gate: gw(&d.gate_proj)?,
6258                    up: gw(&d.up_proj)?,
6259                    down: gw(&d.down_proj)?,
6260                },
6261                FfnKind::Moe(m) => {
6262                    // Adaptive τ and expert masks keep the CPU path, where
6263                    // they are implemented; so does a routed scale ≠ 1 (rare,
6264                    // and folding it into the select kernel is not written).
6265                    // Sigmoid routing with a selection bias (LFM2-MoE /
6266                    // DeepSeek noaux_tc) IS graphed — before it was, every
6267                    // LFM2-MoE token fell to the per-op path whole.
6268                    if m.route_tau.is_some()
6269                        || m.mask.is_some()
6270                        || (m.routed_scaling - 1.0).abs() > 1e-9
6271                    {
6272                        return None;
6273                    }
6274                    let shared = m.shared.as_ref();
6275                    let has_shared = shared.is_some();
6276                    let sgate = match shared {
6277                        Some((_, sg)) => gw(sg.as_ref()?)?,
6278                        // Unused by the kernel when has_shared is false; the
6279                        // router weight stands in so the plumbing stays total.
6280                        None => gw(&m.router)?,
6281                    };
6282                    let router = gw(&m.router)?;
6283                    let inter = m.experts.first()?.gate_proj.rows();
6284                    let mut experts = Vec::with_capacity(m.experts.len() + 1);
6285                    // q4t or q4tp, but not both in one layer — the kernels
6286                    // are picked per layer, not per expert.
6287                    let mut q4tp: Option<bool> = None;
6288                    // The mixed 2-bit profile: q2tp gate/up over a q4tp
6289                    // down. Uniform across the layer, like `q4tp` itself.
6290                    let mut gu_q2: Option<bool> = None;
6291                    for e in m
6292                        .experts
6293                        .iter()
6294                        .chain(shared.map(|(se, _)| se))
6295                    {
6296                        if !matches!(e.act, Act::Silu)
6297                            || e.gate_proj.rows() != inter
6298                            || e.up_proj.rows() != inter
6299                        {
6300                            return None;
6301                        }
6302                        let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
6303                            Some((mm, gi)) => (
6304                                mm,
6305                                gi,
6306                                e.up_proj.mapped_q4t()?.1,
6307                                e.down_proj.mapped_q4t()?.1,
6308                                false,
6309                                false,
6310                            ),
6311                            None => match e.gate_proj.mapped_q2tp() {
6312                                Some((mm, gi)) => (
6313                                    mm,
6314                                    gi,
6315                                    e.up_proj.mapped_q2tp()?.1,
6316                                    e.down_proj.mapped_q4tp()?.1,
6317                                    true,
6318                                    true,
6319                                ),
6320                                None => {
6321                                    let (mm, gi) = e.gate_proj.mapped_q4tp()?;
6322                                    (
6323                                        mm,
6324                                        gi,
6325                                        e.up_proj.mapped_q4tp()?.1,
6326                                        e.down_proj.mapped_q4tp()?.1,
6327                                        true,
6328                                        false,
6329                                    )
6330                                }
6331                            },
6332                        };
6333                        if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
6334                        {
6335                            // The shared expert rides in the same packed
6336                            // buffer as the routed ones, so a layer that
6337                            // mixes layouts cannot be indexed by one stride.
6338                            // Say so: the symptom is a whole model quietly
6339                            // running its MoE on the CPU.
6340                            tracing::warn!(
6341                                "MoE layer mixes expert layouts (q4tp={is_p}, q2tp gate/up={is_q2})                                  — every expert of a layer, INCLUDING the shared one, must share                                  a layout. The whole-token graph declines this layer."
6342                            );
6343                            return None;
6344                        }
6345                        model.get_or_insert_with(|| mm.clone());
6346                        experts.push((gi, ui, di));
6347                    }
6348                    crate::gpu::GraphFfn::Moe {
6349                        router,
6350                        shared_gate: sgate,
6351                        experts,
6352                        n_exp: m.experts.len(),
6353                        // CMF_TOPK_PROBE: timing probe only — output is WRONG.
6354                        // Fewer experts shrink the MoE arithmetic while the
6355                        // dispatch count stays identical, which is the only
6356                        // clean way to tell a launch-bound decode from a
6357                        // compute-bound one.
6358                        top_k: std::env::var("CMF_TOPK_PROBE")
6359                            .ok()
6360                            .and_then(|v| v.parse::<usize>().ok())
6361                            .filter(|k| *k > 0 && *k <= m.top_k)
6362                            .unwrap_or(m.top_k),
6363                        inter,
6364                        norm_topk: m.norm_topk_prob,
6365                        q4tp: q4tp?,
6366                        gu_q2: gu_q2.unwrap_or(false),
6367                        sigmoid: m.router_sigmoid,
6368                        bias: m.expert_bias.as_deref(),
6369                        has_shared,
6370                    }
6371                }
6372            };
6373            let attn = match &lw.attn {
6374                AttnKind::Full {
6375                    wq,
6376                    wk,
6377                    wv,
6378                    wo,
6379                    q_norm,
6380                    k_norm,
6381                    output_gate,
6382                    softplus_gate,
6383                    bias,
6384                } => {
6385                    if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
6386                        return None;
6387                    }
6388                    let (m, _, _, _) = wq.graph_weight()?;
6389                    model = Some(m.clone());
6390                    crate::gpu::GraphAttn::Full {
6391                        wq: gw(wq)?,
6392                        wk: gw(wk)?,
6393                        wv: gw(wv)?,
6394                        wo: gw(wo)?,
6395                        q_norm: q_norm.as_deref(),
6396                        k_norm: k_norm.as_deref(),
6397                        bias: bias
6398                            .as_ref()
6399                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6400                        output_gate: *output_gate,
6401                        cpu_k: self.kv_cache.layers[li].k_heads(),
6402                        cpu_v: self.kv_cache.layers[li].v_heads(),
6403                    }
6404                }
6405                AttnKind::LinearGdn(w) => {
6406                    let cfg = self.gdn_cfg?;
6407                    let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
6408                    model = Some(m.clone());
6409                    crate::gpu::GraphAttn::Gdn {
6410                        qkv: gw(&w.in_proj_qkv)?,
6411                        z: gw(&w.in_proj_z)?,
6412                        a: gw(&w.in_proj_a)?,
6413                        b: gw(&w.in_proj_b)?,
6414                        out: gw(&w.out_proj)?,
6415                        conv1d: &w.conv1d,
6416                        a_log: &w.a_log,
6417                        dt_bias: &w.dt_bias,
6418                        norm: &w.norm,
6419                        nv: cfg.num_v_heads,
6420                        nk: cfg.num_k_heads,
6421                        dk: cfg.key_head_dim,
6422                        dv: cfg.value_head_dim,
6423                        kk: cfg.conv_kernel,
6424                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
6425                    }
6426                }
6427                AttnKind::ShortConv(w) => {
6428                    let cfg = self.short_conv_cfg?;
6429                    let (m, _, _, _) = w.in_proj.graph_weight()?;
6430                    model = Some(m.clone());
6431                    crate::gpu::GraphAttn::ShortConv {
6432                        inp: gw(&w.in_proj)?,
6433                        out: gw(&w.out_proj)?,
6434                        taps: &w.conv,
6435                        kernel: cfg.kernel,
6436                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
6437                    }
6438                }
6439                _ => return None,
6440            };
6441            layers.push(crate::gpu::GraphLayer {
6442                input_norm: &lw.input_norm,
6443                attn,
6444                post_norm: &lw.post_norm,
6445                ffn: gffn,
6446            });
6447        }
6448        let model = model?;
6449        // Fold final-norm + lm_head into the graph when this call wants logits
6450        // and the lm_head is a graphable (quantized) weight — the graph then
6451        // reads back logits (into logits_out) instead of the hidden, dropping
6452        // the separate CPU/GPU lm_head op + its sync. Never the f32 fallback:
6453        // an unquantized lm_head is vocab·hidden and must not be uploaded.
6454        let lm_gw = if upto_excl == self.num_layers
6455            && self.graph_want_logits
6456            && std::env::var("CMF_GPU_LMHEAD")
6457                .map(|v| v != "0")
6458                .unwrap_or(true)
6459        {
6460            self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
6461                (
6462                    crate::gpu::GraphW {
6463                        idx: i,
6464                        kind,
6465                        row_scale: rs,
6466                        data: &[],
6467                    },
6468                    self.weights.lm_head.rows(),
6469                )
6470            })
6471        } else {
6472            None
6473        };
6474        let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
6475        // Multi-step re-embeds the winner on the device.
6476        let emb_gw = if steps > 1 {
6477            self.weights
6478                .embed_tokens
6479                .graph_weight()
6480                .map(|(_, i, kind, rs)| {
6481                    (
6482                        crate::gpu::GraphW {
6483                            idx: i,
6484                            kind,
6485                            row_scale: rs,
6486                            data: &[],
6487                        },
6488                        self.weights.embed_tokens.rows(),
6489                        self.embed_multiplier,
6490                    )
6491                })
6492        } else {
6493            None
6494        };
6495
6496        // Loop boundaries: virtual layer indices after which final_norm is
6497        // applied (mid-stack only; the GLOBAL last layer's norm folds into
6498        // lm_head). Span-relative — the executor compares its enumerate
6499        // index. A span ending mid-stack keeps its boundary norm even when
6500        // it is the span's own last layer.
6501        let loop_norm_at: Vec<usize> = if self.loop_final_norm {
6502            (from..upto_excl.min(self.num_layers - 1))
6503                .filter(|&li| (li + 1) % self.physical_layers == 0)
6504                .map(|li| li - from)
6505                .collect()
6506        } else {
6507            Vec::new()
6508        };
6509        let mut h = hidden.to_vec();
6510        crate::gpu::forward_token_graph(
6511            &model,
6512            self.graph_kv_id,
6513            &layers,
6514            &o1_views,
6515            self.o1_epoch,
6516            &self.inv_freq,
6517            &mut h,
6518            nh,
6519            nkv,
6520            hd,
6521            rd,
6522            self.hidden_size,
6523            self.intermediate_size,
6524            position,
6525            self.kv_cache.max_seq_len,
6526            gemma,
6527            self.rms_eps as f32,
6528            lm,
6529            &self.weights.final_norm,
6530            logits_out,
6531            &loop_norm_at,
6532            steps,
6533            emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
6534            ids_out,
6535            layers_run,
6536            from,
6537            false,
6538        )
6539        .then_some(h)
6540    }
6541
6542    /// Batched prefill: k contiguous prompt positions through the whole wgpu
6543    /// graph in ONE submit (projections/FFN as GEMMs). `hiddens` is [k·hidden]
6544    /// in/out (embeddings in, layer output out); KV mirror / GDN state advance.
6545    /// false ⇒ unsupported → caller keeps the per-position graph.
6546    /// The b-row Metal graph plan for the whole model: every layer as a
6547    /// GDN run or a full-attention item, all-or-nothing (a layer outside the
6548    /// graph's contract → None, the caller runs plain). Shared by the
6549    /// speculative verify and the batched prefill.
6550    #[cfg(target_os = "macos")]
6551    #[allow(clippy::type_complexity)]
6552    fn metal_rows_plan(&self) -> Option<(Vec<MetalRowsItem<'_>>, std::sync::Arc<cortiq_core::CmfModel>, Option<crate::gpu_metal::GdnGpuCfg>)> {
6553        use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
6554        if !crate::gpu::q1_force()
6555            || !crate::gpu::enabled_here()
6556            || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
6557            || self.attn_softcap > 0.0
6558            || self.o1_active()
6559            || self.swa.is_some()
6560            || self.global_attn.is_some()
6561            || self.attention_heads_per_layer.is_some()
6562            || self.attn_v_norm
6563            || self.loop_final_norm
6564            || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
6565        {
6566            return None;
6567        }
6568        let attend_contract = self.head_dim % 4 == 0
6569            && self.head_dim <= 256
6570            && self.rotary_dim >= 2
6571            && self.rotary_dim <= self.head_dim
6572            && (self.rotary_dim / 2) % 32 == 0
6573            && self.num_kv_heads > 0
6574            && self.num_heads % self.num_kv_heads == 0;
6575        if !attend_contract {
6576            return None;
6577        }
6578        let mut plan: Vec<MetalRowsItem> = Vec::new();
6579        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
6580        for li in 0..self.num_layers {
6581            let lw = &self.weights.layers[self.phys_layer(li)];
6582            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
6583                return None;
6584            }
6585            let ffn = match &lw.ffn {
6586                FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
6587                    let (Some(g), Some(u), Some(dn)) =
6588                        (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts())
6589                    else {
6590                        return None;
6591                    };
6592                    MetalFfn::Dense { gate: g, up: u, down: dn }
6593                }
6594                _ => return None,
6595            };
6596            match &lw.attn {
6597                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
6598                    let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
6599                        w.in_proj_qkv.q1_parts(),
6600                        w.in_proj_z.q1_parts(),
6601                        w.in_proj_a.f32_parts(),
6602                        w.in_proj_b.f32_parts(),
6603                        w.out_proj.q1_parts(),
6604                    ) else {
6605                        return None;
6606                    };
6607                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
6608                        model_ref.get_or_insert_with(|| model.clone());
6609                    }
6610                    let gl = GdnGpuLayer {
6611                        attn_norm: &lw.input_norm,
6612                        post_norm: &lw.post_norm,
6613                        qkv,
6614                        z,
6615                        a,
6616                        b: bb,
6617                        out,
6618                        ffn,
6619                        conv1d: &w.conv1d,
6620                        a_log: &w.a_log,
6621                        dt_bias: &w.dt_bias,
6622                        gnorm: &w.norm,
6623                    };
6624                    match plan.last_mut() {
6625                        Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
6626                        _ => plan.push(MetalRowsItem::Gdn { run: vec![gl], first: li }),
6627                    }
6628                }
6629                AttnKind::Full {
6630                    wq,
6631                    wk,
6632                    wv,
6633                    wo,
6634                    q_norm,
6635                    k_norm,
6636                    output_gate,
6637                    softplus_gate: None,
6638                    bias: None,
6639                } => {
6640                    let (Some(pq), Some(pk), Some(pv), Some(po)) =
6641                        (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
6642                    else {
6643                        return None;
6644                    };
6645                    if let QTensor::Mapped { model, .. } = wq {
6646                        model_ref.get_or_insert_with(|| model.clone());
6647                    }
6648                    let cache = &self.kv_cache.layers[li];
6649                    if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
6650                        return None;
6651                    }
6652                    plan.push(MetalRowsItem::Attn {
6653                        l: AttnGpuLayer {
6654                            attn_norm: &lw.input_norm,
6655                            post_norm: &lw.post_norm,
6656                            wq: pq,
6657                            wk: pk,
6658                            wv: pv,
6659                            wo: po,
6660                            ffn,
6661                        },
6662                        li,
6663                        q_norm: q_norm.as_deref(),
6664                        k_norm: k_norm.as_deref(),
6665                        output_gate: *output_gate,
6666                    });
6667                }
6668                _ => return None,
6669            }
6670        }
6671        let model = model_ref?;
6672        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
6673            nv: cfg.num_v_heads,
6674            nk: cfg.num_k_heads,
6675            dk: cfg.key_head_dim,
6676            dv: cfg.value_head_dim,
6677            kk: cfg.conv_kernel,
6678            hidden: self.hidden_size,
6679            inter: self.intermediate_size,
6680            c_dim: cfg.conv_dim(),
6681            eps: cfg.rms_eps as f32,
6682            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6683        });
6684        Some((plan, model, gcfg))
6685    }
6686
6687    /// `AttnDeviceParams` for a plan item over the CPU cache as it stands.
6688    #[cfg(target_os = "macos")]
6689    #[allow(clippy::too_many_arguments)]
6690    fn metal_attn_params<'a>(
6691        li: usize,
6692        cache: &'a crate::kv_cache::LayerKvCache,
6693        q_norm: Option<&'a [f32]>,
6694        k_norm: Option<&'a [f32]>,
6695        output_gate: bool,
6696        inv_freq: &'a [f32],
6697        geom: (usize, usize, usize, usize),
6698        pos0: usize,
6699        kv_id: u64,
6700        eps: f32,
6701        gemma: bool,
6702    ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
6703        let (nh, nkv, hd, rd) = geom;
6704        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
6705        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
6706        let cpu_stored = cpu_k[0].len() / hd;
6707        (
6708            crate::gpu_metal::AttnDeviceParams {
6709                kv_id,
6710                layer: li,
6711                nh,
6712                nkv,
6713                hd,
6714                rd,
6715                position: pos0,
6716                eps,
6717                gemma,
6718                output_gate,
6719                q_norm,
6720                k_norm,
6721                inv_freq,
6722                cpu_k,
6723                cpu_v,
6724                cpu_stored,
6725                o1: None,
6726            },
6727            cpu_stored,
6728        )
6729    }
6730
6731    /// Run the rows plan over `hiddens` (b rows at `pos0..`): validate,
6732    /// encode every item, optionally the head, sync. Returns the graph
6733    /// (for the commit / state finish) plus the GDN layer indices and the
6734    /// attention layers with the row count they were encoded against.
6735    #[cfg(target_os = "macos")]
6736    #[allow(clippy::type_complexity)]
6737    fn metal_rows_run(
6738        &mut self,
6739        hiddens: &mut [f32],
6740        pos0: usize,
6741        b: usize,
6742        prefill: bool,
6743        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
6744    ) -> Option<MetalVerifyPending> {
6745        use crate::gpu_metal::{GraphDims, VerifyGraph};
6746        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
6747        for l in &mut self.kv_cache.layers {
6748            if l.linear_state.len() != want && want > 0 {
6749                l.linear_state = vec![0f32; want];
6750            }
6751        }
6752        let (plan, model, gcfg) = self.metal_rows_plan()?;
6753        let dims = GraphDims {
6754            hidden: self.hidden_size,
6755            eps: self.rms_eps as f32,
6756            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6757        };
6758        let mut graph = if prefill {
6759            VerifyGraph::new_prefill(&model, dims, hiddens, b)?
6760        } else {
6761            VerifyGraph::new(&model, dims, hiddens, b)?
6762        };
6763        let geom = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
6764        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6765        let eps = self.rms_eps as f32;
6766        let kv_id = self.graph_kv_id;
6767        let inv_freq = self.inv_freq.clone();
6768        for item in &plan {
6769            let ok = match item {
6770                MetalRowsItem::Gdn { run, .. } => gcfg
6771                    .as_ref()
6772                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
6773                    .unwrap_or(false),
6774                MetalRowsItem::Attn { l, li, q_norm, k_norm, output_gate } => {
6775                    let (p, _) = Self::metal_attn_params(*li, &self.kv_cache.layers[*li], *q_norm, *k_norm, *output_gate, &inv_freq, geom, pos0, kv_id, eps, gemma);
6776                    graph.attn_ok(l, &p)
6777                }
6778            };
6779            if !ok {
6780                use std::sync::atomic::{AtomicBool, Ordering};
6781                static SAID: AtomicBool = AtomicBool::new(false);
6782                if !SAID.swap(true, Ordering::Relaxed) {
6783                    tracing::warn!("metal rows graph: a layer failed preflight — declining");
6784                }
6785                return None;
6786            }
6787        }
6788        let lm = match &spec {
6789            Some((lm, _, _)) => {
6790                if !graph.lm_head_ok(*lm) {
6791                    return None;
6792                }
6793                Some(*lm)
6794            }
6795            None => None,
6796        };
6797        let mut gdn_layers = Vec::new();
6798        let mut attn_layers = Vec::new();
6799        for item in &plan {
6800            match item {
6801                MetalRowsItem::Gdn { run, first } => {
6802                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
6803                        .iter()
6804                        .map(|l| l.linear_state.as_slice())
6805                        .collect();
6806                    if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
6807                        return None;
6808                    }
6809                    gdn_layers.extend(*first..*first + run.len());
6810                }
6811                MetalRowsItem::Attn { l, li, q_norm, k_norm, output_gate } => {
6812                    let (p, cpu_stored) = Self::metal_attn_params(*li, &self.kv_cache.layers[*li], *q_norm, *k_norm, *output_gate, &inv_freq, geom, pos0, kv_id, eps, gemma);
6813                    if !graph.encode_attn_b(l, &p) {
6814                        return None;
6815                    }
6816                    attn_layers.push((*li, cpu_stored));
6817                }
6818            }
6819        }
6820        if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
6821            if !graph.encode_lm_head_b(final_norm, lm) {
6822                return None;
6823            }
6824        }
6825        graph.sync();
6826        if let Some((lm, _, logits)) = spec {
6827            logits.resize(b * lm.1, 0.0);
6828            graph.read_logits(logits);
6829        }
6830        graph.read_hidden(hiddens);
6831        Some(MetalVerifyPending { graph, gdn_layers, attn_layers })
6832    }
6833
6834    /// Native-Metal twin of `try_batch_graph_wgpu`: the b rows through the
6835    /// whole model on the `VerifyGraph` (one submit), the head folded in
6836    /// when `spec` asks; `hiddens` come back as the last layer's output
6837    /// rows, `spec.2` as `[b][lm_rows]` logits. The graph is parked in
6838    /// `metal_verify` for `metal_verify_commit`.
6839    #[cfg(target_os = "macos")]
6840    fn try_batch_graph_metal(
6841        &mut self,
6842        hiddens: &mut [f32],
6843        positions: &[usize],
6844        b: usize,
6845        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
6846    ) -> bool {
6847        let _t0 = std::time::Instant::now();
6848        if positions.len() != b
6849            || positions.windows(2).any(|w| w[1] != w[0] + 1)
6850            || hiddens.len() != b * self.hidden_size
6851        {
6852            return false;
6853        }
6854        let Some(pending) = self.metal_rows_run(hiddens, positions[0], b, false, spec) else {
6855            return false;
6856        };
6857        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6858            eprintln!("metal-verify: {:.1} ms | b={b}", _t0.elapsed().as_secs_f64() * 1e3);
6859        }
6860        self.metal_verify = Some(pending);
6861        true
6862    }
6863
6864    /// Batched prefill on the Metal rows graph: `ids` (≤ 512) at
6865    /// `start_pos..`, states written in place, K/V rows appended to the
6866    /// CPU caches; returns every position's output hidden (`[b][hidden]`).
6867    /// None = the graph declined before touching anything.
6868    #[cfg(target_os = "macos")]
6869    fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> Option<Vec<f32>> {
6870        let b = ids.len();
6871        if b == 0 || b > 512 {
6872            return None;
6873        }
6874        let hs = self.hidden_size;
6875        let mut hiddens = vec![0f32; b * hs];
6876        for (j, &id) in ids.iter().enumerate() {
6877            let e = self.embed_single(id);
6878            hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
6879        }
6880        let mut pending = self.metal_rows_run(&mut hiddens, start_pos, b, true, None)?;
6881        // states are final: copy them to the owners
6882        let idxs = pending.gdn_layers.clone();
6883        let mut outs: Vec<&mut [f32]> = self
6884            .kv_cache
6885            .layers
6886            .iter_mut()
6887            .enumerate()
6888            .filter(|(i, _)| idxs.binary_search(i).is_ok())
6889            .map(|(_, l)| l.linear_state.as_mut_slice())
6890            .collect();
6891        pending.graph.finish_states(&mut outs);
6892        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6893        let mut kbuf = vec![0f32; b * nkv * hd];
6894        let mut vbuf = vec![0f32; b * nkv * hd];
6895        for (li, cpu_stored) in &pending.attn_layers {
6896            if crate::gpu_metal::kv_mirror_read_rows(self.graph_kv_id, *li, nkv, hd, *cpu_stored, b, &mut kbuf, &mut vbuf) {
6897                let cache = &mut self.kv_cache.layers[*li];
6898                for r in 0..b {
6899                    cache.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
6900                }
6901                crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + b);
6902            }
6903        }
6904        Some(hiddens)
6905    }
6906
6907    /// Commit a Metal verify round: replay the GDN recurrences over the
6908    /// `a + 1` accepted positions into the CPU states, append the accepted
6909    /// K/V rows from the mirrors to the CPU caches, re-point the mirrors.
6910    #[cfg(target_os = "macos")]
6911    fn metal_verify_commit(&mut self, a: usize) -> bool {
6912        let Some(mut pending) = self.metal_verify.take() else {
6913            return false;
6914        };
6915        let n = a + 1;
6916        // encode order == ascending layer order (the plan walks 0..layers)
6917        let idxs = pending.gdn_layers.clone();
6918        let mut outs: Vec<&mut [f32]> = self
6919            .kv_cache
6920            .layers
6921            .iter_mut()
6922            .enumerate()
6923            .filter(|(i, _)| idxs.binary_search(i).is_ok())
6924            .map(|(_, l)| l.linear_state.as_mut_slice())
6925            .collect();
6926        if !pending.graph.commit(n, &mut outs) {
6927            return false;
6928        }
6929        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6930        let mut kbuf = vec![0f32; n * nkv * hd];
6931        let mut vbuf = vec![0f32; n * nkv * hd];
6932        for (li, cpu_stored) in &pending.attn_layers {
6933            if crate::gpu_metal::kv_mirror_read_rows(self.graph_kv_id, *li, nkv, hd, *cpu_stored, n, &mut kbuf, &mut vbuf) {
6934                let cache = &mut self.kv_cache.layers[*li];
6935                for r in 0..n {
6936                    cache.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
6937                }
6938                crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + n);
6939            }
6940        }
6941        true
6942    }
6943
6944    /// The round's warm-ups as ONE b-row graph run over the MTP block on
6945    /// Metal: `pairs` = (trunk hidden, next token) at consecutive positions
6946    /// from `first_pos`; the block's input projection is folded in, the
6947    /// appended K/V rows are pulled into the CPU MTP cache. False = the
6948    /// graph declined (nothing appended).
6949    #[cfg(target_os = "macos")]
6950    fn mtp_warm_batch_metal(&mut self, m: &mut MtpModule, pairs: &[(&[f32], u32)], first_pos: usize) -> bool {
6951        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
6952        let b = pairs.len();
6953        if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
6954            return false;
6955        }
6956        let AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, softplus_gate: None, bias: None } = &m.layer.attn else {
6957            return false;
6958        };
6959        let FfnKind::Dense(d) = &m.layer.ffn else { return false };
6960        if !d.segs.is_empty() {
6961            return false;
6962        }
6963        let (Some(pq), Some(pk), Some(pv), Some(po)) = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts()) else {
6964            return false;
6965        };
6966        let (Some(g), Some(u), Some(dn)) = (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts()) else {
6967            return false;
6968        };
6969        let Some(eh) = m.eh_proj.q1_parts() else { return false };
6970        let QTensor::Mapped { model, .. } = wq else { return false };
6971        let model = model.clone();
6972        let hs = self.hidden_size;
6973        // [enorm(embed(tok)); hnorm(hidden)] rows
6974        let mut cat = vec![0f32; b * 2 * hs];
6975        for (j, (h, tok)) in pairs.iter().enumerate() {
6976            let e = self.embed_single(*tok);
6977            let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
6978            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
6979            inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
6980        }
6981        let dims = GraphDims { hidden: hs, eps: self.rms_eps as f32, gemma: self.norm_style == cortiq_core::NormStyle::Gemma };
6982        let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
6983            return false;
6984        };
6985        let l = AttnGpuLayer {
6986            attn_norm: &m.layer.input_norm,
6987            post_norm: &m.layer.post_norm,
6988            wq: pq,
6989            wk: pk,
6990            wv: pv,
6991            wo: po,
6992            ffn: MetalFfn::Dense { gate: g, up: u, down: dn },
6993        };
6994        let (nh, nkv, hd, rd) = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
6995        let inv_freq = self.inv_freq.clone();
6996        let cpu_stored;
6997        {
6998            let cache = &m.kv;
6999            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
7000            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
7001            cpu_stored = cpu_k[0].len() / hd;
7002            if cpu_stored != first_pos {
7003                return false;
7004            }
7005            let p = AttnDeviceParams {
7006                kv_id: self.mtp_kv_id(),
7007                layer: Self::MTP_LAYER_BASE,
7008                nh,
7009                nkv,
7010                hd,
7011                rd,
7012                position: first_pos,
7013                eps: self.rms_eps as f32,
7014                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
7015                output_gate: *output_gate,
7016                q_norm: q_norm.as_deref(),
7017                k_norm: k_norm.as_deref(),
7018                inv_freq: &inv_freq,
7019                cpu_k,
7020                cpu_v,
7021                cpu_stored,
7022                o1: None,
7023            };
7024            if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
7025                return false;
7026            }
7027        }
7028        graph.sync();
7029        let mut kbuf = vec![0f32; b * nkv * hd];
7030        let mut vbuf = vec![0f32; b * nkv * hd];
7031        if !crate::gpu_metal::kv_mirror_read_rows(self.mtp_kv_id(), Self::MTP_LAYER_BASE, nkv, hd, cpu_stored, b, &mut kbuf, &mut vbuf) {
7032            return false;
7033        }
7034        for r in 0..b {
7035            m.kv.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
7036        }
7037        crate::gpu_metal::kv_mirror_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, cpu_stored + b);
7038        true
7039    }
7040
7041    /// Draft-head shortlist size: `CMF_DRAFT_VOCAB` rows (default 65536,
7042    /// capped at the head; 0 = full head).
7043    fn draft_vocab_rows(head_rows: usize) -> usize {
7044        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
7045        let n = *N.get_or_init(|| {
7046            std::env::var("CMF_DRAFT_VOCAB")
7047                .ok()
7048                .and_then(|v| v.parse().ok())
7049                .unwrap_or(65536)
7050        });
7051        if n == 0 { head_rows } else { n.min(head_rows) }
7052    }
7053
7054    /// One MTP block step on the native Metal token graph: block input on
7055    /// the host, the attention layer + FFN device-resident over the MTP
7056    /// mirror, the head folded in when `want_logits`. The appended K/V row
7057    /// is pulled into the CPU MTP cache (owner of record) after the sync.
7058    #[cfg(target_os = "macos")]
7059    fn mtp_step_metal(
7060        &mut self,
7061        m: &mut MtpModule,
7062        hidden: &[f32],
7063        next_token: u32,
7064        position: usize,
7065        want_logits: bool,
7066    ) -> Option<(Vec<f32>, Vec<f32>)> {
7067        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
7068        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
7069            || !crate::gpu::q1_force()
7070            || !crate::gpu::enabled_here()
7071            || self.attn_softcap > 0.0
7072            || self.attention_heads_per_layer.is_some()
7073            || m.kv.mode != crate::kv_cache::KvMode::F32
7074            || m.kv.o1.is_some()
7075        {
7076            return None;
7077        }
7078        let AttnKind::Full {
7079            wq,
7080            wk,
7081            wv,
7082            wo,
7083            q_norm,
7084            k_norm,
7085            output_gate,
7086            softplus_gate: None,
7087            bias: None,
7088        } = &m.layer.attn
7089        else {
7090            return None;
7091        };
7092        let FfnKind::Dense(d) = &m.layer.ffn else { return None };
7093        if d.act != Act::Silu || !d.segs.is_empty() {
7094            return None;
7095        }
7096        let (pq, pk, pv, po) = (wq.q1_parts()?, wk.q1_parts()?, wv.q1_parts()?, wo.q1_parts()?);
7097        let (g, u, dn) = (d.gate_proj.q1_parts()?, d.up_proj.q1_parts()?, d.down_proj.q1_parts()?);
7098        let QTensor::Mapped { model, .. } = wq else { return None };
7099        let model = model.clone();
7100        let lm = if want_logits { Some(self.weights.lm_head.q1_parts()?) } else { None };
7101        let dims = GraphDims {
7102            hidden: self.hidden_size,
7103            eps: self.rms_eps as f32,
7104            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
7105        };
7106        // The block input `eh_proj · [enorm(e); hnorm(h)]` rides in the
7107        // graph (one submit a step); the host per-op matvec if it cannot.
7108        let hs = self.hidden_size;
7109        let mut x = vec![0f32; hs];
7110        let mut graph = TokenGraph::new(&model, dims, &x)?;
7111        let mut folded = false;
7112        if let Some(eh) = m.eh_proj.q1_parts() {
7113            let e = self.embed_single(next_token);
7114            let mut cat = vec![0.0f32; 2 * hs];
7115            let (cat_e, cat_h) = cat.split_at_mut(hs);
7116            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7117            inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7118            folded = graph.encode_input_proj(eh, &cat);
7119        }
7120        if !folded {
7121            x = self.mtp_block_input(m, hidden, next_token);
7122            graph = TokenGraph::new(&model, dims, &x)?;
7123        }
7124        let l = AttnGpuLayer {
7125            attn_norm: &m.layer.input_norm,
7126            post_norm: &m.layer.post_norm,
7127            wq: pq,
7128            wk: pk,
7129            wv: pv,
7130            wo: po,
7131            ffn: MetalFfn::Dense { gate: g, up: u, down: dn },
7132        };
7133        let (nh, nkv, hd, rd) = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
7134        let inv_freq = self.inv_freq.clone();
7135        {
7136            let cache = &m.kv;
7137            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
7138            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
7139            let cpu_stored = cpu_k[0].len() / hd;
7140            let p = AttnDeviceParams {
7141                kv_id: self.mtp_kv_id(),
7142                layer: Self::MTP_LAYER_BASE,
7143                nh,
7144                nkv,
7145                hd,
7146                rd,
7147                position,
7148                eps: self.rms_eps as f32,
7149                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
7150                output_gate: *output_gate,
7151                q_norm: q_norm.as_deref(),
7152                k_norm: k_norm.as_deref(),
7153                inv_freq: &inv_freq,
7154                cpu_k,
7155                cpu_v,
7156                cpu_stored,
7157                o1: None,
7158            };
7159            if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
7160                return None;
7161            }
7162        }
7163        // The draft's head over a vocabulary SHORTLIST (the first
7164        // CMF_DRAFT_VOCAB rows — BPE ids run roughly by merge rank, so the
7165        // low ids carry the mass): the verify keeps the full head, so a true
7166        // token past the cut is only a rejected draft, never a wrong token.
7167        // 662 MB a step on Qwen3.8 becomes 170 MB at 65536.
7168        let draft_rows = if let Some(lm) = lm { Self::draft_vocab_rows(lm.1) } else { 0 };
7169        if let Some(lm) = lm {
7170            if !graph.lm_head_ok(lm) {
7171                return None;
7172            }
7173            if draft_rows < lm.1 {
7174                if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
7175                    return None;
7176                }
7177            } else {
7178                graph.encode_lm_head(&m.final_norm, lm);
7179            }
7180        }
7181        graph.sync();
7182        let mut logits = Vec::new();
7183        if let Some(lm) = lm {
7184            let n_read = draft_rows.min(lm.1).min(self.vocab_size);
7185            logits = attention::take_buf(n_read);
7186            graph.read_logits(&mut logits);
7187            // ids past the shortlist: never drafted (−∞ in every chain)
7188            logits.resize(self.vocab_size, f32::NEG_INFINITY);
7189        }
7190        graph.finish(&mut x);
7191        let mut krow = attention::take_buf(nkv * hd);
7192        let mut vrow = attention::take_buf(nkv * hd);
7193        if crate::gpu_metal::kv_mirror_read_last(self.mtp_kv_id(), Self::MTP_LAYER_BASE, nkv, hd, &mut krow, &mut vrow) {
7194            m.kv.append(&krow, &vrow, &[]);
7195        }
7196        attention::recycle_buf(&mut krow);
7197        attention::recycle_buf(&mut vrow);
7198        Some((logits, x))
7199    }
7200
7201    fn try_batch_graph_wgpu(
7202        &self,
7203        hiddens: &mut [f32],
7204        positions: &[usize],
7205        k: usize,
7206        spec: Option<crate::gpu::SpecTail<'_>>,
7207    ) -> bool {
7208        let _tb = std::time::Instant::now();
7209        if self.attn_softcap > 0.0 {
7210            return false; // capped scores: no graph kernel — CPU path
7211        }
7212        if self.o1_active() {
7213            return false;
7214        }
7215        let nh = self.num_heads;
7216        let (nkv, hd, rd) = self.layer_geom(0);
7217        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7218        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7219            if let Some((_, i, kind, rs)) = t.graph_weight() {
7220                return Some(crate::gpu::GraphW {
7221                    idx: i,
7222                    kind,
7223                    row_scale: rs,
7224                    data: &[],
7225                });
7226            }
7227            t.as_f32().map(|d| crate::gpu::GraphW {
7228                idx: 0,
7229                kind: 4,
7230                row_scale: &[],
7231                data: d,
7232            })
7233        }
7234        let built: Option<(
7235            Vec<crate::gpu::GraphLayer<'_>>,
7236            std::sync::Arc<cortiq_core::CmfModel>,
7237        )> = (|| {
7238            let mut layers = Vec::with_capacity(self.num_layers);
7239            let mut model = None;
7240            for li in 0..self.num_layers {
7241                let lw = &self.weights.layers[self.phys_layer(li)];
7242                // MoE routes per token, so its experts are encoded token by
7243                // token inside the batched submit while attention and the
7244                // projections stay GEMMs. Refusing MoE here is what left
7245                // prefill running one position at a time: 33 tok/s against
7246                // 54 on decode, i.e. reading the prompt was slower than
7247                // writing the answer.
7248                let gffn = match &lw.ffn {
7249                    FfnKind::Dense(d) if !d.segs.is_empty() => return None,
7250                    FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7251                        gate: gw(&d.gate_proj)?,
7252                        up: gw(&d.up_proj)?,
7253                        down: gw(&d.down_proj)?,
7254                    },
7255                    FfnKind::Moe(m) => {
7256                        if m.router_sigmoid
7257                            || m.expert_bias.is_some()
7258                            || m.route_tau.is_some()
7259                            || m.mask.is_some()
7260                        {
7261                            return None;
7262                        }
7263                        let (se, sg) = m.shared.as_ref()?;
7264                        let sgate = gw(sg.as_ref()?)?;
7265                        let router = gw(&m.router)?;
7266                        let inter = m.experts.first()?.gate_proj.rows();
7267                        let mut experts = Vec::with_capacity(m.experts.len() + 1);
7268                        let mut q4tp: Option<bool> = None;
7269                        let mut gu_q2: Option<bool> = None;
7270                        for e in m.experts.iter().chain(std::iter::once(se)) {
7271                            if !matches!(e.act, Act::Silu)
7272                                || e.gate_proj.rows() != inter
7273                                || e.up_proj.rows() != inter
7274                            {
7275                                return None;
7276                            }
7277                            // Same ladder as the token graph: q4t → q2tp
7278                            // (mixed profile: 2-bit gate/up over a q4tp
7279                            // down) → q4tp. Uniform across the layer.
7280                            let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
7281                                Some((mm, gi)) => (
7282                                    mm,
7283                                    gi,
7284                                    e.up_proj.mapped_q4t()?.1,
7285                                    e.down_proj.mapped_q4t()?.1,
7286                                    false,
7287                                    false,
7288                                ),
7289                                None => match e.gate_proj.mapped_q2tp() {
7290                                    Some((mm, gi)) => (
7291                                        mm,
7292                                        gi,
7293                                        e.up_proj.mapped_q2tp()?.1,
7294                                        e.down_proj.mapped_q4tp()?.1,
7295                                        true,
7296                                        true,
7297                                    ),
7298                                    None => {
7299                                        let (mm, gi) = e.gate_proj.mapped_q4tp()?;
7300                                        (
7301                                            mm,
7302                                            gi,
7303                                            e.up_proj.mapped_q4tp()?.1,
7304                                            e.down_proj.mapped_q4tp()?.1,
7305                                            true,
7306                                            false,
7307                                        )
7308                                    }
7309                                },
7310                            };
7311                            if *q4tp.get_or_insert(is_p) != is_p
7312                                || *gu_q2.get_or_insert(is_q2) != is_q2
7313                            {
7314                                return None;
7315                            }
7316                            model.get_or_insert_with(|| mm.clone());
7317                            experts.push((gi, ui, di));
7318                        }
7319                        crate::gpu::GraphFfn::Moe {
7320                            router,
7321                            shared_gate: sgate,
7322                            experts,
7323                            n_exp: m.experts.len(),
7324                            top_k: m.top_k,
7325                            inter,
7326                            norm_topk: m.norm_topk_prob,
7327                            q4tp: q4tp?,
7328                            gu_q2: gu_q2.unwrap_or(false),
7329                            sigmoid: false,
7330                            bias: None,
7331                            has_shared: true,
7332                        }
7333                    }
7334                    _ => return None,
7335                };
7336                let attn = match &lw.attn {
7337                    AttnKind::Full {
7338                        wq,
7339                        wk,
7340                        wv,
7341                        wo,
7342                        q_norm,
7343                        k_norm,
7344                        output_gate,
7345                        softplus_gate,
7346                        bias,
7347                    } => {
7348                        if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
7349                            return None;
7350                        }
7351                        let (m, _, _, _) = wq.graph_weight()?;
7352                        model = Some(m.clone());
7353                        crate::gpu::GraphAttn::Full {
7354                            wq: gw(wq)?,
7355                            wk: gw(wk)?,
7356                            wv: gw(wv)?,
7357                            wo: gw(wo)?,
7358                            q_norm: q_norm.as_deref(),
7359                            k_norm: k_norm.as_deref(),
7360                            bias: bias
7361                                .as_ref()
7362                                .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7363                            output_gate: *output_gate,
7364                            cpu_k: self.kv_cache.layers[li].k_heads(),
7365                            cpu_v: self.kv_cache.layers[li].v_heads(),
7366                        }
7367                    }
7368                    AttnKind::LinearGdn(w) => {
7369                        let cfg = self.gdn_cfg?;
7370                        let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
7371                        model = Some(m.clone());
7372                        crate::gpu::GraphAttn::Gdn {
7373                            qkv: gw(&w.in_proj_qkv)?,
7374                            z: gw(&w.in_proj_z)?,
7375                            a: gw(&w.in_proj_a)?,
7376                            b: gw(&w.in_proj_b)?,
7377                            out: gw(&w.out_proj)?,
7378                            conv1d: &w.conv1d,
7379                            a_log: &w.a_log,
7380                            dt_bias: &w.dt_bias,
7381                            norm: &w.norm,
7382                            nv: cfg.num_v_heads,
7383                            nk: cfg.num_k_heads,
7384                            dk: cfg.key_head_dim,
7385                            dv: cfg.value_head_dim,
7386                            kk: cfg.conv_kernel,
7387                            cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
7388                        }
7389                    }
7390                    _ => return None,
7391                };
7392                layers.push(crate::gpu::GraphLayer {
7393                    input_norm: &lw.input_norm,
7394                    attn,
7395                    post_norm: &lw.post_norm,
7396                    ffn: gffn,
7397                });
7398            }
7399            Some((layers, model?))
7400        })();
7401        let Some((layers, model)) = built else {
7402            {
7403                use std::sync::atomic::{AtomicBool, Ordering};
7404                static SAID: AtomicBool = AtomicBool::new(false);
7405                if !SAID.swap(true, Ordering::Relaxed) {
7406                    tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
7407                }
7408            }
7409            return false;
7410        };
7411        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7412            eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
7413        }
7414        crate::gpu::forward_batch_graph(
7415            &model,
7416            self.graph_kv_id,
7417            &layers,
7418            &self.inv_freq,
7419            hiddens,
7420            nh,
7421            nkv,
7422            hd,
7423            rd,
7424            self.hidden_size,
7425            self.intermediate_size,
7426            positions,
7427            self.kv_cache.max_seq_len,
7428            gemma,
7429            self.rms_eps as f32,
7430            k,
7431            spec,
7432        )
7433    }
7434
7435    /// Same, stopping after layer `upto` inclusive (routing probe φ).
7436    /// `CMF_DSV4_DRAFT_PROBE=1` — grade the draft against what the trunk goes on
7437    /// to produce. Off by default; it runs a whole draft per decoded token.
7438    fn draft_probe() -> bool {
7439        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7440        *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
7441    }
7442
7443    /// `CMF_DSV4_DRAFT_PROBE=1`: measure how much of the draft the trunk
7444    /// would have agreed with, WITHOUT verifying or rolling anything back.
7445    ///
7446    /// The number this produces decides the whole speculation design — at
7447    /// acceptance a, a block of B positions yields 1 + a + a² + ... tokens
7448    /// per trunk pass — so it is worth measuring before any of the machinery
7449    /// that would exploit it exists. Each draft is parked with the position
7450    /// it was made at, and graded as the real tokens arrive.
7451    /// `CMF_DSV4_SPEC=1` — the DeepSeek-V4 speculative decode: draft five
7452    /// on the card, verify them in one batched trunk pass, commit the
7453    /// accepted prefix, roll the rest back.
7454    #[cfg(feature = "gpu")]
7455    fn dsv4_spec_on() -> bool {
7456        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7457        *ON.get_or_init(|| {
7458            // Test-only runtime gate: model loading still performs the same
7459            // reservation and trunk packing, which gives rollback parity a
7460            // topology-identical non-speculative control arm.
7461            if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
7462                return v != "0";
7463            }
7464            // An explicit value is a diagnostic force/escape hatch.  With no
7465            // knob, speculation is eligible only when model loading reserved
7466            // its bounded pack.  On small q4tp cards the geometric reserve
7467            // gate deliberately leaves this at zero: trying to build DSpark
7468            // after the exact trunk filled VRAM is both slower and a device
7469            // OOM (measured on A40).
7470            std::env::var("CMF_DSV4_SPEC")
7471                .map(|v| v != "0")
7472                .unwrap_or_else(|_| {
7473                    crate::gpu_wgpu::DRAFT_RESERVE
7474                        .load(std::sync::atomic::Ordering::Relaxed)
7475                        > 0
7476                })
7477        })
7478    }
7479
7480    /// One speculative round at the decode tip. `t_next` is the token the
7481    /// sampler just committed for `next_pos`. Returns the EXTRA accepted
7482    /// tokens (possibly none) and the new position, with `graph_logits`
7483    /// left holding the last accepted position's logits — exactly what the
7484    /// loop top expects. `None` means "speculate not this round": nothing
7485    /// was committed, the caller forwards normally.
7486    #[cfg(feature = "gpu")]
7487    fn dsv4_spec_step(
7488        &mut self,
7489        tip_token: u32,
7490        t_next: u32,
7491        next_pos: usize,
7492        max_extra: usize,
7493        drafted: &mut usize,
7494        accepted_ctr: &mut usize,
7495    ) -> Option<(Vec<u32>, usize)> {
7496        let t_all = std::time::Instant::now();
7497        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7498            thread_local! {
7499                static LAST: std::cell::Cell<Option<std::time::Instant>> =
7500                    const { std::cell::Cell::new(None) };
7501            }
7502            LAST.with(|l| {
7503                if let Some(prev) = l.get() {
7504                    eprintln!(
7505                        "между раундами {:.1} мс",
7506                        prev.elapsed().as_secs_f64() * 1e3
7507                    );
7508                }
7509                l.set(Some(std::time::Instant::now()));
7510            });
7511        }
7512        if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
7513            eprintln!("spec_step: вход pos={next_pos}");
7514        }
7515        let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
7516        let cfg = self.dsv4.as_ref().map(|b| b.2)?;
7517        // The draft state and its capture, armed exactly as the probe does.
7518        if self.dspark.is_none() {
7519            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7520            if t.is_empty() {
7521                return None;
7522            }
7523            crate::dsv4::dspark_arm(&t, cfg.dim);
7524            self.dspark = Some(crate::dsv4::DsparkState::new(
7525                self.dsv4_mtp.len(),
7526                &cfg,
7527                t.len(),
7528            ));
7529        }
7530        let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7531        let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
7532        if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
7533            eprintln!("spec_step: пак не построился (targets {targets:?})");
7534        }
7535        let pack = pack?;
7536        let block = crate::dsv4::dspark_block();
7537        let b_box = self.dsv4.as_mut()?;
7538        let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
7539        let ds = self.dspark.as_mut()?;
7540        // The tip's captures: either this token ran on a normal path that
7541        // filled the thread-local, or the previous spec round left them.
7542        let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
7543        if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
7544            if dbg {
7545                eprintln!("spec_step: нет захвата");
7546            }
7547            return None;
7548        }
7549        ds.have_hidden = true;
7550        let tip_pos = next_pos.checked_sub(1)?;
7551        let draft_started = std::time::Instant::now();
7552        let mut conf = Vec::new();
7553        let props = crate::dsv4::dspark_draft_gpu(
7554            g,
7555            &self.dsv4_mtp,
7556            &cfg,
7557            ds,
7558            pack,
7559            st.kv_id,
7560            tip_token,
7561            tip_pos,
7562            self.pool.as_deref(),
7563            &mut conf,
7564        );
7565        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
7566        *drafted += block;
7567        if props.is_empty() || props[0] != t_next {
7568            if dbg {
7569                eprintln!(
7570                    "spec_step: черновик {} (props0={:?} t_next={t_next})",
7571                    if props.is_empty() {
7572                        "пуст"
7573                    } else {
7574                        "мимо"
7575                    },
7576                    props.first()
7577                );
7578            }
7579            return None;
7580        }
7581        // `fed[0]` is `t_next`, which the outer loop has already committed;
7582        // only `fed[1..]` become additional output tokens. Cap the verify
7583        // transaction itself to the caller's remaining output budget instead
7584        // of merely truncating the returned vector: otherwise the KV/state
7585        // would advance past `max_tokens` and a 64-token request could return
7586        // 66 tokens (and poison a reused session with two invisible steps).
7587        let mut k_verify = crate::dsv4::dspark_verify_k()
7588            .min(props.len())
7589            .min(max_extra.saturating_add(1));
7590        // Adaptive depth: positions the draft itself doubts are paid for on
7591        // every verify and delivered almost never (natural-text survival
7592        // [.67 .50 .29 .08 .04]). `CMF_DSPARK_CONF_MIN=p` trims the fed
7593        // prefix at the first proposal whose confidence drops below p; on
7594        // predictable text the confidences stay high and nothing changes.
7595        let conf_min = {
7596            static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
7597            *M.get_or_init(|| {
7598                std::env::var("CMF_DSPARK_CONF_MIN")
7599                    .ok()
7600                    .and_then(|v| v.parse().ok())
7601                    .unwrap_or(0.0)
7602            })
7603        };
7604        if conf_min > 0.0 && conf.len() >= props.len() {
7605            let mut keep = 1usize;
7606            while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
7607                keep += 1;
7608            }
7609            k_verify = k_verify.min(keep.max(2));
7610        }
7611        if k_verify < 2 {
7612            return None;
7613        }
7614        let mut fed = Vec::with_capacity(k_verify);
7615        fed.push(t_next);
7616        fed.extend_from_slice(&props[1..k_verify]);
7617        let mut argmax = Vec::new();
7618        let mut logits_all = Vec::new();
7619        let mut walked = Vec::new();
7620        let txn = crate::dsv4::dsv4_verify_chunk(
7621            g,
7622            layers,
7623            &cfg,
7624            st,
7625            &fed,
7626            next_pos,
7627            &self.inv_freq,
7628            self.pool.as_deref(),
7629            &targets,
7630            &mut argmax,
7631            &mut logits_all,
7632            &mut walked,
7633        );
7634        if txn.is_none() && dbg {
7635            eprintln!("spec_step: verify отказал");
7636        }
7637        let txn = txn?;
7638        let spec_gpu_end = txn.gpu_end;
7639        let b = fed.len();
7640        let mut accepted = 1usize;
7641        while accepted < b && fed[accepted] == argmax[accepted - 1] {
7642            accepted += 1;
7643        }
7644        // `CMF_DSV4_SPEC_FORCE_REJECT=1` — accept nothing beyond the known
7645        // token, every round: the pure rollback exerciser. The output must
7646        // stay byte-identical to the plain walk; anything else is a
7647        // transaction bug, isolated from the acceptance logic.
7648        if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
7649            accepted = 1;
7650        }
7651        if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
7652            eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
7653        }
7654        let t_fin = std::time::Instant::now();
7655        if !crate::dsv4::dsv4_spec_finish(
7656            g,
7657            layers,
7658            &cfg,
7659            st,
7660            txn,
7661            accepted,
7662            &fed,
7663            &self.inv_freq,
7664            self.pool.as_deref(),
7665        ) {
7666            tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
7667            return None;
7668        }
7669        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7670            eprintln!(
7671                "finish(k={accepted}): {:.1} мс",
7672                t_fin.elapsed().as_secs_f64() * 1e3
7673            );
7674        }
7675        *accepted_ctr += accepted - 1;
7676        // Captures per accepted token: device targets photographed by the
7677        // batch, host targets from the verify's own walk. The last one
7678        // becomes the new tip's draft input; every one owes the ring an
7679        // entry for its position.
7680        let (hc, dim) = (cfg.hc_mult, cfg.dim);
7681        // Complete-chain layers are photographed by the fused submission;
7682        // partial device layers overwrite that slot after exact host cold-
7683        // expert correction.  Thus every target in the contiguous device
7684        // prefix has a valid per-token capture.
7685        let dev_caps: Vec<usize> = targets
7686            .iter()
7687            .copied()
7688            .filter(|&t| t < spec_gpu_end)
7689            .collect();
7690        let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
7691        if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
7692            return None;
7693        }
7694        for t in 0..accepted {
7695            let tip = t + 1 == accepted;
7696            for (slot, &tl) in targets.iter().enumerate() {
7697                if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
7698                    let lo = (di * b + t) * hc * dim;
7699                    crate::dsv4::dspark_capture(
7700                        &caps_all[lo..lo + hc * dim],
7701                        &cfg,
7702                        slot,
7703                        &mut ds.main_hidden,
7704                    );
7705                } else if tip
7706                    && crate::dsv4::dspark_peek_slot(slot, dim, {
7707                        let lo = slot * dim;
7708                        &mut ds.main_hidden[lo..lo + dim]
7709                    })
7710                {
7711                    // The tip's host-layer captures are the walk's own
7712                    // per-layer notes — exact. (The walk that ran last ended
7713                    // on exactly this token, on both the accept-all and the
7714                    // rollback path.)
7715                } else {
7716                    // Intermediate tokens: the post-tail state stands in for
7717                    // the per-layer capture on host targets below the last
7718                    // layer. Ring-entry quality only; the tip is exact.
7719                    crate::dsv4::dspark_capture(
7720                        &walked[t * hc * dim..(t + 1) * hc * dim],
7721                        &cfg,
7722                        slot,
7723                        &mut ds.main_hidden,
7724                    );
7725                }
7726            }
7727            crate::dsv4::dspark_ring_append(
7728                g,
7729                &self.dsv4_mtp,
7730                &cfg,
7731                ds,
7732                next_pos + t,
7733                self.pool.as_deref(),
7734            );
7735        }
7736        let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
7737        self.graph_logits = Some(row);
7738        // The speculative loop never runs the probe, so the trunk tally has
7739        // no other place to cycle. Armed only when someone asked for the
7740        // dump; the host tail is the only tallying path here, which is
7741        // precisely the population a partial pack would serve.
7742        if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
7743            crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
7744            crate::dsv4::pick_tally_arm();
7745        }
7746        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7747            eprintln!(
7748                "spec_step total {:.1} мс (k={accepted})",
7749                t_all.elapsed().as_secs_f64() * 1e3
7750            );
7751        }
7752        Some((fed[1..accepted].to_vec(), next_pos + accepted))
7753    }
7754
7755    fn dspark_probe(&mut self, position: usize, token_id: u32) {
7756        if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
7757            return;
7758        }
7759        // What the trunk just routed to, for this token.
7760        let trunk_now = crate::dsv4::pick_tally_take();
7761        crate::dsv4::trunk_freq_note(&trunk_now);
7762        if !trunk_now.is_empty() {
7763            self.dspark_trunk_picks.push(trunk_now);
7764            let keep = crate::dsv4::dspark_block();
7765            if self.dspark_trunk_picks.len() > keep {
7766                self.dspark_trunk_picks.remove(0);
7767            }
7768        }
7769        // Grade whatever is waiting: the token just decoded sits at
7770        // `position`, so it answers the draft made at `position - 1 - i`.
7771        for p in std::mem::take(&mut self.dspark_pending) {
7772            let Some(i) = position.checked_sub(p.0 + 1) else {
7773                continue;
7774            };
7775            let mut p = p;
7776            if i < p.1.len() {
7777                if p.2 && p.1[i] == token_id {
7778                    p.3 = i + 1;
7779                } else {
7780                    p.2 = false;
7781                }
7782                if i + 1 < p.1.len() {
7783                    self.dspark_pending.push(p);
7784                    continue;
7785                }
7786            }
7787            self.dspark_hist.push(p.3);
7788            self.dspark_real.push(token_id);
7789        }
7790        let Some(b) = &mut self.dsv4 else { return };
7791        let (g, layers, cfg) = (&b.0, &b.1, b.2);
7792        let n_layers = layers.len();
7793        if self.dspark.is_none() {
7794            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7795            if t.is_empty() {
7796                return;
7797            }
7798            eprintln!(
7799                "DSpark: захват со слоёв {t:?}, блок {}",
7800                crate::dsv4::dspark_block()
7801            );
7802            crate::dsv4::dspark_arm(&t, cfg.dim);
7803            self.dspark = Some(crate::dsv4::DsparkState::new(
7804                self.dsv4_mtp.len(),
7805                &cfg,
7806                t.len(),
7807            ));
7808        }
7809        let ds = self.dspark.as_mut().unwrap();
7810        if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
7811            return; // this token ran on a path that captures nothing
7812        }
7813        let mut conf = Vec::new();
7814        crate::dsv4::pick_tally_arm();
7815        // The trunk has already consumed the adaptive VRAM budget. Until the
7816        // draft owns an explicit bounded device pack, its tensors are an
7817        // out-of-core CPU/disk tier by contract: never let per-op probes try
7818        // to squeeze another multi-gigabyte MTP expert cache onto the card.
7819        let draft_started = std::time::Instant::now();
7820        #[cfg(feature = "gpu")]
7821        let gpu_draft = crate::dsv4::dspark_gpu_on();
7822        #[cfg(not(feature = "gpu"))]
7823        let gpu_draft = false;
7824        let props = if gpu_draft {
7825            #[cfg(feature = "gpu")]
7826            {
7827                let kv_id = b.3.kv_id;
7828                match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
7829                    Some(pk) => crate::dsv4::dspark_draft_gpu(
7830                        g,
7831                        &self.dsv4_mtp,
7832                        &cfg,
7833                        ds,
7834                        pk,
7835                        kv_id,
7836                        token_id,
7837                        position,
7838                        self.pool.as_deref(),
7839                        &mut conf,
7840                    ),
7841                    None => Vec::new(),
7842                }
7843            }
7844            #[cfg(not(feature = "gpu"))]
7845            Vec::new()
7846        } else {
7847            crate::gpu::cpu_scope(|| {
7848                crate::dsv4::dspark_draft(
7849                    g,
7850                    &self.dsv4_mtp,
7851                    &cfg,
7852                    ds,
7853                    token_id,
7854                    position,
7855                    self.pool.as_deref(),
7856                    &mut conf,
7857                )
7858            })
7859        };
7860        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
7861        let draft_picks = crate::dsv4::pick_tally_take();
7862        crate::dsv4::dspark_freq_note(&draft_picks);
7863        // Re-arm for the NEXT trunk token; the probe runs after the forward,
7864        // so this is the only place that can.
7865        crate::dsv4::pick_tally_arm();
7866        if !props.is_empty() {
7867            // Two ratios, side by side: what a batched verify over the trunk
7868            // would read against what it asks for, and the same for the
7869            // draft's three stages. Near 1.0 means a batch amortises nothing.
7870            let (tu, tt) = {
7871                let flat: Vec<(usize, Vec<usize>)> = self
7872                    .dspark_trunk_picks
7873                    .iter()
7874                    .flat_map(|v| v.iter().cloned())
7875                    .collect();
7876                // Per layer, across the window of tokens.
7877                let mut per: std::collections::HashMap<usize, Vec<usize>> =
7878                    std::collections::HashMap::new();
7879                for (li, picks) in flat {
7880                    per.entry(li).or_default().extend(picks);
7881                }
7882                let n = per.len().max(1);
7883                let mut u = 0usize;
7884                let mut t = 0usize;
7885                for (_, v) in per {
7886                    t += v.len();
7887                    u += v.iter().collect::<std::collections::HashSet<_>>().len();
7888                }
7889                (u / n, t / n)
7890            };
7891            let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
7892            self.dspark_exp.push((tu, tt, du, dt));
7893            self.dspark_pending.push((position, props, true, 0));
7894        }
7895        if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
7896            let n = self.dspark_hist.len() as f32;
7897            let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
7898            let block = crate::dsv4::dspark_block();
7899            let mut at = vec![0usize; block + 1];
7900            for &k in &self.dspark_hist {
7901                at[k] += 1;
7902            }
7903            // Prefix survival: S_i = P(the first i positions all held).
7904            let mut surv = Vec::with_capacity(block);
7905            for i in 1..=block {
7906                let k = at[i..].iter().sum::<usize>() as f32 / n;
7907                surv.push(format!("{k:.2}"));
7908            }
7909            let distinct = self
7910                .dspark_real
7911                .iter()
7912                .collect::<std::collections::HashSet<_>>()
7913                .len();
7914            let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
7915                (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
7916            });
7917            let m = self.dspark_exp.len().max(1);
7918            eprintln!(
7919                "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
7920                 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
7921                self.dspark_hist.len(),
7922                mean + 1.0,
7923                surv.join(" ")
7924            );
7925            eprintln!(
7926                "DSpark: разных токенов {distinct} из {} (вырожденность), \
7927                 эксперты ствол {}/{} на слой за {block} токенов, \
7928                 черновик {}/{} за блок, draft {:.2} мс/блок",
7929                self.dspark_real.len(),
7930                tu / m,
7931                tt / m,
7932                du / m,
7933                dt / m,
7934                self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
7935            );
7936        }
7937    }
7938
7939    fn forward_layers_upto(
7940        &mut self,
7941        hidden: &[f32],
7942        position: usize,
7943        task_mask: Option<&TaskMask>,
7944        upto: Option<usize>,
7945    ) -> Vec<f32> {
7946        // In-process multi-GPU: each segment runs pinned to its card,
7947        // and the only thing crossing the boundary is one hidden vector
7948        // that never leaves this address space. Same layer split the
7949        // network mode does, minus the second process, the socket, the
7950        // serialization and the dir_hash handshake.
7951        if let Some(plan) = self.gpu_plan.clone() {
7952            if upto.is_none() && plan.len() > 1 {
7953                let mut h = hidden.to_vec();
7954                for &(dev, from, upto_incl) in plan.iter() {
7955                    h = crate::gpu::with_device(dev, || {
7956                        self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
7957                    });
7958                }
7959                return h;
7960            }
7961        }
7962        self.forward_layers_span(hidden, position, task_mask, 0, upto)
7963    }
7964
7965    /// Split this pipeline's layer stack across local GPUs: segment i
7966    /// runs on `devices[i]`. Contiguous and even by layer count — the
7967    /// VRAM-weighted planner is the next step, and an uneven card pair
7968    /// is why it will be needed. `None` clears the plan.
7969    pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
7970        self.set_gpu_plan_at(devices, None)
7971    }
7972
7973    /// The same, with an explicit first boundary (`--peer-split`): card
7974    /// 0 takes layers `[0..at)`, the rest split what remains. Uneven
7975    /// cards, or an attention-heavy head, are why this knob exists.
7976    pub fn set_gpu_plan_at(
7977        &mut self,
7978        devices: Option<&[usize]>,
7979        at: Option<usize>,
7980    ) -> Result<(), String> {
7981        let Some(devs) = devices.filter(|d| d.len() > 1) else {
7982            self.gpu_plan = None;
7983            return Ok(());
7984        };
7985        self.split_supported()?;
7986        let n = self.num_layers;
7987        if devs.len() > n {
7988            return Err(format!("{} devices for {n} layers", devs.len()));
7989        }
7990        if let Some(k) = at {
7991            if k == 0 || k >= n {
7992                return Err(format!("split at {k}: the model has {n} layers"));
7993            }
7994            if devs.len() == 2 {
7995                self.gpu_plan = Some(std::sync::Arc::new(vec![
7996                    (devs[0], 0, k - 1),
7997                    (devs[1], k, n - 1),
7998                ]));
7999                return Ok(());
8000            }
8001            return Err(format!(
8002                "an explicit split point takes exactly 2 devices, got {}",
8003                devs.len()
8004            ));
8005        }
8006        let per = n.div_ceil(devs.len());
8007        let mut plan = Vec::with_capacity(devs.len());
8008        let mut from = 0usize;
8009        for &d in devs {
8010            if from >= n {
8011                break;
8012            }
8013            let upto = (from + per - 1).min(n - 1);
8014            plan.push((d, from, upto));
8015            from = upto + 1;
8016        }
8017        self.gpu_plan = Some(std::sync::Arc::new(plan));
8018        Ok(())
8019    }
8020
8021    /// The active in-process split, if any: (device, first layer, last).
8022    pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
8023        self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
8024    }
8025
8026    /// Layer span [from ..= upto] (upto None = last layer): the building
8027    /// block the network pipeline-split rides on. `from > 0` skips the
8028    /// arch escape hatches (the pub `forward_span` refuses those archs
8029    /// first) and the whole-token graph — the plain per-layer loop is
8030    /// the canonical executor for a partial stack.
8031    fn forward_layers_span(
8032        &mut self,
8033        hidden: &[f32],
8034        position: usize,
8035        task_mask: Option<&TaskMask>,
8036        from: usize,
8037        upto: Option<usize>,
8038    ) -> Vec<f32> {
8039        debug_assert!(from == 0 || (self.dsv4.is_none() && self.g3n.is_none()));
8040        // DeepSeek-V4 runs its own stack: the state is hc_mult copies, and
8041        // the forward returns LOGITS, not a hidden — the head is inside it
8042        // (the final fold sits between the last layer and the norm). The
8043        // token id rides in `hidden[0]`, written by embed_single, because
8044        // the hash layers route by id rather than by content.
8045        if let Some(b) = &mut self.dsv4 {
8046            let _ = (task_mask, upto);
8047            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
8048            let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
8049            st.pos = position;
8050            let mut logits = Vec::new();
8051            crate::dsv4::forward_token(
8052                g,
8053                layers,
8054                &cfg,
8055                st,
8056                token_id,
8057                &self.inv_freq,
8058                self.pool.as_deref(),
8059                &mut logits,
8060            );
8061            self.graph_logits = Some(logits);
8062            self.dspark_probe(position, token_id);
8063            // The caller expects a hidden; the logits went out of band, as
8064            // with the fused lm_head path.
8065            return vec![0.0; self.hidden_size];
8066        }
8067        // Gemma-3n runs its own stack (4 AltUp replicas don't fit this
8068        // loop); `hidden` is the extended embedding from embed_single.
8069        if let Some(b) = &self.g3n {
8070            let _ = (task_mask, upto);
8071            return crate::g3n::g3n_forward(
8072                &b.0,
8073                &b.1,
8074                hidden,
8075                position,
8076                &mut self.kv_cache.layers,
8077                self.num_heads,
8078                self.num_kv_heads,
8079                self.head_dim,
8080                self.pool.as_deref(),
8081            );
8082        }
8083        let mut h = hidden.to_vec();
8084        // Split borrows: copy scalars / clone handles so the per-layer
8085        // cfg does not hold `&self` while the KV cache is `&mut`.
8086        let (nh, _nkv, _hd, hs, _rd, eps) = (
8087            self.num_heads,
8088            self.num_kv_heads,
8089            self.head_dim,
8090            self.hidden_size,
8091            self.rotary_dim,
8092            self.rms_eps,
8093        );
8094        let pool = self.pool.clone();
8095        // Opt-in wgpu token-graph attention (discrete Vulkan/DX12): the whole
8096        // attention sub-block runs resident in one submit. Off by default.
8097        // Whole-token wgpu graph: eligibility + arbitration.
8098        //  - explicit CMF_GPU_WGPU_GRAPH forces it on/off;
8099        //  - discrete adapters (4090: decode 76 -> 137 tok/s) and GDN
8100        //    hybrids (recurrent state device-resident, no CPU twin to
8101        //    race) TRUST it;
8102        //  - integrated/mobile adapters RACE it against the normal path
8103        //    at generation granularity (gpu::graph_race_*) — tiled
8104        //    mobile GPUs can turn the ~300-dispatch graph into seconds
8105        //    per token, while a fast phone GPU keeps its win.
8106        let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
8107        let graph_on = match graph_env.as_deref() {
8108            Some("0") => false,
8109            Some("prefill") => false, // decode keeps the per-op path
8110            Some(_) => true,
8111            // Unset: same discrete-only default as every other graph
8112            // site. "Is the GPU on" used to stand in here — which made
8113            // the 0.2 tok/s whole-token graph race-eligible on mobile
8114            // adapters and cost 12-14× on first tokens (cmfmobile
8115            // TUNING.md); integrated GPUs keep the per-op probe path.
8116            None => crate::gpu::wgpu_graph_default(),
8117        };
8118        let graph_trusted =
8119            graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
8120        let race_eligible = graph_on
8121            && upto.is_none()
8122            && task_mask.is_none()
8123            && from == 0
8124            && !crate::gpu::graph_unsupported();
8125        let mut tail_start = 0usize;
8126        if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
8127            let t_graph = std::time::Instant::now();
8128            let mut lg = Vec::new();
8129            let mut gl = 0usize;
8130            let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
8131            // Past the transient guards (o1 still collecting, a softcap)
8132            // a refusal is about the weights and will never change —
8133            // remember it instead of walking every layer again next
8134            // token.
8135            if built.is_none() && !self.o1_active() && self.attn_softcap == 0.0 {
8136                crate::gpu::graph_mark_unsupported();
8137            }
8138            graph_note(built.is_some());
8139            if let Some(hh) = built {
8140                let dur = t_graph.elapsed();
8141                if std::env::var("CMF_GRAPH_PROF").is_ok() {
8142                    eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
8143                }
8144                if gl > 0 && gl < self.num_layers {
8145                    // Device prefix: the graph ran layers 0..gl and handed
8146                    // back the boundary hidden — the loop below owns the
8147                    // tail. The prefix layers' KV/state advanced on the
8148                    // device; the tail's advances on the host below. One
8149                    // boundary crossing per token.
8150                    h = hh;
8151                    tail_start = gl;
8152                } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
8153                    if !graph_trusted {
8154                        crate::gpu::graph_race_record(true, dur);
8155                    }
8156                    if !lg.is_empty() {
8157                        // Graph produced logits (final-norm + lm_head folded in) —
8158                        // pad/cap to vocab and hand them to the sampler directly.
8159                        lg.resize(self.vocab_size, 0.0);
8160                        if let Some(c) = self.final_softcap {
8161                            for l in lg.iter_mut() {
8162                                *l = c * (*l / c).tanh();
8163                            }
8164                        }
8165                        self.graph_logits = Some(lg);
8166                    }
8167                    return hh;
8168                }
8169                // Hopeless first graph token: discard it and fall through
8170                // to the normal path. Safe exactly here — the prompt KV is
8171                // still CPU-owned (chunked prefill), so recomputing this
8172                // position is exact; the mirror's extra row is never read
8173                // (the race just settled on the normal path).
8174            }
8175        }
8176        // KIMI-LINEAR HAS NO SPLIT BUG. The 2.6× reported from the
8177        // model rotation (12.2 tok/s on one card against 4.6 on two)
8178        // was a single measurement of a model whose arm arbitration is
8179        // borderline, and it did not survive repetition. Three runs an
8180        // arm, same binary, back to back:
8181        //   probe on : 1 GPU 9.5 / 5.7 / 5.9   2 GPU 7.8 / 13.0 / 13.3
8182        //   pinned   : 1 GPU 5.6 / 5.3 / 5.2   2 GPU 3.5 / 4.2 / 3.4
8183        // With the arms pinned the split costs about 1.45×, which is
8184        // what a layer split costs. With the probe free, TWO CARDS RUN
8185        // FASTER — because for this model the CPU arm wins some op
8186        // classes and the probe finds that.
8187        //
8188        // Two things do stand, and both are measured. The token graph
8189        // builds NOTHING here (`covered 0 of 14 layers [0..14)`), so
8190        // every layer walks per-op on either arm — that is where the
8191        // headroom is, not in the split. And this model's benchmark is
8192        // unusable without `CMF_GPU_PROBE=0`: the arbitration alone
8193        // moves it by more than 2×.
8194        //
8195        // Span runs (network split): the graph covers exactly [from..=upto]
8196        // — one submit per SEGMENT per token. No race: its state is global
8197        // and calibrated on full stacks, so spans take the graph only where
8198        // it is trusted by default (discrete adapters / CMF_GPU_WGPU_GRAPH).
8199        let span = from > 0 || upto.is_some();
8200        if span && graph_on && task_mask.is_none() && graph_trusted {
8201            let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
8202            let mut lg = Vec::new();
8203            let mut gl = 0usize;
8204            let span_res =
8205                self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
8206            graph_note(span_res.is_some() && gl == upto_excl - from);
8207            if std::env::var("CMF_GPU_DEBUG").is_ok() {
8208                // How much of the span the graph actually covered. A
8209                // prefix of nothing means every layer walks per-op and
8210                // the split's extra cost is elsewhere.
8211                static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
8212                if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
8213                    eprintln!(
8214                        "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
8215                        upto_excl - from,
8216                        span_res.is_some()
8217                    );
8218                }
8219            }
8220            if let Some(hh) = span_res {
8221                if gl == upto_excl - from {
8222                    if !lg.is_empty() {
8223                        lg.resize(self.vocab_size, 0.0);
8224                        if let Some(c) = self.final_softcap {
8225                            for l in lg.iter_mut() {
8226                                *l = c * (*l / c).tanh();
8227                            }
8228                        }
8229                        self.graph_logits = Some(lg);
8230                    }
8231                    crate::gpu::set_layer(-1);
8232                    return hh;
8233                }
8234                // Partial device prefix of the span: CPU owns the tail.
8235                h = hh;
8236                tail_start = from + gl;
8237            }
8238        }
8239        let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
8240
8241        #[cfg(target_os = "macos")]
8242        let mut gpu_skip_until = 0usize;
8243        for li in tail_start.max(from)..self.num_layers {
8244            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU (CMF_GPU_LAYERS)
8245            if let Some(u) = upto {
8246                if li > u {
8247                    break;
8248                }
8249            }
8250            if let Some(mask) = task_mask {
8251                if !mask.layer_alive(li) {
8252                    continue; // dead layer: residual pass-through
8253                }
8254            }
8255            // Whole-block q1 token graph: a run of consecutive q1
8256            // layers — GDN and full attention — executes with one sync
8257            // per CPU attend instead of per op (macOS/Metal).
8258            #[cfg(target_os = "macos")]
8259            {
8260                if li < gpu_skip_until {
8261                    continue;
8262                }
8263                if task_mask.is_none() {
8264                    let end = self.q1_graph_gpu(li, upto, position, &mut h);
8265                    if end > li {
8266                        gpu_skip_until = end;
8267                        // Looped Transformer: the graph stopped at a loop
8268                        // boundary — apply final norm before the next iteration.
8269                        if self.is_loop_end(end - 1) && end < self.num_layers {
8270                            h = inference::rms_norm(
8271                                &h,
8272                                &self.weights.final_norm,
8273                                self.rms_eps,
8274                                self.norm_style,
8275                            );
8276                        }
8277                        continue;
8278                    }
8279                }
8280            }
8281
8282            let lw = &self.weights.layers[self.phys_layer(li)];
8283            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
8284                if tp.parse::<usize>().ok() == Some(position) {
8285                    let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
8286                    eprintln!(
8287                        "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
8288                        h[0], h[1]
8289                    );
8290                }
8291            }
8292            // Norm into the pipeline scratch — the returning rms_norm
8293            // allocated twice per layer per token (roadmap §3 P0).
8294            inference::rms_norm_into(
8295                &h,
8296                &lw.input_norm,
8297                self.rms_eps,
8298                self.norm_style,
8299                &mut self.ws.n1,
8300            );
8301
8302            let attn_out = match &lw.attn {
8303                AttnKind::Mla(w) => {
8304                    let inv_freq_l = self.layer_inv_freq(li);
8305                    let rs = self.layer_rope_scale(li);
8306                    let eps = self.rms_eps;
8307                    let pool = self.pool.clone();
8308                    mla_attention(
8309                        w,
8310                        &self.ws.n1,
8311                        &mut self.kv_cache.layers[li],
8312                        position,
8313                        &inv_freq_l,
8314                        rs,
8315                        eps,
8316                        pool.as_deref(),
8317                    )
8318                }
8319                AttnKind::Linear(w) => {
8320                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8321                    vmf_phase_forward(
8322                        &self.ws.n1,
8323                        w,
8324                        &cfg,
8325                        &mut self.kv_cache.layers[li].linear_state,
8326                        self.pool.as_deref(),
8327                    )
8328                }
8329                AttnKind::Kda(w) => {
8330                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
8331                    crate::linear_core::kda_forward(
8332                        &self.ws.n1,
8333                        w,
8334                        &cfg,
8335                        &mut self.kv_cache.layers[li].linear_state,
8336                        self.pool.as_deref(),
8337                    )
8338                }
8339                AttnKind::LinearGdn(w) => {
8340                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8341                    gdn_forward(
8342                        &self.ws.n1,
8343                        w,
8344                        &cfg,
8345                        &mut self.kv_cache.layers[li].linear_state,
8346                        self.pool.as_deref(),
8347                    )
8348                }
8349                AttnKind::ShortConv(w) => {
8350                    let cfg = self
8351                        .short_conv_cfg
8352                        .expect("short-conv layer without short_conv_cfg");
8353                    short_conv_forward(
8354                        &self.ws.n1,
8355                        w,
8356                        &cfg,
8357                        &mut self.kv_cache.layers[li].linear_state,
8358                        self.pool.as_deref(),
8359                    )
8360                }
8361                AttnKind::Full {
8362                    wq,
8363                    wk,
8364                    wv,
8365                    wo,
8366                    q_norm,
8367                    k_norm,
8368                    output_gate,
8369                    softplus_gate,
8370                    bias,
8371                } if self.kv_cache.layers[li].o1_sealed() => {
8372                    // O(1) override: decode on the sealed Nyström state
8373                    // instead of the growing KV cache.
8374                    let inv_freq_l = self.layer_inv_freq(li);
8375                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8376                    let cfg = QwenAttnCfg {
8377                        num_heads: self.layer_num_heads(li),
8378                        num_kv_heads: nkv_l,
8379                        head_dim: hd_l,
8380                        hidden_size: hs,
8381                        position,
8382                        inv_freq: &inv_freq_l,
8383                        rotary_dim: rd_l,
8384                        scale: self.attn_scale,
8385                        softcap: self.attn_softcap,
8386                        window: None,
8387                        v_norm: self.attn_v_norm,
8388                        q_norm: q_norm.as_deref(),
8389                        k_norm: k_norm.as_deref(),
8390                        output_gate: *output_gate,
8391                        softplus_gate: softplus_gate
8392                            .as_ref()
8393                            .map(|(gate, per_head)| (gate, *per_head)),
8394                        rope_scale: self.layer_rope_scale(li),
8395                        bias: bias
8396                            .as_ref()
8397                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8398                        rms_eps: eps,
8399                        norm_style: self.norm_style,
8400                        pool: pool.as_deref(),
8401                    };
8402                    attention::qwen_attention_nystrom(
8403                        &self.ws.n1,
8404                        wq,
8405                        wk,
8406                        wv,
8407                        wo,
8408                        &mut self.kv_cache.layers[li],
8409                        &cfg,
8410                    )
8411                }
8412                AttnKind::Full {
8413                    wq,
8414                    wk,
8415                    wv,
8416                    wo,
8417                    q_norm,
8418                    k_norm,
8419                    output_gate,
8420                    softplus_gate,
8421                    bias,
8422                } => 'attn: {
8423                    // wgpu token-graph attention (opt-in): whole sub-block in
8424                    // one submit, device K/V mirror. q1 only, no gate/bias/mask.
8425                    if graph_on
8426                        && !*output_gate
8427                        && softplus_gate.is_none()
8428                        && self.attention_heads_per_layer.is_none()
8429                        && bias.is_none()
8430                        && task_mask.is_none()
8431                    {
8432                        let inv_freq_l = self.layer_inv_freq(li);
8433                        let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8434                        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8435                        if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
8436                            wq.mapped_q1(),
8437                            wk.mapped_q1(),
8438                            wv.mapped_q1(),
8439                            wo.mapped_q1(),
8440                        ) {
8441                            let gm = gm.clone();
8442                            let mut out = vec![0f32; hs];
8443                            let cache = &self.kv_cache.layers[li];
8444                            if crate::gpu::attn_dropin(
8445                                &gm,
8446                                self.graph_kv_id,
8447                                li,
8448                                &self.ws.n1,
8449                                qi,
8450                                ki,
8451                                vi,
8452                                oi,
8453                                q_norm.as_deref(),
8454                                k_norm.as_deref(),
8455                                &inv_freq_l,
8456                                nh,
8457                                nkv_l,
8458                                hd_l,
8459                                rd_l,
8460                                hs,
8461                                position,
8462                                self.kv_cache.max_seq_len,
8463                                gemma,
8464                                eps as f32,
8465                                cache.k_heads(),
8466                                cache.v_heads(),
8467                                &mut out,
8468                            ) {
8469                                break 'attn out;
8470                            }
8471                        }
8472                    }
8473                    let masked = task_mask
8474                        .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
8475                        .unwrap_or(false);
8476                    let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
8477                    match (masked, f32_view) {
8478                        // Historical masked path (f32 slices; the loader
8479                        // keeps masked models in f32).
8480                        (true, (Some(q), Some(k), Some(v), Some(o))) => {
8481                            let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
8482                            attention::multi_head_attention(
8483                                &self.ws.n1,
8484                                q,
8485                                k,
8486                                v,
8487                                o,
8488                                &mut self.kv_cache.layers[li],
8489                                self.num_heads,
8490                                self.num_kv_heads,
8491                                self.head_dim,
8492                                self.hidden_size,
8493                                position,
8494                                &active_heads,
8495                                &self.inv_freq,
8496                            )
8497                        }
8498                        (masked, _) => {
8499                            if masked {
8500                                tracing::warn!(
8501                                    "layer {li}: head mask on quantized weights not \
8502                                     supported yet — executing dense"
8503                                );
8504                            }
8505                            let inv_freq_l = self.layer_inv_freq(li);
8506                            let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8507                            let cfg = QwenAttnCfg {
8508                                num_heads: self.layer_num_heads(li),
8509                                num_kv_heads: nkv_l,
8510                                head_dim: hd_l,
8511                                hidden_size: hs,
8512                                position,
8513                                inv_freq: &inv_freq_l,
8514                                rotary_dim: rd_l,
8515                                scale: self.attn_scale,
8516                                softcap: self.attn_softcap,
8517                                window: self.layer_window(li),
8518                                v_norm: self.attn_v_norm,
8519                                q_norm: q_norm.as_deref(),
8520                                k_norm: k_norm.as_deref(),
8521                                output_gate: *output_gate,
8522                                softplus_gate: softplus_gate
8523                                    .as_ref()
8524                                    .map(|(gate, per_head)| (gate, *per_head)),
8525                                rope_scale: self.layer_rope_scale(li),
8526                                bias: bias
8527                                    .as_ref()
8528                                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8529                                rms_eps: eps,
8530                                norm_style: self.norm_style,
8531                                pool: pool.as_deref(),
8532                            };
8533                            attention::qwen_attention(
8534                                &self.ws.n1,
8535                                wq,
8536                                wk,
8537                                wv,
8538                                wo,
8539                                &mut self.kv_cache.layers[li],
8540                                &cfg,
8541                            )
8542                        }
8543                    }
8544                }
8545            };
8546            // Gemma sandwich norm: normalize the attention branch before
8547            // it joins the residual stream.
8548            let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8549                Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
8550                None => attn_out,
8551            };
8552            let lw = &self.weights.layers[self.phys_layer(li)];
8553            inference::add_rmsnorm_fused_into(
8554                &mut h,
8555                &attn_out,
8556                &lw.post_norm,
8557                self.rms_eps,
8558                self.norm_style,
8559                &mut self.ws.p1,
8560            );
8561            let mut attn_out = attn_out;
8562            attention::recycle_buf(&mut attn_out);
8563            let post_normed = &self.ws.p1;
8564
8565            let ffn_masked = task_mask
8566                .map(|m| m.ffn_active_count(li) < self.intermediate_size)
8567                .unwrap_or(false);
8568            // One masked dense CONTRACT, dispatched by cost. The
8569            // activation-zeroing arm (the batched sweep's, validated
8570            // against the replica to 0.8%) computes the FULL fused FFN
8571            // and zeroes the dead — right whenever most neurons live.
8572            // The sparse arm reads ONLY active rows and down columns —
8573            // per-row dots are slower per element than the fused kernel,
8574            // so it pays only once the mask is deep enough. The 0.5
8575            // crossover is first-principles (fused kernels run ~2x the
8576            // per-row dot throughput); a shallow specialist (95% alive)
8577            // stays fused, a --target-sparsity bake flips arms on its
8578            // own weight.
8579            let ffn_out = match (ffn_masked, &lw.ffn) {
8580                // A defragged tube layer answers its own mask: the core
8581                // always runs, each tube runs when its bit is on, and
8582                // the tubes that are off are never read from the mmap.
8583                (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
8584                    let row = task_mask
8585                        .and_then(|tm| tm.ffn_masks.get(li))
8586                        .map(|v| v.as_slice());
8587                    tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
8588                }
8589                (true, FfnKind::Dense(d)) => {
8590                    let tm = task_mask.unwrap();
8591                    let alive = tm.ffn_active_count(li);
8592                    let deep = alive * 2 <= self.intermediate_size;
8593                    if deep && d.down_proj.sparse_col_ok() {
8594                        let active = tm.ffn_active_indices(li);
8595                        sparse_ffn_quant(
8596                            d,
8597                            post_normed,
8598                            &active,
8599                            self.hidden_size,
8600                            self.pool.as_deref(),
8601                        )
8602                    } else if deep
8603                        && let (Some(g), Some(u), Some(dn)) = (
8604                            d.gate_proj.as_f32(),
8605                            d.up_proj.as_f32(),
8606                            d.down_proj.as_f32(),
8607                        )
8608                    {
8609                        let active = tm.ffn_active_indices(li);
8610                        inference::sparse_ffn_forward(
8611                            post_normed,
8612                            g,
8613                            u,
8614                            dn,
8615                            self.hidden_size,
8616                            self.intermediate_size,
8617                            &active,
8618                            self.pool.as_deref(),
8619                        )
8620                    } else {
8621                        let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
8622                        dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
8623                    }
8624                }
8625                (true, FfnKind::Moe(m)) => {
8626                    // MoE is sparse by expert selection; a task mask
8627                    // narrows the ROUTABLE set via its expert fields
8628                    // (spec §5) when it carries them.
8629                    let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
8630                    ffn_forward(
8631                        &lw.ffn,
8632                        post_normed,
8633                        self.pool.as_deref(),
8634                        allowed.as_deref(),
8635                    )
8636                }
8637                (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
8638                    dm,
8639                    post_normed,
8640                    &h,
8641                    self.rms_eps,
8642                    self.norm_style,
8643                    self.pool.as_deref(),
8644                ),
8645                (false, _) => match &lw.ffn {
8646                    FfnKind::DenseMoe(dm) => dense_moe_ffn(
8647                        dm,
8648                        post_normed,
8649                        &h,
8650                        self.rms_eps,
8651                        self.norm_style,
8652                        self.pool.as_deref(),
8653                    ),
8654                    _ => {
8655                        let allowed = match (&lw.ffn, task_mask) {
8656                            (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
8657                            _ => None,
8658                        };
8659                        ffn_forward(
8660                            &lw.ffn,
8661                            post_normed,
8662                            self.pool.as_deref(),
8663                            allowed.as_deref(),
8664                        )
8665                    }
8666                },
8667            };
8668            let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8669                Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
8670                None => ffn_out,
8671            };
8672            for (i, &f) in ffn_out.iter().enumerate() {
8673                h[i] += f;
8674            }
8675            let mut ffn_out = ffn_out;
8676            attention::recycle_buf(&mut ffn_out);
8677
8678            // Gemma-4: the layer output is scaled by a learned scalar.
8679            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8680                for v in h.iter_mut() {
8681                    *v *= sc;
8682                }
8683            }
8684
8685            // Looped Transformer: apply final norm at the end of each loop iteration.
8686            // Nanbeige 4.2: after layer 21 (virtual), apply norm before looping back to layer 0.
8687            if self.is_loop_end(li) && li + 1 < self.num_layers {
8688                h = inference::rms_norm(
8689                    &h,
8690                    &self.weights.final_norm,
8691                    self.rms_eps,
8692                    self.norm_style,
8693                );
8694            }
8695
8696            // Dynamic routing φ capture (on-policy, fireball-style): the
8697            // EMA of the post-residual hidden at the router's phi_layer,
8698            // updated as the context evolves during decode.
8699            if self.dyn_phi_layer == Some(li) {
8700                self.update_dyn_phi(&h);
8701            }
8702        }
8703        crate::gpu::set_layer(-1); // layers done — lm_head outside layer-split
8704        if let Some(t) = t_race_cpu {
8705            crate::gpu::graph_race_record(false, t.elapsed());
8706        }
8707
8708        h
8709    }
8710
8711    /// EMA of φ at the router layer (rolling, weight 0.2 = ~5-token
8712    /// horizon). First observation seeds it exactly.
8713    fn update_dyn_phi(&mut self, h: &[f32]) {
8714        const A: f32 = 0.2;
8715        if self.dyn_phi_ema.len() != h.len() {
8716            self.dyn_phi_ema = vec![0.0; h.len()];
8717            self.dyn_phi_seen = 0;
8718        }
8719        if self.dyn_phi_seen == 0 {
8720            self.dyn_phi_ema.copy_from_slice(h);
8721        } else {
8722            for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
8723                *e = (1.0 - A) * *e + A * v;
8724            }
8725        }
8726        self.dyn_phi_seen += 1;
8727    }
8728
8729    /// Current router φ (EMA at phi_layer); empty until first capture.
8730    pub fn dyn_phi(&self) -> &[f32] {
8731        &self.dyn_phi_ema
8732    }
8733
8734    /// Enable/disable φ capture at the router layer, reset the EMA.
8735    pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
8736        self.dyn_phi_layer = layer;
8737        self.dyn_phi_ema.clear();
8738        self.dyn_phi_seen = 0;
8739    }
8740
8741    /// Skills eligible for dynamic switching: (index, id, phi_layer).
8742    pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
8743        let Some(model) = &self.model else {
8744            return Vec::new();
8745        };
8746        model
8747            .header
8748            .skills
8749            .iter()
8750            .enumerate()
8751            .filter_map(|(i, sk)| {
8752                let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
8753                let sel = sk.selection.as_ref()?;
8754                (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
8755            })
8756            .collect()
8757    }
8758
8759    /// Index of the currently overlaid skill (None = backbone).
8760    pub fn active_skill(&self) -> Option<usize> {
8761        self.dyn_active
8762    }
8763
8764    /// Enable dynamic per-token skill routing: build the hysteresis
8765    /// router from the container's routable skills, start φ capture at
8766    /// their (shared) phi_layer. Returns the number of routable skills
8767    /// (0 = nothing to route; router stays off). Idempotent.
8768    pub fn enable_dynamic_routing(&mut self) -> usize {
8769        use crate::swarm::{DynRouter, RoutableSkill};
8770        let Some(model) = self.model.clone() else {
8771            return 0;
8772        };
8773        // A blend materialized f32 working tensors into the layers; there
8774        // is no single skill index to revert from → refuse (honest).
8775        if self.dyn_blend_loaded {
8776            tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
8777            return 0;
8778        }
8779        // A statically-overlaid skill that is NOT FFN-eligible can't be
8780        // cheaply reverted at generation start → refuse rather than
8781        // silently keep it overlaid.
8782        if let Some(a) = self.dyn_active {
8783            if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
8784                tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
8785                return 0;
8786            }
8787        }
8788        let hidden = self.hidden_size;
8789        let mut skills = Vec::new();
8790        for (idx, id, _phi) in self.dynamic_skills() {
8791            if let Some(sel) = model.header.skills[idx].selection.as_ref() {
8792                if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
8793                    skills.push(rs);
8794                }
8795            }
8796        }
8797        if skills.is_empty() {
8798            return 0;
8799        }
8800        // Skills should share a phi_layer; warn (not fail) if they don't.
8801        let phi = skills[0].phi_layer;
8802        if skills.iter().any(|s| s.phi_layer != phi) {
8803            tracing::warn!("routable skills disagree on phi_layer; using {phi}");
8804        }
8805        let n = skills.len();
8806        self.set_dyn_phi_layer(Some(phi));
8807        self.dyn_router = Some(DynRouter::new(skills));
8808        n
8809    }
8810
8811    /// Human-readable switch log from the last dynamic-routed generation.
8812    pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
8813        self.dyn_router
8814            .as_ref()
8815            .map(|r| r.switches.clone())
8816            .unwrap_or_default()
8817    }
8818
8819    /// LM head: hidden → logits [vocab_size]. The dominant matvec of
8820    /// every decode step — row-parallel on the worker pool.
8821    fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
8822        let rows = self.weights.lm_head.rows();
8823        let mut logits = attention::take_buf(rows.min(self.vocab_size));
8824        self.weights
8825            .lm_head
8826            .matvec(hidden, &mut logits, self.pool.as_deref());
8827        logits.resize(self.vocab_size, 0.0);
8828        if let Some(m) = self.logit_multiplier {
8829            for l in logits.iter_mut() {
8830                *l *= m;
8831            }
8832        }
8833        if let Some(c) = self.final_softcap {
8834            for l in logits.iter_mut() {
8835                *l = c * (*l / c).tanh();
8836            }
8837        }
8838        if let Some(cm) = self.head_clusters.as_ref() {
8839            self.hierarchical_head_logprobs(hidden, cm, &mut logits);
8840        }
8841        logits
8842    }
8843
8844    /// Two-level head (Cortiq Embryo): in place, logits[v] ← log p(v) =
8845    /// (lc[c] − lse(lc)) + (logit[v] − lse over v's cluster block), c = v / S.
8846    fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
8847        let h = hidden.len();
8848        let ncl = cm.len() / h.max(1);
8849        if ncl == 0 || logits.len() % ncl != 0 {
8850            return;
8851        }
8852        let cs = logits.len() / ncl;
8853        // cluster logits + log-softmax
8854        let mut lc = vec![0.0f32; ncl];
8855        for c in 0..ncl {
8856            let row = &cm[c * h..(c + 1) * h];
8857            let mut s = 0.0f32;
8858            for j in 0..h {
8859                s += row[j] * hidden[j];
8860            }
8861            lc[c] = s;
8862        }
8863        let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8864        let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
8865        for c in 0..ncl {
8866            let blk = &mut logits[c * cs..(c + 1) * cs];
8867            let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8868            let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
8869            let add = lc[c] - lse - bl;
8870            for v in blk.iter_mut() {
8871                *v += add;
8872            }
8873        }
8874    }
8875
8876    /// Prefill `ids` and return the next-token logits — what the model
8877    /// would predict next, WITHOUT committing to generation (introspection
8878    /// for `cortiq explain`). Clears and repopulates the KV cache; leaves
8879    /// the active overlay untouched.
8880    pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
8881        self.kv_cache.clear();
8882        self.kv_history.clear();
8883        let mut hidden = vec![0.0f32; self.hidden_size];
8884        for (pos, &id) in ids.iter().enumerate() {
8885            let emb = self.embed_single(id);
8886            hidden = self.forward_layers(&emb, pos, task_mask);
8887        }
8888        inference::rms_norm_into(
8889            &hidden,
8890            &self.weights.final_norm,
8891            self.rms_eps,
8892            self.norm_style,
8893            &mut self.ws.n1,
8894        );
8895        self.lm_head_forward(&self.ws.n1)
8896    }
8897}
8898
8899/// Convenience: deterministic tiny pipeline for tests.
8900pub fn create_test_pipeline(
8901    hidden_size: usize,
8902    intermediate_size: usize,
8903    num_heads: usize,
8904    num_kv_heads: usize,
8905    head_dim: usize,
8906    num_layers: usize,
8907    vocab_size: usize,
8908) -> Pipeline {
8909    // Small pseudo-random weights: constant weights make attention
8910    // degenerate and hide indexing bugs.
8911    let synth = |n: usize, salt: usize| -> Vec<f32> {
8912        (0..n)
8913            .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
8914            .collect()
8915    };
8916    let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
8917        QTensor::from_f32(synth(rows * cols, salt), rows, cols)
8918    };
8919    let layer_weights: Vec<LayerWeights> = (0..num_layers)
8920        .map(|li| LayerWeights {
8921            input_norm: vec![1.0; hidden_size],
8922            post_norm: vec![1.0; hidden_size],
8923            attn_out_norm: None,
8924            ffn_out_norm: None,
8925            layer_scale: None,
8926            ffn: FfnKind::Dense(DenseFfn {
8927                gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
8928                up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
8929                down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
8930                act: Act::Silu,
8931                down_t: None,
8932            segs: Vec::new(),
8933        }),
8934            attn: AttnKind::Full {
8935                bias: None,
8936                wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
8937                wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
8938                wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
8939                wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
8940                q_norm: None,
8941                k_norm: None,
8942                output_gate: false,
8943                softplus_gate: None,
8944            },
8945        })
8946        .collect();
8947
8948    Pipeline::new(
8949        Tokenizer::byte_level(),
8950        PipelineWeights {
8951            embed_tokens: qt(vocab_size, hidden_size, 100),
8952            layers: layer_weights,
8953            lm_head: qt(vocab_size, hidden_size, 200),
8954            final_norm: vec![1.0; hidden_size],
8955        },
8956        hidden_size,
8957        intermediate_size,
8958        num_heads,
8959        num_kv_heads,
8960        head_dim,
8961        num_layers,
8962        num_layers, // physical_layers = num_layers (non-looped)
8963        false,      // loop_final_norm
8964        vocab_size,
8965        1e-6,
8966        10_000.0,
8967        NormStyle::Qwen,
8968        4096,
8969        SamplerConfig {
8970            seed: Some(42),
8971            ..Default::default()
8972        },
8973    )
8974}
8975
8976/// Batched dense-FFN: gate/up/down via matmat (element-wise the same
8977/// math as b × dense_ffn — the same dot kernels).
8978/// One mask bit, LSB-first per byte — `TaskMask::ffn_active_indices`'s
8979/// convention.
8980#[inline]
8981fn mask_bit(row: &[u8], j: usize) -> bool {
8982    (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
8983}
8984
8985/// Zero the CLOSED neurons' activations in a [rows × inter] panel — the
8986/// masked-inference fast path's whole trick: full fused quant compute,
8987/// then the mask lands on the ACTIVATIONS, which is arithmetically the
8988/// pruned network without touching a quantized weight byte. Whole open
8989/// bytes (0xFF = 8 open neurons) skip in one test.
8990/// `CMF_FFN_MASK_GAIN` — Patent 12 FIG. 4, variance-preserving
8991/// rescaling: truncation removes a share of the layer's output energy,
8992/// so the survivors are scaled up to put the variance back where the
8993/// downstream norm expects it. A scalar here; per layer it is
8994/// `sqrt(total energy / kept energy)`.
8995fn mask_gain() -> f32 {
8996    static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
8997    *G.get_or_init(|| {
8998        std::env::var("CMF_FFN_MASK_GAIN")
8999            .ok()
9000            .and_then(|v| v.parse().ok())
9001            .unwrap_or(1.0)
9002    })
9003}
9004
9005fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
9006    // With CMF_FFN_MEANFILL a closed neuron contributes its average
9007    // instead of nothing — same bytes read, one constant restored.
9008    let fill = meanfill().and_then(|(i, v)| {
9009        let li = crate::gpu::cur_layer();
9010        (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
9011    });
9012    for r in 0..rows {
9013        let base = r * inter;
9014        for (bi, &byte) in row.iter().enumerate() {
9015            if byte == 0xFF {
9016                continue;
9017            }
9018            let j0 = bi * 8;
9019            for bit in 0..8 {
9020                let j = j0 + bit;
9021                if j < inter && byte & (1 << bit) == 0 {
9022                    g[base + j] = fill.map_or(0.0, |f| f[j]);
9023                }
9024            }
9025        }
9026    }
9027    let gain = mask_gain();
9028    if gain != 1.0 {
9029        for v in g[..rows * inter].iter_mut() {
9030            *v *= gain;
9031        }
9032    }
9033}
9034
9035/// True when neuron `i`'s bit is set (no mask = everything runs).
9036#[inline]
9037fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
9038    row.is_none_or(|r| mask_bit(r, i))
9039}
9040
9041/// Every bit below `n` set — the common case for a tube file's CORE,
9042/// where only the tube bits vary per task.
9043fn all_bits_on(row: &[u8], n: usize) -> bool {
9044    (0..n).all(|i| mask_bit(row, i))
9045}
9046
9047/// `CMF_TUBE_TOPK` — how many tubes a TOKEN may open (0 = the task mask
9048/// decides alone). This is the dense FFN read as a mixture: the tubes
9049/// are the experts a k-means over `gate_proj` rows found, and the token
9050/// picks among them. `CMF_TUBE_SCORE=gate` scores a tube by its own
9051/// gate (realizable: only `up`/`down` of the losers go unread),
9052/// `=oracle` scores by the true `silu(gate)·up` mass (the ceiling —
9053/// only `down` is saved, and the selection has read what it predicts).
9054fn tube_topk() -> usize {
9055    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9056    *K.get_or_init(|| {
9057        std::env::var("CMF_TUBE_TOPK")
9058            .ok()
9059            .and_then(|v| v.parse().ok())
9060            .unwrap_or(0)
9061    })
9062}
9063
9064fn tube_score_oracle() -> bool {
9065    static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9066    *O.get_or_init(|| {
9067        std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle")
9068    })
9069}
9070
9071/// The routed arm of `tube_ffn`: a token opens only its best `k` tubes.
9072/// At `b == 1` (decode) the losers are genuinely never read — that is
9073/// the speed. At `b > 1` (the scoring sweep) every tube is computed and
9074/// the losers' activations are zeroed instead: same arithmetic, so the
9075/// perplexity is the routed model's, measured without a per-token
9076/// gather in the middle of a GEMM.
9077fn tube_ffn_routed(
9078    d: &DenseFfn,
9079    xs: &[f32],
9080    b: usize,
9081    pool: Option<&Pool>,
9082    mask_row: Option<&[u8]>,
9083    k: usize,
9084) -> Vec<f32> {
9085    let hidden = d.down_proj.rows();
9086    let core = d.gate_proj.rows();
9087    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
9088    let mut out = match (b, core_full, mask_row) {
9089        (1, true, _) => dense_ffn(d, xs, pool),
9090        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
9091        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
9092        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
9093    };
9094    let cand: Vec<usize> = (0..d.segs.len())
9095        .filter(|&i| tube_bit(mask_row, d.segs[i].start))
9096        .collect();
9097    if cand.is_empty() {
9098        return out;
9099    }
9100    // gate (and, where the score or the batch needs it, up) per tube.
9101    // The SCORE is taken at the point the serving path could take it:
9102    // off the gate alone, or off the finished activation for the oracle.
9103    let oracle = tube_score_oracle();
9104    let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
9105    let mut scores = vec![0f32; b * cand.len()];
9106    for (ci, &i) in cand.iter().enumerate() {
9107        let seg = &d.segs[i];
9108        let w = seg.width;
9109        let mut g = vec![0.0f32; b * w];
9110        if b == 1 {
9111            seg.gate.matvec(xs, &mut g, pool);
9112        } else {
9113            seg.gate.matmat(xs, b, &mut g, pool);
9114        }
9115        for v in g.iter_mut() {
9116            *v = Act::Silu.combine(*v, 1.0);
9117        }
9118        if !oracle {
9119            for t in 0..b {
9120                scores[t * cand.len() + ci] =
9121                    g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
9122            }
9123        }
9124        if oracle || b > 1 {
9125            let mut u = vec![0.0f32; b * w];
9126            if b == 1 {
9127                seg.up.matvec(xs, &mut u, pool);
9128            } else {
9129                seg.up.matmat(xs, b, &mut u, pool);
9130            }
9131            for (a, &v) in g.iter_mut().zip(u.iter()) {
9132                *a *= v;
9133            }
9134            if oracle {
9135                for t in 0..b {
9136                    scores[t * cand.len() + ci] =
9137                        g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
9138                }
9139            }
9140        }
9141        acts.push(g);
9142    }
9143    // per-token scores and the winners
9144    let keep = k.min(cand.len());
9145    let mut scratch: Vec<f32> = Vec::new();
9146    for t in 0..b {
9147        let mut sc: Vec<(f32, usize)> = (0..cand.len())
9148            .map(|ci| (scores[t * cand.len() + ci], ci))
9149            .collect();
9150        sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
9151        let mut alive = vec![false; cand.len()];
9152        for &(_, ci) in sc.iter().take(keep) {
9153            alive[ci] = true;
9154        }
9155        if b > 1 {
9156            for (ci, a) in acts.iter_mut().enumerate() {
9157                if !alive[ci] {
9158                    let w = d.segs[cand[ci]].width;
9159                    a[t * w..(t + 1) * w].fill(0.0);
9160                }
9161            }
9162        } else {
9163            // decode: finish only the winners — the losers' up/down
9164            // (and, with the gate score, everything but their gate)
9165            // are never touched.
9166            for (ci, &i) in cand.iter().enumerate() {
9167                if !alive[ci] {
9168                    continue;
9169                }
9170                let seg = &d.segs[i];
9171                let w = seg.width;
9172                let g = &mut acts[ci];
9173                if !tube_score_oracle() {
9174                    scratch.clear();
9175                    scratch.resize(w, 0.0);
9176                    seg.up.matvec(xs, &mut scratch, pool);
9177                    for (a, &v) in g.iter_mut().zip(scratch.iter()) {
9178                        *a *= v;
9179                    }
9180                }
9181                let mut acc = vec![0.0f32; hidden];
9182                seg.down.matvec(g, &mut acc, pool);
9183                for (o, a) in out.iter_mut().zip(&acc) {
9184                    *o += *a;
9185                }
9186            }
9187        }
9188    }
9189    if b > 1 {
9190        for (ci, &i) in cand.iter().enumerate() {
9191            let seg = &d.segs[i];
9192            let mut acc = vec![0.0f32; b * hidden];
9193            seg.down.matmat(&acts[ci], b, &mut acc, pool);
9194            for (o, a) in out.iter_mut().zip(&acc) {
9195                *o += *a;
9196            }
9197        }
9198    }
9199    out
9200}
9201
9202/// FFN of a defragged tube layer: the always-on core plus the tubes the
9203/// task mask switches on. Each tube is a normal tensor triple, so the
9204/// same kernels run it and an inactive tube's bytes are never read —
9205/// that is the whole point of the defrag (a scattered mask cannot skip
9206/// bytes; a contiguous one is just a smaller matrix).
9207fn tube_ffn(
9208    d: &DenseFfn,
9209    xs: &[f32],
9210    b: usize,
9211    pool: Option<&Pool>,
9212    mask_row: Option<&[u8]>,
9213) -> Vec<f32> {
9214    if tube_topk() > 0 {
9215        return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
9216    }
9217    let hidden = d.down_proj.rows();
9218    let core = d.gate_proj.rows();
9219    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
9220    let mut out = match (b, core_full, mask_row) {
9221        (1, true, _) => dense_ffn(d, xs, pool),
9222        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
9223        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
9224        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
9225    };
9226    TUBE_SCRATCH.with(|sc| {
9227    let mut sc = sc.borrow_mut();
9228    let [g, u, acc] = &mut *sc;
9229    for seg in &d.segs {
9230        if !tube_bit(mask_row, seg.start) {
9231            continue;
9232        }
9233        let w = seg.width;
9234        g.resize(b * w, 0.0);
9235        if b == 1 && d.act == Act::Silu && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
9236        {
9237            // g holds silu(gate)·up.
9238        } else {
9239            u.resize(b * w, 0.0);
9240            if b == 1 {
9241                QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
9242            } else {
9243                seg.gate.matmat(xs, b, g, pool);
9244                seg.up.matmat(xs, b, u, pool);
9245            }
9246            for i in 0..b * w {
9247                g[i] = d.act.combine(g[i], u[i]);
9248            }
9249        }
9250        acc.resize(b * hidden, 0.0);
9251        acc.fill(0.0);
9252        if b == 1 {
9253            seg.down.matvec(g, acc, pool);
9254        } else {
9255            seg.down.matmat(g, b, acc, pool);
9256        }
9257        for (o, a) in out.iter_mut().zip(acc.iter()) {
9258            *o += *a;
9259        }
9260    }
9261    out
9262    })
9263}
9264
9265thread_local! {
9266    /// gate / up / down-accumulator scratch for the tube loop — a tube
9267    /// runs once per layer per token, and a fresh Vec each time is a
9268    /// malloc per tube per layer per token.
9269    static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
9270        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
9271}
9272
9273fn dense_ffn_batch(
9274    d: &DenseFfn,
9275    xs: &[f32],
9276    b: usize,
9277    pool: Option<&Pool>,
9278    mask_row: Option<&[u8]>,
9279) -> Vec<f32> {
9280    let inter = d.gate_proj.rows();
9281    let hidden = d.down_proj.rows();
9282    // Fused on-device SwiGLU when the device is in play: three separate
9283    // `matmat` calls are three round trips per layer, and the gate/up
9284    // panels (b × inter — 22 MB each at a 512-token chunk) cross the bus
9285    // twice for nothing. The kernel already existed for the image DiT;
9286    // the LLM prefill was simply never wired to it. A task mask needs the
9287    // activations on the host between the halves, so it keeps the CPU
9288    // arm below.
9289    if mask_row.is_none()
9290        && d.act == Act::Silu
9291        && b >= 32
9292        && crate::gpu::enabled_here()
9293        && !crate::gpu::mm_killed()
9294        // The refit pass needs this layer's activations on the host; the
9295        // fused chain keeps them on the device. Refusing it here costs
9296        // one round trip and keeps every GEMM on the card — the
9297        // alternative was running the whole calibration on the CPU.
9298        && refit_dir().is_none()
9299        // Same for the mass/hit probes. The accumulator at the bottom of
9300        // this function only sees `g` when `g` came back to the host, so
9301        // a fused batch would leave it summing nothing — a probe that
9302        // reports zeros rather than failing, which is worse.
9303        && !ffn_probe_active()
9304    {
9305        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
9306            d.gate_proj.mapped_q4t(),
9307            d.up_proj.mapped_q4t(),
9308            d.down_proj.mapped_q4t(),
9309        ) {
9310            let mut out = vec![0.0f32; b * hidden];
9311            if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
9312                return out;
9313            }
9314        }
9315        // The q4tp twin (same kernel family, scale from the row ladder) —
9316        // the DiT has run it in production since the pipeline containers;
9317        // the LLM prefill was simply never wired to it, so a q4tp model's
9318        // prefill panels stayed on the CPU.
9319        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
9320            d.gate_proj.mapped_q4tp(),
9321            d.up_proj.mapped_q4tp(),
9322            d.down_proj.mapped_q4tp(),
9323        ) {
9324            let mut out = vec![0.0f32; b * hidden];
9325            if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
9326                return out;
9327            }
9328        }
9329    }
9330    let mut g = vec![0.0f32; b * inter];
9331    d.gate_proj.matmat(xs, b, &mut g, pool);
9332    let mut u = vec![0.0f32; b * inter];
9333    d.up_proj.matmat(xs, b, &mut u, pool);
9334    if gate_topk() > 0 && d.act == Act::Silu {
9335        for t in 0..b {
9336            let row = &mut g[t * inter..(t + 1) * inter];
9337            for v in row.iter_mut() {
9338                *v = Act::Silu.combine(*v, 1.0);
9339            }
9340            keep_top_k(row, gate_topk());
9341        }
9342        for i in 0..b * inter {
9343            g[i] *= u[i];
9344        }
9345    } else {
9346        for i in 0..b * inter {
9347            g[i] = d.act.combine(g[i], u[i]);
9348        }
9349    }
9350    if let Some(row) = mask_row {
9351        zero_masked_cols(&mut g, b, inter, row);
9352    }
9353    if oracle_topk() > 0 {
9354        for t in 0..b {
9355            keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
9356        }
9357    }
9358    let mut out = vec![0.0f32; b * hidden];
9359    d.down_proj.matmat(&g, b, &mut out, pool);
9360    if refit_dir().is_some() {
9361        let li = crate::gpu::cur_layer();
9362        if li >= 0 {
9363            refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
9364        }
9365    }
9366    // The DTG-MA probe, on the batched path: one prefill sweep gives the
9367    // same per-neuron statistic the per-position probe does, and on a 27B
9368    // that is minutes instead of hours.
9369    FFN_PROBE.with(|pr| {
9370        if let Some(acc) = pr.borrow_mut().as_mut() {
9371            let li = crate::gpu::cur_layer();
9372            if li < 0 {
9373                return;
9374            }
9375            let Some(row) = acc.get_mut(li as usize) else {
9376                return;
9377            };
9378            let sq = probe_sq();
9379            for t in 0..b {
9380                for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
9381                    *a += if sq { (v as f64) * (v as f64) } else { (v as f64).abs() };
9382                }
9383            }
9384        }
9385    });
9386    out
9387}
9388
9389/// Batched MoE-FFN: router batched, positions are GROUPED by expert —
9390/// an expert's weights are read once for all its positions in the chunk
9391/// (the main prefill-GEMM win on MoE: 960MB/token of 35B experts).
9392/// Accumulate per-channel activation energy for `CMF_RMS_TRACE`.
9393fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
9394    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9395    static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9396    let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
9397    let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
9398    if (!on && !dump) || b == 0 {
9399        return;
9400    }
9401    let hidden = xs.len() / b;
9402    if on {
9403        let mut acc = m.act_sq.borrow_mut();
9404        if acc.len() < hidden {
9405            acc.resize(hidden, 0.0);
9406        }
9407        for t in 0..b {
9408            let row = &xs[t * hidden..(t + 1) * hidden];
9409            for (a, &v) in acc.iter_mut().zip(row) {
9410                *a += (v as f64) * (v as f64);
9411            }
9412        }
9413    }
9414    if dump {
9415        // Cap the capture: the covariance needs a few thousand rows, and a
9416        // whole prefill of every layer would be gigabytes for no extra rank.
9417        let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
9418            .ok()
9419            .and_then(|v| v.parse().ok())
9420            .unwrap_or(4096);
9421        let mut rows = m.act_rows.borrow_mut();
9422        if rows.len() < cap * hidden {
9423            let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
9424            rows.extend_from_slice(&xs[..take * hidden]);
9425        }
9426    }
9427}
9428
9429/// Send-able cursor over a Vec-of-Vecs: each pool worker writes only its
9430/// own slots (disjoint by construction in the caller).
9431#[derive(Clone, Copy)]
9432struct SendVecs(*mut Vec<f32>);
9433unsafe impl Send for SendVecs {}
9434unsafe impl Sync for SendVecs {}
9435impl SendVecs {
9436    #[inline]
9437    fn at(self, i: usize) -> *mut Vec<f32> {
9438        unsafe { self.0.add(i) }
9439    }
9440}
9441
9442fn moe_ffn_batch(
9443    m: &MoeFfn,
9444    xs: &[f32],
9445    b: usize,
9446    hidden: usize,
9447    pool: Option<&Pool>,
9448    allowed: Option<&[bool]>,
9449) -> Vec<f32> {
9450    accumulate_act(m, xs, b);
9451    let ne = m.experts.len();
9452    let mut logits = vec![0.0f32; b * ne];
9453    match &m.resonance {
9454        Some(r) => {
9455            let hdim = xs.len() / b.max(1);
9456            for bi in 0..b {
9457                r.scores(&xs[bi * hdim..(bi + 1) * hdim], &mut logits[bi * ne..(bi + 1) * ne]);
9458            }
9459        }
9460        None => m.router.matmat(xs, b, &mut logits, pool),
9461    }
9462
9463    // Assignments: expert → [(position, weight)] — same routing as
9464    // moe_ffn, per position (see `moe_route`).
9465    let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
9466    {
9467        let mut st = m.stats.borrow_mut();
9468        if st.len() < ne {
9469            st.resize(ne, 0);
9470        }
9471        for bi in 0..b {
9472            let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
9473            for &e in &idx {
9474                st[e] += 1;
9475                assign[e].push((bi, p[e] / wsum));
9476            }
9477        }
9478    }
9479
9480    let mut out = vec![0.0f32; b * hidden];
9481    let cols = m.experts[0].gate_proj.cols();
9482    let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
9483        let sb = list.len();
9484        let mut sub = vec![0.0f32; sb * cols];
9485        for (k, &(bi, _)) in list.iter().enumerate() {
9486            sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
9487        }
9488        let eo = dense_ffn_batch(d, &sub, sb, pool, None);
9489        for (k, &(bi, w)) in list.iter().enumerate() {
9490            for i in 0..hidden {
9491                out[bi * hidden + i] += w * eo[k * hidden + i];
9492            }
9493        }
9494    };
9495    // Routed experts: the panels are TINY (b·top_k spread over every
9496    // expert — a few positions each), so a pool dispatch per expert is
9497    // pure barrier cost. Invert the parallelism: workers take WHOLE
9498    // experts (serial math inside), then one deterministic scatter in
9499    // expert order — the exact accumulation order the serial loop had.
9500    let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
9501    if pool.is_some() && active.len() >= 8 {
9502        let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
9503        {
9504            let panel_ptr = SendVecs(panels.as_mut_ptr());
9505            // Capture only the expert table: `m` itself carries RefCell
9506            // stats and must not cross the pool boundary.
9507            let experts = &m.experts;
9508            let (active_r, assign_r) = (&active, &assign);
9509            let run = |start: usize, end: usize| {
9510                for ai in start..end {
9511                    let e = active_r[ai];
9512                    let list = &assign_r[e];
9513                    let sb = list.len();
9514                    let mut sub = vec![0.0f32; sb * cols];
9515                    for (k, &(bi, _)) in list.iter().enumerate() {
9516                        sub[k * cols..(k + 1) * cols]
9517                            .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
9518                    }
9519                    // SAFETY: each worker owns a disjoint panels[ai].
9520                    unsafe {
9521                        *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
9522                    }
9523                }
9524            };
9525            match pool {
9526                Some(p) => p.run_rows(active.len(), &run),
9527                None => run(0, active.len()),
9528            }
9529        }
9530        for (ai, &e) in active.iter().enumerate() {
9531            for (k, &(bi, w)) in assign[e].iter().enumerate() {
9532                let eo = &panels[ai][k * hidden..(k + 1) * hidden];
9533                for i in 0..hidden {
9534                    out[bi * hidden + i] += w * eo[i];
9535                }
9536            }
9537        }
9538    } else {
9539        for &e in &active {
9540            run_expert(&m.experts[e], &assign[e], &mut out);
9541        }
9542    }
9543    if let Some((se, gate)) = &m.shared {
9544        let all: Vec<(usize, f32)> = if let Some(gate) = gate {
9545            let mut gl = vec![0.0f32; b];
9546            gate.matmat(xs, b, &mut gl, pool);
9547            (0..b)
9548                .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
9549                .collect()
9550        } else {
9551            (0..b).map(|bi| (bi, 1.0)).collect()
9552        };
9553        run_expert(se, &all, &mut out);
9554    }
9555    out
9556}
9557
9558thread_local! {
9559    /// gate/up activation scratch for the dense FFN paths (single uses
9560    /// two slots, the fused pair all four) — these were fresh
9561    /// intermediate-size Vecs on every layer of every token.
9562    static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
9563        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
9564}
9565
9566/// Dense SwiGLU FFN through QTensor matvecs (any storage).
9567fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
9568    // Per-token sparsity, when the file was built for it: gate first,
9569    // then only the chosen neurons' up/down rows leave the mmap.
9570    if gate_topk() > 0
9571        && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
9572    {
9573        return out;
9574    }
9575    // Whole-FFN GPU submit (этап 4.2 increment): gate → silu·up → down
9576    // chained in ONE command buffer with the intermediate activations
9577    // resident on the device — 3 per-op polls become 1 per layer. The
9578    // moe_block backend already implements exactly this chain; a dense
9579    // FFN is one expert with weight 1. Runtime probe: the chain still
9580    // pays one submit+poll per layer — alternate it against the pure-CPU
9581    // FFN and keep whichever is faster on this machine.
9582    // q1 FFNs offload at any practical size: the q1 CPU kernel is
9583    // compute-bound, so the UMA threshold logic does not apply — the
9584    // probe measures and decides either way.
9585    if crate::gpu::enabled_here()
9586        && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
9587    {
9588        let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
9589            crate::gpu::ProbeArm::Gpu
9590        } else {
9591            crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
9592        };
9593        match arm {
9594            crate::gpu::ProbeArm::Gpu => {
9595                let t0 = std::time::Instant::now();
9596                if let Some(out) = dense_ffn_gpu(d, x, pool) {
9597                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
9598                    return out;
9599                }
9600                // Declined: no timing exists, so say so. Silence here is
9601                // what left `ffn` undecided for 9000 calls and cost a
9602                // failed device attempt on half of them.
9603                crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
9604            }
9605            crate::gpu::ProbeArm::CpuTimed => {
9606                let t0 = std::time::Instant::now();
9607                let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
9608                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
9609                return out;
9610            }
9611            crate::gpu::ProbeArm::Cpu => {
9612                return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
9613            }
9614        }
9615    }
9616    dense_ffn_cpu(d, x, pool)
9617}
9618
9619/// The pure-CPU dense-FFN body (also the fallback of every GPU refusal).
9620fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
9621    let inter = d.gate_proj.rows();
9622    FFN_SCRATCH.with(|s| {
9623        let mut s = s.borrow_mut();
9624        let [g, u, ..] = &mut *s;
9625        g.resize(inter, 0.0);
9626        // Fused gate+up+silu: one dispatch, no separate silu pass.
9627        // Falls back to matvec_many + silu loop for unsupported dtypes.
9628        if gate_topk() > 0 {
9629            // Gate first, select, and only then pay for `up`: the
9630            // measurement arm computes both and zeroes the losers, which
9631            // is the same arithmetic.
9632            u.resize(inter, 0.0);
9633            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
9634            for i in 0..inter {
9635                g[i] = Act::Silu.combine(g[i], 1.0);
9636            }
9637            keep_top_k(g, gate_topk());
9638            for i in 0..inter {
9639                g[i] *= u[i];
9640            }
9641        } else if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
9642            // g now holds silu(gate)·up directly.
9643        } else {
9644            u.resize(inter, 0.0);
9645            // Multi-matrix job: gate+up under one pool dispatch.
9646            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
9647            for i in 0..inter {
9648                g[i] = d.act.combine(g[i], u[i]);
9649            }
9650        }
9651        // DTG-MA bake probe (Patent 2): accumulate this layer's
9652        // per-neuron activation mass while a probe pass is active.
9653        // `CMF_FFN_PROBE_TOPK=k` switches the statistic from mass to a
9654        // HIT COUNT — how many tokens rank the neuron in their own top
9655        // k. Mass asks "how loud is this neuron overall", the count
9656        // asks "how often does this task actually need it", and the two
9657        // rank neurons differently whenever a few tokens are loud.
9658        FFN_PROBE.with(|pr| {
9659            if let Some(acc) = pr.borrow_mut().as_mut() {
9660                let li = crate::gpu::cur_layer();
9661                if li >= 0 {
9662                    if let Some(row) = acc.get_mut(li as usize) {
9663                        match probe_topk() {
9664                            0 if probe_sq() => {
9665                                for (a, &v) in row.iter_mut().zip(g.iter()) {
9666                                    *a += (v as f64) * (v as f64);
9667                                }
9668                            }
9669                            0 if probe_signed() => {
9670                                for (a, &v) in row.iter_mut().zip(g.iter()) {
9671                                    *a += v as f64;
9672                                }
9673                            }
9674                            0 => {
9675                                for (a, &v) in row.iter_mut().zip(g.iter()) {
9676                                    *a += (v as f64).abs();
9677                                }
9678                            }
9679                            k => {
9680                                let n = g.len();
9681                                let k = k.min(n);
9682                                let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
9683                                let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
9684                                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
9685                                });
9686                                let thr = *kth;
9687                                for (a, &v) in row.iter_mut().zip(g.iter()) {
9688                                    if v.abs() >= thr {
9689                                        *a += 1.0;
9690                                    }
9691                                }
9692                            }
9693                        }
9694                    }
9695                }
9696            }
9697        });
9698        if oracle_topk() > 0 {
9699            keep_top_k(g, oracle_topk());
9700        }
9701        {
9702            let li = crate::gpu::cur_layer();
9703            if li >= 0 {
9704                adump_row(li as usize, g);
9705            }
9706        }
9707        let mut out = attention::take_buf(d.down_proj.rows());
9708        d.down_proj.matvec(g, &mut out, pool);
9709        out
9710    })
9711}
9712
9713/// Online accumulators for the AWNP refit of a narrowed FFN.
9714///
9715/// The refit needs `Gss = A_SᵀA_S` and `YA = YᵀA_S` per layer, where `A_S`
9716/// are the calibration activations of the KEPT neurons and `Y` the full
9717/// FFN output. Both are small enough to hold; the thing that is not is
9718/// the activations they are built from — a 27B layer would dump a
9719/// gigabyte per thousand tokens. So they are accumulated as the
9720/// calibration runs and written once at the end.
9721///
9722/// `CMF_FFN_REFIT=<dir>` holds `support.<L>.u32` (a u32 count then the
9723/// kept indices) for every layer to accumulate; `CMF_FFN_REFIT_FROM/TO`
9724/// bound the layer span so the accumulators fit in RAM.
9725pub struct RefitAcc {
9726    pub support: Vec<u32>,
9727    pub gss: Vec<f32>,
9728    pub ya: Vec<f32>,
9729    pub hidden: usize,
9730    pub tokens: u64,
9731    /// Activations staged transposed ([ns, t] and [hidden, t]) until the
9732    /// batch is worth a GEMM. The product costs `ns²` to move and add
9733    /// REGARDLESS of how many tokens went into it, so folding 16 chunks
9734    /// into one call cuts that cost 16× — it was 15 TB of traffic per
9735    /// calibration pass at one call per 256 tokens.
9736    pub buf_g: Vec<f32>,
9737    pub buf_o: Vec<f32>,
9738    pub buf_t: usize,
9739}
9740
9741/// The product buffer is SHARED across layers — one 473 MB allocation,
9742/// not one per layer (that was 30 GB of nothing on a 64-layer model).
9743/// It lives under the same lock as the accumulators.
9744type RefitState = (
9745    std::collections::HashMap<usize, RefitAcc>,
9746    Vec<f32>,
9747);
9748
9749static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
9750    std::sync::OnceLock::new();
9751
9752/// Is an FFN probe accumulator installed on this thread? The fused GPU
9753/// FFN must decline while one is, or the probe silently measures zero.
9754fn ffn_probe_active() -> bool {
9755    FFN_PROBE.with(|p| p.borrow().is_some())
9756}
9757
9758fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
9759    REFIT
9760        .get_or_init(|| {
9761            std::env::var("CMF_FFN_REFIT").ok().map(|d| {
9762                (
9763                    d,
9764                    std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
9765                )
9766            })
9767        })
9768        .as_ref()
9769}
9770
9771/// Accumulate one prefill panel into the layer's refit statistics.
9772fn refit_accumulate(
9773    li: usize,
9774    g: &[f32],
9775    b: usize,
9776    inter: usize,
9777    out: &[f32],
9778    hidden: usize,
9779    pool: Option<&Pool>,
9780) {
9781    let Some((dir, map)) = refit_dir() else {
9782        return;
9783    };
9784    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
9785    let (from, to) = *SPAN.get_or_init(|| {
9786        let g = |k: &str, d: usize| {
9787            std::env::var(k)
9788                .ok()
9789                .and_then(|v| v.parse().ok())
9790                .unwrap_or(d)
9791        };
9792        (g("CMF_FFN_REFIT_FROM", 0), g("CMF_FFN_REFIT_TO", usize::MAX))
9793    });
9794    if li < from || li > to {
9795        return;
9796    }
9797    let mut guard = map.lock().unwrap();
9798    let (map, shared) = &mut *guard;
9799    let acc = match map.entry(li) {
9800        std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
9801        std::collections::hash_map::Entry::Vacant(e) => {
9802            let path = format!("{dir}/support.{li}.u32");
9803            let Ok(bytes) = std::fs::read(&path) else {
9804                eprintln!("refit: no {path} — layer {li} skipped");
9805                return;
9806            };
9807            let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
9808            let support: Vec<u32> = bytes[4..4 + n * 4]
9809                .chunks_exact(4)
9810                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
9811                .collect();
9812            eprintln!(
9813                "refit: layer {li} support {n} ({:.0} MB of accumulator)",
9814                (n * n + hidden * n) as f64 * 4.0 / 1e6
9815            );
9816            e.insert(RefitAcc {
9817                gss: vec![0.0; n * n],
9818                ya: vec![0.0; hidden * n],
9819                buf_g: Vec::new(),
9820                buf_o: Vec::new(),
9821                buf_t: 0,
9822                support,
9823                hidden,
9824                tokens: 0,
9825            })
9826        }
9827    };
9828    let ns = acc.support.len();
9829    // Stage this chunk transposed; the GEMM fires once the batch is full.
9830    let cap = refit_batch();
9831    if acc.buf_g.is_empty() {
9832        acc.buf_g = vec![0.0; ns * cap];
9833        acc.buf_o = vec![0.0; hidden * cap];
9834    }
9835    let take = b.min(cap - acc.buf_t);
9836    for t in 0..take {
9837        let col = acc.buf_t + t;
9838        for (j, &n) in acc.support.iter().enumerate() {
9839            acc.buf_g[j * cap + col] = g[t * inter + n as usize];
9840        }
9841        for h in 0..hidden {
9842            acc.buf_o[h * cap + col] = out[t * hidden + h];
9843        }
9844    }
9845    acc.buf_t += take;
9846    acc.tokens += take as u64;
9847    if acc.buf_t < cap {
9848        return;
9849    }
9850    let bt = acc.buf_t;
9851    acc.buf_t = 0;
9852    // The GEMM WRITES its C (it zeroes the accumulators it uses), so the
9853    // chunk product lands in scratch and is added on — the one thing that
9854    // silently turns a Gram over 13 000 tokens into a Gram over 256.
9855    // Both products are `C[n, m] += X[n, b] · Yᵀ[b, m]` with X and Y
9856    // stored row-major [·, b] — exactly `gemm_nt_f32`'s shape, so the
9857    // card does them when it is up (this is the whole calibration's
9858    // cost: O(|S|²) per token, 2.9 PFLOP for a 27B pass). The tiled CPU
9859    // loop stays as the fallback. Neither accumulates, so the product
9860    // lands in scratch and is added on.
9861    let RefitAcc {
9862        gss, ya, buf_g, buf_o, ..
9863    } = acc;
9864    let need = (ns * ns).max(hidden * ns);
9865    if shared.len() < need {
9866        shared.resize(need, 0.0);
9867    }
9868    let scratch = &mut shared[..];
9869    let _ = bt;
9870    if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
9871        add_into(gss, &scratch[..ns * ns], pool);
9872        if crate::gpu::gemm_nt_f32_transient(buf_o, buf_g, &mut scratch[..hidden * ns], hidden, cap, ns) {
9873            add_into(ya, &scratch[..hidden * ns], pool);
9874        } else {
9875            accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
9876        }
9877    } else {
9878        accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
9879        accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
9880    }
9881    // No zeroing: the batch is always filled exactly (cap is a multiple
9882    // of the prefill chunk), and a memset of 178 MB a layer would cost
9883    // more than the GEMM.
9884}
9885
9886/// `CMF_FFN_REFIT_BATCH` — tokens staged before each GEMM (default 4096).
9887fn refit_batch() -> usize {
9888    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9889    *B.get_or_init(|| {
9890        std::env::var("CMF_FFN_REFIT_BATCH")
9891            .ok()
9892            .and_then(|v| v.parse().ok())
9893            .unwrap_or(4096)
9894    })
9895}
9896
9897/// `c[m, n] += Σ_t left[m, t]·right[n, t]` — both operands transposed,
9898/// the CPU fallback for the staged batch.
9899fn accum_outer_t(
9900    c: &mut [f32],
9901    m: usize,
9902    n: usize,
9903    b: usize,
9904    left: &[f32],
9905    right: &[f32],
9906    pool: Option<&Pool>,
9907) {
9908    let ptr = SendMut(c.as_mut_ptr());
9909    let body = |i: usize| {
9910        let ptr = &ptr;
9911        let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
9912        for t in 0..b {
9913            let a = left[i * b + t];
9914            if a == 0.0 {
9915                continue;
9916            }
9917            for (j, o) in row.iter_mut().enumerate() {
9918                *o += a * right[j * b + t];
9919            }
9920        }
9921    };
9922    match pool {
9923        Some(p) if m > 1 => p.run_rows(m, &|s, e| {
9924            for i in s..e {
9925                body(i);
9926            }
9927        }),
9928        _ => {
9929            for i in 0..m {
9930                body(i);
9931            }
9932        }
9933    }
9934}
9935
9936/// `dst += src`, spread over the pool — at 118 M floats a layer this is
9937/// not a loop to leave on one core.
9938fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
9939    let n = dst.len().min(src.len());
9940    match pool {
9941        Some(p) if n >= 1 << 16 => {
9942            let ptr = SendMut(dst.as_mut_ptr());
9943            let f = |s: usize, e: usize| {
9944                let ptr = &ptr;
9945                for blk in s..e {
9946                    let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
9947                    for i in a..b {
9948                        unsafe { *ptr.0.add(i) += src[i] };
9949                    }
9950                }
9951            };
9952            p.run_rows(n.div_ceil(4096), &f);
9953        }
9954        _ => {
9955            for (d, v) in dst.iter_mut().zip(&src[..n]) {
9956                *d += *v;
9957            }
9958        }
9959    }
9960}
9961
9962/// `c[m, n] += Σ_t left[t, m]·right[t, n]`, with `left` stored [m, t] and
9963/// `right` [t, n]. Tiled over the rows of `c` so a tile stays in cache
9964/// while each token's `right` row streams past it once, and parallel
9965/// over tiles.
9966fn accum_outer(
9967    c: &mut [f32],
9968    m: usize,
9969    n: usize,
9970    b: usize,
9971    left: &[f32],
9972    right: &[f32],
9973    pool: Option<&Pool>,
9974) {
9975    const TILE: usize = 32;
9976    let tiles = m.div_ceil(TILE);
9977    let cp = SendMut(c.as_mut_ptr());
9978    let body = |ti: usize| {
9979        let cp = &cp;
9980        let i0 = ti * TILE;
9981        let i1 = (i0 + TILE).min(m);
9982        for t in 0..b {
9983            let r = &right[t * n..t * n + n];
9984            for i in i0..i1 {
9985                let a = left[i * b + t];
9986                if a == 0.0 {
9987                    continue;
9988                }
9989                // SAFETY: tiles partition c's rows; workers never overlap.
9990                let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
9991                for (o, v) in row.iter_mut().zip(r) {
9992                    *o += a * *v;
9993                }
9994            }
9995        }
9996    };
9997    match pool {
9998        Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
9999            for ti in s..e {
10000                body(ti);
10001            }
10002        }),
10003        _ => {
10004            for ti in 0..tiles {
10005                body(ti);
10006            }
10007        }
10008    }
10009}
10010
10011/// Write what the calibration accumulated: `gss.<L>.f32` and `ya.<L>.f32`.
10012pub fn refit_flush() -> usize {
10013    let Some((dir, map)) = refit_dir() else {
10014        return 0;
10015    };
10016    let guard = map.lock().unwrap();
10017    let mut n = 0;
10018    for (li, acc) in guard.0.iter() {
10019        // A silently truncated write here is a Gram that reshapes to
10020        // nothing an hour later — say it out loud instead.
10021        let w = |name: &str, v: &[f32]| {
10022            let path = format!("{dir}/{name}.{li}.f32");
10023            let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
10024            match std::fs::write(&path, &bytes) {
10025                Ok(()) => {}
10026                Err(e) => eprintln!("refit: FAILED to write {path} ({} MB): {e}", bytes.len() / 1_000_000),
10027            }
10028        };
10029        w("gss", &acc.gss);
10030        w("ya", &acc.ya);
10031        println!(
10032            "refit L{li}: {} support, {} tokens, hidden {}",
10033            acc.support.len(),
10034            acc.tokens,
10035            acc.hidden
10036        );
10037        n += 1;
10038    }
10039    n
10040}
10041
10042/// `CMF_FFN_ADUMP=<prefix>` — append every probed token's FFN activation
10043/// row to `<prefix>.<layer>.f16`. The co-activation record: which
10044/// neurons fire together, which is what a tube has to group if a token
10045/// is ever going to open one tube instead of sixteen.
10046fn adump_row(li: usize, g: &[f32]) {
10047    use std::io::Write as _;
10048    static FILES: std::sync::OnceLock<
10049        Option<(String, std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>)>,
10050    > = std::sync::OnceLock::new();
10051    let Some((prefix, map)) = FILES
10052        .get_or_init(|| {
10053            std::env::var("CMF_FFN_ADUMP")
10054                .ok()
10055                .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
10056        })
10057        .as_ref()
10058    else {
10059        return;
10060    };
10061    // `CMF_FFN_ADUMP_FROM/_TO` narrow the dump to a layer span, so a big
10062    // calibration run fits on disk in a few passes instead of one.
10063    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
10064    let (from, to) = *SPAN.get_or_init(|| {
10065        let g = |k: &str, d: usize| {
10066            std::env::var(k)
10067                .ok()
10068                .and_then(|v| v.parse().ok())
10069                .unwrap_or(d)
10070        };
10071        (g("CMF_FFN_ADUMP_FROM", 0), g("CMF_FFN_ADUMP_TO", usize::MAX))
10072    });
10073    if li < from || li > to {
10074        return;
10075    }
10076    let mut map = map.lock().unwrap();
10077    let f = map.entry(li).or_insert_with(|| {
10078        std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
10079    });
10080    let mut bytes = Vec::with_capacity(g.len() * 2);
10081    for v in g {
10082        bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
10083    }
10084    let _ = f.write_all(&bytes);
10085}
10086
10087/// `CMF_FFN_ORACLE_TOPK` — keep only the k largest |silu(g)·u| of each
10088/// token and zero the rest. Not a serving mode: it is the CEILING of
10089/// contextual sparsity — what a per-token router would be chasing —
10090/// measured by cheating, since the selection reads the very activations
10091/// it would have to predict.
10092fn oracle_topk() -> usize {
10093    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10094    *K.get_or_init(|| {
10095        std::env::var("CMF_FFN_ORACLE_TOPK")
10096            .ok()
10097            .and_then(|v| v.parse().ok())
10098            .unwrap_or(0)
10099    })
10100}
10101
10102/// `CMF_FFN_GATE_TOPK` — the REALIZABLE cousin of the oracle: rank the
10103/// neurons by their gate alone (which the kernel has computed anyway
10104/// before it reads `up`), keep the k best, and drop the rest. Every
10105/// dropped neuron's `up` row and `down` column stay unread, so this is
10106/// the sparsity a serving path can actually take without a router.
10107fn gate_topk() -> usize {
10108    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10109    *K.get_or_init(|| {
10110        std::env::var("CMF_FFN_GATE_TOPK")
10111            .ok()
10112            .and_then(|v| v.parse().ok())
10113            .unwrap_or(0)
10114    })
10115}
10116
10117/// `CMF_FFN_GATE_BLOCK` — select in blocks of B neurons instead of one
10118/// by one. A scattered per-neuron choice cannot be read efficiently (a
10119/// row at a time, no prefetch runway); a block of 32 is a contiguous
10120/// 32-row slab of `up` and of the transposed `down`, which the ordinary
10121/// kernels stream. The question the measurement answers is what the
10122/// block costs in quality.
10123fn gate_block() -> usize {
10124    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10125    *B.get_or_init(|| {
10126        std::env::var("CMF_FFN_GATE_BLOCK")
10127            .ok()
10128            .and_then(|v| v.parse().ok())
10129            .unwrap_or(1)
10130    })
10131}
10132
10133/// Zero all but the `k` largest BLOCKS (by summed square) of a row.
10134fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
10135    let n = g.len();
10136    let nb = n.div_ceil(block);
10137    let kb = (keep_n.div_ceil(block)).clamp(1, nb);
10138    if kb >= nb {
10139        return;
10140    }
10141    let mut score: Vec<f32> = (0..nb)
10142        .map(|b| {
10143            g[b * block..((b + 1) * block).min(n)]
10144                .iter()
10145                .map(|v| v * v)
10146                .sum::<f32>()
10147        })
10148        .collect();
10149    let mut ord = score.clone();
10150    let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
10151        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
10152    });
10153    let thr = *kth;
10154    for b in 0..nb {
10155        if score[b] < thr {
10156            g[b * block..((b + 1) * block).min(n)].fill(0.0);
10157        }
10158    }
10159    score.clear();
10160}
10161
10162/// Zero all but the `k` largest magnitudes of one token's activation row.
10163fn keep_top_k(g: &mut [f32], k: usize) {
10164    if gate_block() > 1 {
10165        return keep_top_blocks(g, k, gate_block());
10166    }
10167    let n = g.len();
10168    if k == 0 || k >= n {
10169        return;
10170    }
10171    let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
10172    let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
10173        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
10174    });
10175    let thr = *kth;
10176    for v in g.iter_mut() {
10177        if v.abs() < thr {
10178            *v = 0.0;
10179        }
10180    }
10181}
10182
10183/// `CMF_FFN_PROBE_SQ` — accumulate Σa², so the dump divided by the token
10184/// count and square-rooted is the RMS activation trace Patent 12 weights
10185/// its matrices by.
10186fn probe_sq() -> bool {
10187    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10188    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
10189}
10190
10191/// `CMF_FFN_PROBE_SIGNED` — accumulate the SIGNED activation sum
10192/// instead of its magnitude: what a dropped neuron contributes ON
10193/// AVERAGE, which is the bias a narrowed FFN can add back for free.
10194fn probe_signed() -> bool {
10195    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10196    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
10197}
10198
10199/// `CMF_FFN_MEANFILL=<file>` — a masked-out neuron contributes its MEAN
10200/// activation instead of zero (`u32 layers, u32 inter, f32[…]`, the mass
10201/// dump layout, holding per-neuron means). Dropping a neuron outright
10202/// also drops its average contribution, which shifts the layer output by
10203/// a constant; filling the mean back is one add per layer and costs no
10204/// bytes off the bus. This is the measurement arm — in a tube file the
10205/// same correction ships as a per-task bias vector.
10206fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
10207    static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
10208    M.get_or_init(|| {
10209        let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
10210        let b = std::fs::read(&p).ok()?;
10211        let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
10212        let vals: Vec<f32> = b[8..]
10213            .chunks_exact(4)
10214            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10215            .collect();
10216        eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
10217        Some((inter, vals))
10218    })
10219    .as_ref()
10220}
10221
10222/// `CMF_FFN_PROBE_TOPK` — 0 (default) = accumulate mass, k>0 = count
10223/// how often a neuron lands in a token's top k.
10224fn probe_topk() -> usize {
10225    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10226    *K.get_or_init(|| {
10227        std::env::var("CMF_FFN_PROBE_TOPK")
10228            .ok()
10229            .and_then(|v| v.parse().ok())
10230            .unwrap_or(0)
10231    })
10232}
10233
10234thread_local! {
10235    /// DTG-MA activation probe: per-layer per-neuron Σ|silu(g)·u|
10236    /// accumulator, alive only during `Pipeline::probe_ffn_mass`.
10237    static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
10238        const { std::cell::RefCell::new(None) };
10239}
10240
10241/// Per-token structured sparsity, paid for in bytes.
10242///
10243/// The gate is the cheapest third of an FFN and it already says which
10244/// neurons matter: `silu(gate)` near zero means the neuron contributes
10245/// nothing whatever `up` says. So compute every gate, keep the `k`
10246/// loudest, and read ONLY those neurons' `up` rows and `down` rows —
10247/// the latter needs `down_proj` stored transposed, otherwise a neuron's
10248/// down weights are a strided column and "reading only those" costs a
10249/// full cache line each.
10250///
10251/// Returns `None` when the file has no transposed `down` (the caller
10252/// then runs the ordinary dense path).
10253fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
10254    let dt = d.down_t.as_ref()?;
10255    let inter = d.gate_proj.rows();
10256    let hidden = dt.cols();
10257    if k == 0 || k >= inter || d.act != Act::Silu {
10258        return None;
10259    }
10260    DYN_SCRATCH.with(|sc| {
10261        let mut sc = sc.borrow_mut();
10262        let DynScratch { g, mag, live, parts } = &mut *sc;
10263        g.resize(inter, 0.0);
10264        d.gate_proj.matvec(x, g, pool);
10265        for v in g.iter_mut() {
10266            *v = inference::silu(*v);
10267        }
10268        // The k-th largest |silu(gate)| is the threshold; ties keep more,
10269        // which is the safe side.
10270        mag.clear();
10271        mag.extend(g.iter().map(|v| v.abs()));
10272        let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
10273            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
10274        });
10275        let thr = *kth;
10276        live.clear();
10277        live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
10278        let mut out = vec![0.0f32; hidden];
10279        match pool {
10280            Some(p) if live.len() >= 64 => {
10281                let nw = p.n_workers() + 1;
10282                parts.clear();
10283                parts.resize(nw * hidden, 0.0);
10284                let ptr = SendMut(parts.as_mut_ptr());
10285                let n = live.len();
10286                let live_ref: &[u32] = live;
10287                let g_ref: &[f32] = g;
10288                p.run(&|w, workers| {
10289                    let chunk = n.div_ceil(workers);
10290                    let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
10291                    if s >= e {
10292                        return;
10293                    }
10294                    WORKER_SCRATCH.with(|ws| {
10295                        let mut ws = ws.borrow_mut();
10296                        let [scratch, acc] = &mut *ws;
10297                        scratch.resize(hidden.max(x.len()), 0.0);
10298                        acc.clear();
10299                        acc.resize(hidden, 0.0);
10300                        for (o, &nrm) in live_ref[s..e].iter().enumerate() {
10301                            // One neuron of runway: the next row's lines
10302                            // start moving while this one is multiplied.
10303                            if let Some(&nx) = live_ref[s..e].get(o + 1) {
10304                                d.up_proj.prefetch_row(nx as usize);
10305                                dt.prefetch_row(nx as usize);
10306                            }
10307                            let idx = nrm as usize;
10308                            let up = d.up_proj.row_dot(idx, x, scratch);
10309                            let a = g_ref[idx] * up;
10310                            if a != 0.0 {
10311                                dt.add_row_scaled(idx, a, acc, scratch);
10312                            }
10313                        }
10314                        for (j, v) in acc.iter().enumerate() {
10315                            unsafe { *ptr.at(w * hidden + j) = *v };
10316                        }
10317                    });
10318                });
10319                for w in 0..nw {
10320                    for (j, o) in out.iter_mut().enumerate() {
10321                        *o += parts[w * hidden + j];
10322                    }
10323                }
10324            }
10325            _ => {
10326                WORKER_SCRATCH.with(|ws| {
10327                    let mut ws = ws.borrow_mut();
10328                    let [scratch, _acc] = &mut *ws;
10329                    scratch.resize(hidden.max(x.len()), 0.0);
10330                    for &nrm in live.iter() {
10331                        let idx = nrm as usize;
10332                        let up = d.up_proj.row_dot(idx, x, scratch);
10333                        let a = g[idx] * up;
10334                        if a != 0.0 {
10335                            dt.add_row_scaled(idx, a, &mut out, scratch);
10336                        }
10337                    }
10338                });
10339            }
10340        }
10341        Some(out)
10342    })
10343}
10344
10345/// Caller-side scratch of the dynamic path — one allocation per thread,
10346/// not one per layer per token (that alone cost a third of the decode).
10347struct DynScratch {
10348    g: Vec<f32>,
10349    mag: Vec<f32>,
10350    live: Vec<u32>,
10351    parts: Vec<f32>,
10352}
10353
10354thread_local! {
10355    static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
10356        std::cell::RefCell::new(DynScratch {
10357            g: Vec::new(),
10358            mag: Vec::new(),
10359            live: Vec::new(),
10360            parts: Vec::new(),
10361        })
10362    };
10363    /// Pool-worker scratch: the row buffer and this worker's partial sum.
10364    static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
10365        const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
10366}
10367
10368/// `dense_ffn_cpu` with a per-visit mask landing on the activations —
10369/// the masked-inference fast path's decode arm. Full fused quant
10370/// compute, closed neurons zeroed before down: arithmetically the
10371/// pruned network, no dequant, no weight bytes touched.
10372fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
10373    let inter = d.gate_proj.rows();
10374    FFN_SCRATCH.with(|s| {
10375        let mut s = s.borrow_mut();
10376        let [g, u, ..] = &mut *s;
10377        g.resize(inter, 0.0);
10378        if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
10379            // g holds silu(gate)·up.
10380        } else {
10381            u.resize(inter, 0.0);
10382            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
10383            for i in 0..inter {
10384                g[i] = d.act.combine(g[i], u[i]);
10385            }
10386        }
10387        zero_masked_cols(g, 1, inter, mask_row);
10388        let mut out = attention::take_buf(d.down_proj.rows());
10389        d.down_proj.matvec(g, &mut out, pool);
10390        out
10391    })
10392}
10393
10394/// Dense FFN as one GPU submission via the MoE block path (single
10395/// expert, weight 1.0): gate → silu·up → down chained in one command
10396/// buffer, intermediate activations device-resident. None → weights
10397/// not q8-mapped in the primary shard / over the VRAM budget / backend
10398/// refusal → honest CPU path.
10399fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
10400    // The GPU block hardcodes SiLU; GeLU FFNs (Gemma) stay on CPU.
10401    if d.act != Act::Silu {
10402        return None;
10403    }
10404    // Threshold: tiny FFNs are not worth a submission (q1 excepted —
10405    // see the caller's gate).
10406    if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
10407        return None;
10408    }
10409    let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
10410    let mut model_ref = None;
10411    moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
10412    let model = model_ref?;
10413    let hidden = jobs[0].down.1;
10414    let mut out = attention::take_buf(hidden);
10415    if crate::gpu::moe_block(&model, &jobs, &mut out) {
10416        Some(out)
10417    } else {
10418        let mut out = out;
10419        attention::recycle_buf(&mut out);
10420        None
10421    }
10422}
10423
10424/// q8-mapped primary-shard tensor parts for a GPU job: q8_2f carries
10425/// its column field, q8_row runs with empty col slices (the backend
10426/// skips the multiply). Shared by the MoE block and the dense-FFN
10427/// single-job path.
10428#[allow(clippy::type_complexity)]
10429#[allow(clippy::type_complexity)]
10430pub(crate) fn moe_parts(
10431    t: &QTensor,
10432) -> Option<(
10433    &std::sync::Arc<cortiq_core::CmfModel>,
10434    usize,
10435    usize,
10436    usize,
10437    &[f32],
10438    &[f32],
10439    bool,
10440    bool,
10441    bool,
10442)> {
10443    match t {
10444        QTensor::Mapped {
10445            model,
10446            idx,
10447            dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
10448            rows,
10449            cols,
10450            row_scale,
10451            col_field,
10452            ..
10453        } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
10454            model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
10455        )),
10456        // q1: tile-embedded scales — empty rs/col slices, raw xs.
10457        QTensor::Mapped {
10458            model,
10459            idx,
10460            dtype: cortiq_core::TensorDtype::Q1,
10461            rows,
10462            cols,
10463            ..
10464        } => Some((
10465            model,
10466            *idx,
10467            *rows,
10468            *cols,
10469            &[][..],
10470            &[][..],
10471            true,
10472            false,
10473            false,
10474        )),
10475        // q4_tiled: 18-byte tiles with embedded f16 scales — raw xs.
10476        QTensor::Mapped {
10477            model,
10478            idx,
10479            dtype: cortiq_core::TensorDtype::Q4Tiled,
10480            rows,
10481            cols,
10482            ..
10483        } => Some((
10484            model,
10485            *idx,
10486            *rows,
10487            *cols,
10488            &[][..],
10489            &[][..],
10490            false,
10491            true,
10492            false,
10493        )),
10494        // q4tp: same raw-xs contract, different stride and scale plane.
10495        QTensor::Mapped {
10496            model,
10497            idx,
10498            dtype: cortiq_core::TensorDtype::Q4TiledP,
10499            rows,
10500            cols,
10501            ..
10502        } => Some((
10503            model,
10504            *idx,
10505            *rows,
10506            *cols,
10507            &[][..],
10508            &[][..],
10509            false,
10510            true,
10511            false,
10512        )),
10513        // q2tp: the 2-bit expert plane of the mixed profile — q4 family
10514        // for stride bookkeeping, flagged q2 so the trio validation can
10515        // demand a q4tp down.
10516        QTensor::Mapped {
10517            model,
10518            idx,
10519            dtype: cortiq_core::TensorDtype::Q2TiledP,
10520            rows,
10521            cols,
10522            ..
10523        } => Some((
10524            model,
10525            *idx,
10526            *rows,
10527            *cols,
10528            &[][..],
10529            &[][..],
10530            false,
10531            true,
10532            true,
10533        )),
10534        _ => None,
10535    }
10536}
10537
10538/// Map a softmax-router MoE onto the Metal token graph's contract:
10539/// f32 router, gated shared expert, experts uniformly q4tp (or the
10540/// mixed profile: q2tp gate/up over a q4tp down). Sigmoid/bias/τ
10541/// routers, masks, per-expert scales and Gemma's router-input norm
10542/// refuse here — those semantics stay on the CPU path.
10543#[cfg(target_os = "macos")]
10544fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
10545    if m.router_sigmoid
10546        || m.router_input_norm
10547        || m.expert_bias.is_some()
10548        || m.route_tau.is_some()
10549        || m.mask.is_some()
10550        || m.per_expert_scale.is_some()
10551        || m.experts.is_empty()
10552        || m.top_k == 0
10553        || m.resonance.is_some()
10554    {
10555        return None;
10556    }
10557    // The select kernel hard-codes the gated shared expert; an
10558    // ungated one would need its own weight-1 slot.
10559    let (sh, sg) = match &m.shared {
10560        Some((sh, Some(sg))) => (sh, sg),
10561        _ => return None,
10562    };
10563    let (rf, rr, rc) = m.router.f32_parts()?;
10564    if rr != m.experts.len() || rc != hidden {
10565        return None;
10566    }
10567    let (sf, sr, sc) = sg.f32_parts()?;
10568    if sr * sc != hidden {
10569        return None;
10570    }
10571    let inter = m.experts[0].gate_proj.rows();
10572    // The first expert's gate decides the profile; every trio (shared
10573    // included) must agree — the jobs ladder flips ONE kernel for all.
10574    let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
10575    let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
10576        if e.act != Act::Silu
10577            || e.gate_proj.rows() != inter
10578            || e.gate_proj.cols() != hidden
10579            || e.up_proj.rows() != inter
10580            || e.up_proj.cols() != hidden
10581            || e.down_proj.rows() != hidden
10582            || e.down_proj.cols() != inter
10583        {
10584            return None;
10585        }
10586        let pick = |t: &QTensor| -> Option<usize> {
10587            if gu_q2 {
10588                t.mapped_q2tp().map(|(_, i)| i)
10589            } else {
10590                t.mapped_q4tp().map(|(_, i)| i)
10591            }
10592        };
10593        Some((
10594            pick(&e.gate_proj)?,
10595            pick(&e.up_proj)?,
10596            e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
10597        ))
10598    };
10599    let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
10600    let shared = trio(sh)?;
10601    Some(crate::gpu::GpuMoe {
10602        router: rf,
10603        sgate: sf,
10604        experts,
10605        shared,
10606        n_exp: m.experts.len(),
10607        top_k: m.top_k,
10608        inter,
10609        norm_topk: m.norm_topk_prob,
10610        route_scale: m.routed_scaling,
10611        gu_q2,
10612    })
10613}
10614
10615/// Build one gate/up/down GPU job from three tensors. `moe_push_job` is the
10616/// DenseFfn-shaped caller; architectures that keep their experts in their own
10617/// structs (DeepSeek-V4) come here directly.
10618pub(crate) fn moe_push_job_parts<'a>(
10619    gate: &'a QTensor,
10620    up: &'a QTensor,
10621    down: &'a QTensor,
10622    x: &[f32],
10623    w: f32,
10624    swiglu_limit: f32,
10625    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
10626    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
10627) -> Option<()> {
10628    use crate::qtensor::prescale;
10629    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
10630    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
10631    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
10632    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
10633        return None; // mixed-dtype trio — honest CPU path
10634    }
10635    // The 2-bit profile is gate/up q2tp over a PLAIN q4tp down; any other
10636    // 2-bit arrangement stays on the CPU.
10637    if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
10638        return None;
10639    }
10640    if !gq2 && dq2 {
10641        return None;
10642    }
10643    model_ref.get_or_insert_with(|| gm.clone());
10644    let dt = |cf: &[f32]| {
10645        if cf.is_empty() {
10646            cortiq_core::TensorDtype::Q8Row
10647        } else {
10648            cortiq_core::TensorDtype::Q8_2f
10649        }
10650    };
10651    jobs.push(crate::gpu::MoeJob {
10652        gate: (gi, gr, gc, grs),
10653        up: (ui, ur, uc, urs),
10654        down: (di, dr, dc, drs),
10655        xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
10656        xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
10657        down_col: dcf,
10658        w,
10659        q1: gq1,
10660        q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
10661        q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
10662        gu_q2: gq2,
10663        swiglu_limit,
10664    });
10665    Some(())
10666}
10667
10668/// Build one gate/up/down GPU job (see `moe_parts`).
10669fn moe_push_job<'a>(
10670    d: &'a DenseFfn,
10671    x: &[f32],
10672    w: f32,
10673    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
10674    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
10675) -> Option<()> {
10676    use crate::qtensor::prescale;
10677    if d.act != Act::Silu {
10678        return None; // GPU block hardcodes SiLU
10679    }
10680    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
10681    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
10682    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
10683    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
10684        return None; // mixed-dtype trio — honest CPU path
10685    }
10686    if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
10687        return None;
10688    }
10689    if !gq2 && dq2 {
10690        return None;
10691    }
10692    model_ref.get_or_insert_with(|| gm.clone());
10693    let gdt = if gcf.is_empty() {
10694        cortiq_core::TensorDtype::Q8Row
10695    } else {
10696        cortiq_core::TensorDtype::Q8_2f
10697    };
10698    let udt = if ucf.is_empty() {
10699        cortiq_core::TensorDtype::Q8Row
10700    } else {
10701        cortiq_core::TensorDtype::Q8_2f
10702    };
10703    jobs.push(crate::gpu::MoeJob {
10704        gate: (gi, gr, gc, grs),
10705        up: (ui, ur, uc, urs),
10706        down: (di, dr, dc, drs),
10707        xs_gate: prescale(x, gcf, gdt).into_owned(),
10708        xs_up: prescale(x, ucf, udt).into_owned(),
10709        down_col: dcf,
10710        w,
10711        q1: gq1,
10712        q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
10713        q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
10714        gu_q2: gq2,
10715        swiglu_limit: 0.0,
10716    });
10717    Some(())
10718}
10719
10720/// Sparse dense-FFN directly on QUANTIZED weights (mask × mmap): reads
10721/// ONLY the active neurons' gate/up rows and down columns from the mmap
10722/// — no full-matrix dequant, no f32 model copy. This is what lets a
10723/// masked big model run at quantized RSS (the historical mask path
10724/// forced the whole model to f32). Semantics identical to the f32
10725/// sparse path within quant tolerance.
10726fn sparse_ffn_quant(
10727    d: &DenseFfn,
10728    x: &[f32],
10729    active: &[u16],
10730    hidden: usize,
10731    pool: Option<&Pool>,
10732) -> Vec<f32> {
10733    let n = active.len();
10734    let inter = d.gate_proj.rows();
10735    let mut act = vec![0.0f32; n];
10736    // Scratch is needed if EITHER projection is group-packed (q4/vbit);
10737    // gate/up normally share a dtype but sizing on both is robust.
10738    let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
10739    let compute = |ai: usize| -> f32 {
10740        let idx = active[ai] as usize;
10741        if idx >= inter {
10742            return 0.0; // defensive parity with the f32 sparse path
10743        }
10744        let mut s = if need_scratch {
10745            vec![0.0f32; hidden]
10746        } else {
10747            Vec::new()
10748        };
10749        let gate = d.gate_proj.row_dot(idx, x, &mut s);
10750        let up = d.up_proj.row_dot(idx, x, &mut s);
10751        d.act.combine(gate, up)
10752    };
10753    match pool {
10754        Some(p) if n >= 256 => {
10755            let ptr = SendMut(act.as_mut_ptr());
10756            p.run(&|widx, nw| {
10757                let chunk = n.div_ceil(nw);
10758                let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
10759                for ai in s..e {
10760                    unsafe { *ptr.at(ai) = compute(ai) };
10761                }
10762            });
10763        }
10764        _ => {
10765            for (ai, a) in act.iter_mut().enumerate() {
10766                *a = compute(ai);
10767            }
10768        }
10769    }
10770    // Scatter through active down columns (reads only those columns).
10771    let mut out = vec![0.0f32; hidden];
10772    for (ai, &idx) in active.iter().enumerate() {
10773        let w = act[ai];
10774        if w.abs() >= 1e-12 && (idx as usize) < inter {
10775            d.down_proj.add_col_scaled(idx as usize, w, &mut out);
10776        }
10777    }
10778    out
10779}
10780
10781/// Test-only re-export of the private sparse-quant FFN (mask × mmap gate).
10782#[doc(hidden)]
10783pub fn sparse_ffn_quant_for_test(
10784    d: &DenseFfn,
10785    x: &[f32],
10786    active: &[u16],
10787    hidden: usize,
10788) -> Vec<f32> {
10789    sparse_ffn_quant(d, x, active, hidden, None)
10790}
10791
10792/// Dequantize a DenseFfn's three matrices to f32 (transient; only the
10793/// q4/vbit-masked fallback uses it — the memory-lean path is
10794/// sparse_ffn_quant). Reuses row_f32 row-by-row.
10795fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
10796    let deq = |t: &QTensor| -> Vec<f32> {
10797        let (rows, cols) = (t.rows(), t.cols());
10798        let mut out = vec![0.0f32; rows * cols];
10799        for r in 0..rows {
10800            t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
10801        }
10802        out
10803    };
10804    (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
10805}
10806
10807/// Pointer wrapper for the worker-pool scatter (same pattern as qtensor).
10808struct SendMut(*mut f32);
10809unsafe impl Send for SendMut {}
10810unsafe impl Sync for SendMut {}
10811impl SendMut {
10812    #[inline]
10813    // Deliberate unsynchronized scatter: pool workers write disjoint indices
10814    // in parallel, so returning `&mut` from `&self` is intentional here.
10815    #[allow(clippy::mut_from_ref)]
10816    unsafe fn at(&self, i: usize) -> &mut f32 {
10817        unsafe { &mut *self.0.add(i) }
10818    }
10819}
10820
10821/// Router → (selected experts in torch.topk order, per-expert score
10822/// vector, normalizer). The final weight of expert `e` is `p[e] / wsum`.
10823///
10824/// Two regimes share this. Qwen: softmax over ALL experts, top-k of the
10825/// probabilities, optional renorm — `router_sigmoid=false`, no bias,
10826/// scale 1 → bit-identical to the historical path. LFM2-MoE /
10827/// DeepSeek-V3 `noaux_tc`: per-expert sigmoid scores, an optional
10828/// selection bias (top-k CHOICE only; weights stay unbiased), a 1e-6 renorm
10829/// floor and a routed scale.
10830fn moe_route(logits: &[f32], m: &MoeFfn, allowed: Option<&[bool]>) -> (Vec<usize>, Vec<f32>, f32) {
10831    let ne = logits.len();
10832    let p: Vec<f32> = if m.router_sigmoid {
10833        logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
10834    } else {
10835        let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10836        let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
10837        let s: f32 = e.iter().sum();
10838        for v in &mut e {
10839            *v /= s;
10840        }
10841        e
10842    };
10843    // Expert restriction: the static env mask (CMF_MOE_MASK) AND the
10844    // active task mask's expert fields (spec §5) both narrow the
10845    // candidate set; selection happens over the admitted experts only.
10846    // With norm_topk the kept weights renormalize below; without it
10847    // the excluded mass is honestly dropped.
10848    let admit = |e: usize| {
10849        m.mask.as_ref().is_none_or(|mk| mk[e])
10850            && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
10851    };
10852    let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
10853    // Descending by selection score, lower index wins ties (torch.topk).
10854    match &m.expert_bias {
10855        Some(b) => idx.sort_unstable_by(|&x, &y| {
10856            (p[y] + b[y])
10857                .partial_cmp(&(p[x] + b[x]))
10858                .unwrap()
10859                .then(x.cmp(&y))
10860        }),
10861        None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
10862    }
10863    idx.truncate(m.top_k);
10864    // Adaptive τ-routing: trim the tail experts once the kept mass is
10865    // enough. wsum below renormalizes over the KEPT set, so the output
10866    // stays a proper weighted average.
10867    if let Some(tau) = m.route_tau {
10868        let total: f32 = idx.iter().map(|&e| p[e]).sum();
10869        if total > 0.0 {
10870            let mut acc = 0.0f32;
10871            let mut keep = idx.len();
10872            for (i, &e) in idx.iter().enumerate() {
10873                acc += p[e];
10874                if acc >= tau * total {
10875                    keep = i + 1;
10876                    break;
10877                }
10878            }
10879            idx.truncate(keep);
10880        }
10881    }
10882    let wsum: f32 = if m.norm_topk_prob {
10883        let s: f32 = idx.iter().map(|&e| p[e]).sum();
10884        // LFM2 floors the denom (matches HF `+ 1e-6`); the softmax path's
10885        // probs already sum near 1, so it stays exactly as before.
10886        (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
10887    } else {
10888        1.0 / m.routed_scaling
10889    };
10890    (idx, p, wsum)
10891}
10892
10893/// See the call site: one `layer:e1,e2,…` line per routed token.
10894fn moe_trace(idx: &[usize]) {
10895    moe_trace_at(crate::gpu::cur_layer() as i32, idx)
10896}
10897
10898/// The same, for callers that know their layer (DSV4 owns its layers and
10899/// never sets the pipeline's current-layer marker).
10900pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
10901    use std::io::Write;
10902    static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
10903        std::sync::OnceLock::new();
10904    let Some(f) = F.get_or_init(|| {
10905        let p = std::env::var("CMF_MOE_TRACE").ok()?;
10906        Some(std::sync::Mutex::new(
10907            std::fs::OpenOptions::new().create(true).append(true).open(p).ok()?,
10908        ))
10909    }) else {
10910        return;
10911    };
10912    let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
10913    let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
10914}
10915
10916/// MoE FFN: router → top-k experts (see `moe_route`). Only selected
10917/// experts' pages are touched in mmap.
10918fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
10919    accumulate_act(m, x, 1);
10920    let ne = m.experts.len();
10921    let mut logits = vec![0.0f32; ne];
10922    match &m.resonance {
10923        Some(r) => r.scores(x, &mut logits),
10924        None => m.router.matvec(x, &mut logits, pool),
10925    }
10926    let (idx, p, wsum) = moe_route(&logits, m, allowed);
10927    {
10928        let mut st = m.stats.borrow_mut();
10929        if st.len() < ne {
10930            st.resize(ne, 0);
10931        }
10932        for &e in &idx {
10933            st[e] += 1;
10934        }
10935    }
10936    // `CMF_MOE_TRACE=<file>`: append one line per (layer, token) with the
10937    // selected expert ids. The cumulative `stats` above answer "which
10938    // experts are popular"; a residency design needs the question they
10939    // cannot answer — whether CONSECUTIVE tokens reuse experts (the
10940    // temporal locality an LRU cache lives on, FreeToken §4).
10941    moe_trace(&idx);
10942    // D5: the whole layer MoE block in one GPU command buffer (experts — the
10943    // same mmap via a no-copy buffer; intermediate activations on the GPU).
10944    // Same Ffn probe class as the dense chain: one submit per layer
10945    // either wins on this driver stack or it doesn't.
10946    if crate::gpu::enabled_here() {
10947        match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
10948            crate::gpu::ProbeArm::Gpu => {
10949                let t0 = std::time::Instant::now();
10950                if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
10951                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
10952                    return out;
10953                }
10954            }
10955            crate::gpu::ProbeArm::CpuTimed => {
10956                let t0 = std::time::Instant::now();
10957                let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
10958                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
10959                return out;
10960            }
10961            crate::gpu::ProbeArm::Cpu => {
10962                return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
10963            }
10964        }
10965    }
10966    moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
10967}
10968
10969/// One-shot report of whether the whole-token wgpu graph actually formed.
10970/// A refusal silently reverts to the per-op path, which is how a model can
10971/// look "GPU-accelerated" while every layer walks the host.
10972fn graph_note(built: bool) {
10973    use std::sync::atomic::{AtomicBool, Ordering};
10974    if built {
10975        GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
10976    } else {
10977        GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
10978    }
10979    static SAID: AtomicBool = AtomicBool::new(false);
10980    if !SAID.swap(true, Ordering::Relaxed) {
10981        if built {
10982            tracing::info!("wgpu whole-token graph: ACTIVE");
10983        } else {
10984            tracing::warn!("wgpu whole-token graph refused — per-op path");
10985        }
10986    }
10987}
10988
10989/// Whole-token graph outcomes, process-wide: a benchmark that claims a
10990/// GPU number while MISS climbs is measuring the CPU — the honest-bench
10991/// contract makes that an error, not a footnote.
10992pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
10993pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
10994
10995/// `CMF_MOE_BATCH=0` restores the per-expert serial loop — the A/B lever
10996/// for the batched kernel, and how its bit-identity is checked.
10997fn moe_batch_enabled() -> bool {
10998    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10999    *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
11000}
11001
11002/// Two-dispatch CPU MoE: every routed expert (and the shared one) fused
11003/// into one gate/up/SiLU dispatch and one down dispatch, instead of two
11004/// pool barriers per expert. Bit-identical to the serial loop below —
11005/// see `moe_gate_up_many` / `moe_down_many`. `None` = the batched kernel
11006/// does not cover this layer, walk the serial path.
11007fn moe_ffn_cpu_batched(
11008    m: &MoeFfn,
11009    x: &[f32],
11010    idx: &[usize],
11011    p: &[f32],
11012    wsum: f32,
11013    pool: Option<&Pool>,
11014) -> Option<Vec<f32>> {
11015    if idx.is_empty() || !moe_batch_enabled() {
11016        return None;
11017    }
11018    // The bake probe reads per-neuron activation mass out of the
11019    // single-expert path; batching would skip it. Rare and offline —
11020    // hand those runs to the serial loop.
11021    if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
11022        return None;
11023    }
11024    let n = idx.len() + usize::from(m.shared.is_some());
11025    let mut pairs = Vec::with_capacity(n);
11026    let mut downs = Vec::with_capacity(n);
11027    let mut ws = Vec::with_capacity(n);
11028    for &e in idx {
11029        let d = &m.experts[e];
11030        if d.act != Act::Silu {
11031            return None;
11032        }
11033        pairs.push((&d.gate_proj, &d.up_proj));
11034        downs.push(&d.down_proj);
11035        ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
11036    }
11037    // The shared expert goes last, matching the serial loop's order —
11038    // the f32 accumulation order is part of the bit-identity claim.
11039    if let Some((se, gate)) = &m.shared {
11040        if se.act != Act::Silu {
11041            return None;
11042        }
11043        let g = gate.as_ref().map_or(1.0, |gate| {
11044            let mut gl = [0.0f32; 1];
11045            gate.matvec(x, &mut gl, pool);
11046            1.0 / (1.0 + (-gl[0]).exp())
11047        });
11048        pairs.push((&se.gate_proj, &se.up_proj));
11049        downs.push(&se.down_proj);
11050        ws.push(g);
11051    }
11052    let inter = pairs[0].0.rows();
11053    let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
11054    if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
11055        return None;
11056    }
11057    let mut out = attention::take_buf(x.len());
11058    if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
11059        attention::recycle_buf(&mut out);
11060        return None;
11061    }
11062    Some(out)
11063}
11064
11065/// The pure-CPU MoE expert loop (also the fallback of every GPU refusal).
11066fn moe_ffn_cpu(
11067    m: &MoeFfn,
11068    x: &[f32],
11069    idx: &[usize],
11070    p: &[f32],
11071    wsum: f32,
11072    pool: Option<&Pool>,
11073) -> Vec<f32> {
11074    if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
11075        return out;
11076    }
11077    let mut out = attention::take_buf(x.len());
11078    for &e in idx {
11079        let mut eo = dense_ffn(&m.experts[e], x, pool);
11080        let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
11081        for i in 0..out.len() {
11082            out[i] += w * eo[i];
11083        }
11084        attention::recycle_buf(&mut eo);
11085    }
11086    if let Some((se, gate)) = &m.shared {
11087        let mut so = dense_ffn(se, x, pool);
11088        let g = gate.as_ref().map_or(1.0, |gate| {
11089            let mut gl = [0.0f32; 1];
11090            gate.matvec(x, &mut gl, pool);
11091            1.0 / (1.0 + (-gl[0]).exp())
11092        });
11093        for i in 0..out.len() {
11094            out[i] += g * so[i];
11095        }
11096        attention::recycle_buf(&mut so);
11097    }
11098    out
11099}
11100
11101/// DeepSeek-V2 MLA forward, expand-to-MHA form (see `AttnKind::Mla`):
11102/// per token the latent expands to every head's K/V and the ordinary
11103/// cache + grouped attend do the rest. K head layout is [rope | nope]
11104/// (rotary_dim = qk_rope rotates the shared rope key and each q head's
11105/// prefix); V rows are zero-padded to the K head_dim inside the cache
11106/// and the pad is sliced off before O. Born importance is not
11107/// accumulated for MLA yet (no eviction interplay).
11108#[allow(clippy::too_many_arguments)]
11109fn mla_attention(
11110    w: &MlaWeights,
11111    normed: &[f32],
11112    cache: &mut crate::kv_cache::LayerKvCache,
11113    position: usize,
11114    inv_freq: &[f32],
11115    rope_scale: f32,
11116    eps: f64,
11117    pool: Option<&Pool>,
11118) -> Vec<f32> {
11119    let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
11120    let hd = dr + dn;
11121    let mut q = vec![0.0f32; nh * hd];
11122    match (&w.q_a, &w.q_a_norm) {
11123        (Some(qa), Some(qn)) => {
11124            let mut t = vec![0.0f32; qa.rows()];
11125            qa.matvec(normed, &mut t, pool);
11126            let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
11127            w.q_proj.matvec(&tn, &mut q, pool);
11128        }
11129        _ => w.q_proj.matvec(normed, &mut q, pool),
11130    }
11131    let mut ca = vec![0.0f32; lora + dr];
11132    w.kv_a.matvec(normed, &mut ca, pool);
11133    let (c_lat, k_rope) = ca.split_at_mut(lora);
11134    let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
11135    let mut kvb = vec![0.0f32; nh * (dn + dv)];
11136    w.kv_b.matvec(&latn, &mut kvb, pool);
11137    if !w.nope {
11138        attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
11139    }
11140    for h in 0..nh {
11141        if !w.nope {
11142            attention::rope_rotate_scaled(
11143                &mut q[h * hd..h * hd + dr],
11144                position,
11145                inv_freq,
11146                rope_scale,
11147            );
11148        }
11149    }
11150    let mut k = vec![0.0f32; nh * hd];
11151    let mut v = vec![0.0f32; nh * hd];
11152    for h in 0..nh {
11153        k[h * hd..h * hd + dr].copy_from_slice(k_rope);
11154        k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
11155        v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
11156    }
11157    cache.append(&k, &v, &vec![true; nh]);
11158    let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
11159    attention::recycle_buf(&mut imp);
11160    let mut ov = vec![0.0f32; nh * dv];
11161    for h in 0..nh {
11162        ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
11163    }
11164    let mut out = vec![0.0f32; w.o_proj.rows()];
11165    w.o_proj.matvec(&ov, &mut out, pool);
11166    out
11167}
11168
11169/// Gemma-4 dual-branch FFN (spec: see `FfnKind::DenseMoe`). The dense
11170/// branch reads the pre-FFN-normed activation; the router and the
11171/// expert branch read the RAW residual — the router through a
11172/// scale-less rms norm (its constant gain is folded into the weights),
11173/// the experts through `pre_norm_2`. CPU path; GPU graphs refuse the
11174/// layer kind honestly.
11175fn dense_moe_ffn(
11176    dm: &DenseMoeFfn,
11177    x_normed: &[f32],
11178    h_raw: &[f32],
11179    eps: f64,
11180    norm_style: NormStyle,
11181    pool: Option<&Pool>,
11182) -> Vec<f32> {
11183    let mut d = dense_ffn(&dm.dense, x_normed, pool);
11184    d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
11185    let m = &dm.moe;
11186    let ne = m.experts.len();
11187    let mut logits = vec![0.0f32; ne];
11188    if m.router_input_norm {
11189        let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
11190        let inv = 1.0 / (ss + eps as f32).sqrt();
11191        let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
11192        m.router.matvec(&xr, &mut logits, pool);
11193    } else {
11194        m.router.matvec(h_raw, &mut logits, pool);
11195    }
11196    let (idx, p, wsum) = moe_route(&logits, m, None);
11197    {
11198        let mut st = m.stats.borrow_mut();
11199        if st.len() < ne {
11200            st.resize(ne, 0);
11201        }
11202        for &e in &idx {
11203            st[e] += 1;
11204        }
11205    }
11206    let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
11207    let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
11208    let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
11209    for (di, mi) in d.iter_mut().zip(&mo) {
11210        *di += mi;
11211    }
11212    d
11213}
11214
11215/// Building the MoE-layer GPU jobs: all selected experts (+shared) must
11216/// be q8_2f-Mapped from the primary mapping; otherwise None → CPU path.
11217/// One-shot report of why the MoE GPU block refused. A silent `?` here
11218/// sends every expert to the CPU with nothing in the logs to say so —
11219/// which is exactly how a q4tp MoE model looked "GPU-accelerated" while
11220/// running entirely on the host.
11221fn moe_gpu_refused(why: &'static str) {
11222    use std::sync::atomic::{AtomicBool, Ordering};
11223    static SAID: AtomicBool = AtomicBool::new(false);
11224    if !SAID.swap(true, Ordering::Relaxed) {
11225        tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
11226    }
11227}
11228
11229fn moe_ffn_gpu(
11230    m: &MoeFfn,
11231    x: &[f32],
11232    idx: &[usize],
11233    p: &[f32],
11234    wsum: f32,
11235    pool: Option<&Pool>,
11236) -> Option<Vec<f32>> {
11237    use crate::gpu::MoeJob;
11238
11239    let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
11240    let mut model_ref = None;
11241    for &e in idx {
11242        if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
11243            moe_gpu_refused("push_job(expert)");
11244            return None;
11245        }
11246    }
11247    if let Some((se, gate)) = &m.shared {
11248        let g = gate.as_ref().map_or(1.0, |gate| {
11249            let mut gl = [0.0f32; 1];
11250            gate.matvec(x, &mut gl, pool);
11251            1.0 / (1.0 + (-gl[0]).exp())
11252        });
11253        if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
11254            moe_gpu_refused("push_job(shared)");
11255            return None;
11256        }
11257    }
11258    let Some(model) = model_ref else {
11259        moe_gpu_refused("no model_ref");
11260        return None;
11261    };
11262    let hidden = jobs[0].down.1;
11263    let mut out = vec![0.0f32; hidden];
11264    if crate::gpu::moe_block(&model, &jobs, &mut out) {
11265        Some(out)
11266    } else {
11267        moe_gpu_refused("gpu::moe_block");
11268        None
11269    }
11270}
11271
11272/// Single-position FFN dispatch.
11273fn ffn_forward(
11274    ffn: &FfnKind,
11275    x: &[f32],
11276    pool: Option<&Pool>,
11277    experts_allowed: Option<&[bool]>,
11278) -> Vec<f32> {
11279    match ffn {
11280        FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
11281        FfnKind::Dense(d) => dense_ffn(d, x, pool),
11282        FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
11283        // Dual-branch layers need the raw residual — their callers
11284        // dispatch dense_moe_ffn directly; the auxiliary paths that land
11285        // here (MTP draft, o1 replay) do not co-occur with gemma-4 MoE.
11286        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
11287    }
11288}
11289
11290/// Fused two-position FFN: gate/up/down streamed once (dense). MoE
11291/// falls back to two singles — expert sets differ per position, there
11292/// is nothing to fuse.
11293fn ffn_forward_pair(
11294    ffn: &FfnKind,
11295    x1: &[f32],
11296    x2: &[f32],
11297    pool: Option<&Pool>,
11298    experts_allowed: Option<&[bool]>,
11299) -> (Vec<f32>, Vec<f32>) {
11300    let d = match ffn {
11301        // A tube layer has nothing to fuse across the pair — the tubes
11302        // are separate matrices; two singles are the honest path.
11303        FfnKind::Dense(d) if !d.segs.is_empty() => {
11304            return (
11305                tube_ffn(d, x1, 1, pool, None),
11306                tube_ffn(d, x2, 1, pool, None),
11307            );
11308        }
11309        FfnKind::Dense(d) => d,
11310        FfnKind::Moe(m) => {
11311            return (
11312                moe_ffn(m, x1, pool, experts_allowed),
11313                moe_ffn(m, x2, pool, experts_allowed),
11314            );
11315        }
11316        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
11317    };
11318    let inter = d.gate_proj.rows();
11319    FFN_SCRATCH.with(|s| {
11320        let mut s = s.borrow_mut();
11321        let [g1, g2, u1, u2] = &mut *s;
11322        g1.resize(inter, 0.0);
11323        g2.resize(inter, 0.0);
11324        u1.resize(inter, 0.0);
11325        u2.resize(inter, 0.0);
11326        // Multi-matrix pair job: gate+up under one pool dispatch
11327        // (o1s = lane-1 outputs across tensors, o2s = lane-2).
11328        QTensor::matvec2_many(
11329            [&d.gate_proj, &d.up_proj],
11330            x1,
11331            x2,
11332            [g1.as_mut_slice(), u1.as_mut_slice()],
11333            [g2.as_mut_slice(), u2.as_mut_slice()],
11334            pool,
11335        );
11336        for i in 0..inter {
11337            g1[i] = d.act.combine(g1[i], u1[i]);
11338            g2[i] = d.act.combine(g2[i], u2[i]);
11339        }
11340        let mut o1 = attention::take_buf(d.down_proj.rows());
11341        let mut o2 = attention::take_buf(d.down_proj.rows());
11342        d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
11343        (o1, o2)
11344    })
11345}
11346
11347#[cfg(test)]
11348mod tests {
11349
11350    #[test]
11351    fn cancel_flag_stops_generation() {
11352        let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
11353        // Set before the call: the prefill loops honour it, the run
11354        // returns immediately with the cancelled reason and no tokens.
11355        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
11356        let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
11357        assert_eq!(r.finish_reason, "cancelled");
11358        assert!(
11359            r.token_ids.is_empty(),
11360            "no tokens after cancel: {:?}",
11361            r.token_ids
11362        );
11363        // Flag auto-cleared: the next call generates normally.
11364        let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
11365        assert_ne!(r2.finish_reason, "cancelled");
11366    }
11367    use super::*;
11368
11369    /// sparse_ffn_quant must equal a dense FFN where inactive neurons are
11370    /// zeroed (mask × mmap correctness). On F32 tensors this is EXACT —
11371    /// it validates the row_dot / add_col_scaled / scatter indexing, the
11372    /// bug-prone part. The q8 branches reuse the golden-tested linear
11373    /// The per-token sparse path reads a transposed `down`; it must
11374    /// agree with the arm that computes everything and zeroes the
11375    /// losers, or the speed measurement is measuring a different model.
11376    #[test]
11377    fn dynamic_ffn_equals_the_zeroing_arm() {
11378        let (hidden, inter) = (8usize, 32usize);
11379        let synth = |n: usize, salt: usize| -> Vec<f32> {
11380            (0..n)
11381                .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
11382                .collect()
11383        };
11384        let down = synth(hidden * inter, 3);
11385        let mut down_t = vec![0.0f32; inter * hidden];
11386        for r in 0..hidden {
11387            for c in 0..inter {
11388                down_t[c * hidden + r] = down[r * inter + c];
11389            }
11390        }
11391        let d = DenseFfn {
11392            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
11393            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
11394            down_proj: QTensor::from_f32(down.clone(), hidden, inter),
11395            act: Act::Silu,
11396            down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
11397            segs: Vec::new(),
11398        };
11399        let x = synth(hidden, 11);
11400        let k = 12usize;
11401        let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
11402        // Reference: full compute, keep the k loudest |silu(gate)|.
11403        let mut g = vec![0.0f32; inter];
11404        d.gate_proj.matvec(&x, &mut g, None);
11405        let mut u = vec![0.0f32; inter];
11406        d.up_proj.matvec(&x, &mut u, None);
11407        for v in g.iter_mut() {
11408            *v = inference::silu(*v);
11409        }
11410        keep_top_k(&mut g, k);
11411        for i in 0..inter {
11412            g[i] *= u[i];
11413        }
11414        let mut want = vec![0.0f32; hidden];
11415        d.down_proj.matvec(&g, &mut want, None);
11416        for (a, b) in want.iter().zip(&got) {
11417            assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
11418        }
11419    }
11420
11421    /// A tube layer is the same layer, re-cut. With every tube open the
11422    /// answer must equal the dense FFN over the concatenated neurons
11423    /// (the permutation is an identity on the layer's function); with a
11424    /// tube closed it must equal the dense FFN with those neurons
11425    /// zeroed — the mask semantics, now paid for in bytes not read.
11426    #[test]
11427    fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
11428        let (hidden, core, tube) = (8usize, 12usize, 8usize);
11429        let inter = core + tube;
11430        let synth = |n: usize, salt: usize| -> Vec<f32> {
11431            (0..n)
11432                .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
11433                .collect()
11434        };
11435        let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
11436        let d_all = synth(hidden * inter, 3);
11437        // The dense layer, and the same weights cut into core + tube.
11438        let dense = DenseFfn {
11439            gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
11440            up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
11441            down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
11442            act: Act::Silu,
11443            down_t: None,
11444            segs: Vec::new(),
11445        };
11446        let rows = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
11447            v[a * hidden..b * hidden].to_vec()
11448        };
11449        let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
11450            let mut o = Vec::with_capacity(hidden * (b - a));
11451            for r in 0..hidden {
11452                o.extend_from_slice(&v[r * inter + a..r * inter + b]);
11453            }
11454            o
11455        };
11456        let tubed = DenseFfn {
11457            down_t: None,
11458            gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
11459            up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
11460            down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
11461            act: Act::Silu,
11462            segs: vec![FfnSeg {
11463                gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
11464                up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
11465                down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
11466                start: core,
11467                width: tube,
11468            }],
11469        };
11470        let x = synth(hidden, 7);
11471        let want = dense_ffn(&dense, &x, None);
11472        let got = tube_ffn(&tubed, &x, 1, None, None);
11473        for (a, b) in want.iter().zip(&got) {
11474            assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
11475        }
11476        // Closed tube: bits on for the core, off for the tube.
11477        let mut bits = vec![0u8; inter.div_ceil(8)];
11478        for n in 0..core {
11479            bits[n / 8] |= 1 << (n % 8);
11480        }
11481        let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
11482        let masked = dense_ffn_masked(&dense, &x, None, &bits);
11483        for (a, b) in masked.iter().zip(&closed) {
11484            assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
11485        }
11486        // The batched arm must agree with the single-position one.
11487        let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
11488        for (a, b) in closed.iter().zip(&batch) {
11489            assert_eq!(a, b, "batch arm disagrees with decode arm");
11490        }
11491    }
11492
11493    /// scale, structurally identical to the matvec kernels.
11494    #[test]
11495    fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
11496        let (hidden, inter) = (16usize, 40usize);
11497        let synth = |n: usize, salt: usize| -> Vec<f32> {
11498            (0..n)
11499                .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
11500                .collect()
11501        };
11502        let d = DenseFfn {
11503            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
11504            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
11505            down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
11506            act: Act::Silu,
11507            down_t: None,
11508            segs: Vec::new(),
11509        };
11510        let x = synth(hidden, 9);
11511        // Active = every 3rd neuron.
11512        let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
11513
11514        let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
11515
11516        // Reference: full dense FFN but g[i]=0 for inactive neurons.
11517        let mut g = vec![0.0f32; inter];
11518        d.gate_proj.matvec(&x, &mut g, None);
11519        let mut u = vec![0.0f32; inter];
11520        d.up_proj.matvec(&x, &mut u, None);
11521        let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
11522        for i in 0..inter {
11523            g[i] = if act_set.contains(&(i as u16)) {
11524                inference::silu(g[i]) * u[i]
11525            } else {
11526                0.0
11527            };
11528        }
11529        let mut reference = vec![0.0f32; hidden];
11530        d.down_proj.matvec(&g, &mut reference, None);
11531
11532        let max_d = sparse
11533            .iter()
11534            .zip(&reference)
11535            .map(|(a, b)| (a - b).abs())
11536            .fold(0.0f32, f32::max);
11537        assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
11538    }
11539
11540    /// Attach a synthetic MTP head (same structure as a main layer).
11541    fn attach_test_mtp(p: &mut Pipeline) {
11542        let (h, inter, heads, kv, hd) = (
11543            p.hidden_size,
11544            p.intermediate_size,
11545            p.num_heads,
11546            p.num_kv_heads,
11547            p.head_dim,
11548        );
11549        let synth = |n: usize, salt: usize| -> Vec<f32> {
11550            (0..n)
11551                .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
11552                .collect()
11553        };
11554        let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
11555            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
11556        };
11557        p.mtp = Some(MtpModule {
11558            enorm: vec![1.0; h],
11559            hnorm: vec![1.0; h],
11560            eh_proj: qt(h, 2 * h, 301),
11561            layer: LayerWeights {
11562                input_norm: vec![1.0; h],
11563                post_norm: vec![1.0; h],
11564                attn_out_norm: None,
11565                ffn_out_norm: None,
11566                layer_scale: None,
11567                ffn: FfnKind::Dense(DenseFfn {
11568                    gate_proj: qt(inter, h, 315),
11569                    up_proj: qt(inter, h, 316),
11570                    down_proj: qt(h, inter, 317),
11571                    act: Act::Silu,
11572            down_t: None,
11573            segs: Vec::new(),
11574        }),
11575                attn: AttnKind::Full {
11576                    bias: None,
11577                    wq: qt(heads * hd, h, 311),
11578                    wk: qt(kv * hd, h, 312),
11579                    wv: qt(kv * hd, h, 313),
11580                    wo: qt(h, heads * hd, 314),
11581                    q_norm: None,
11582                    k_norm: None,
11583                    output_gate: false,
11584                    softplus_gate: None,
11585                },
11586            },
11587            final_norm: vec![1.0; h],
11588            kv: crate::kv_cache::LayerKvCache::new(kv, hd),
11589        });
11590    }
11591
11592    #[test]
11593    fn speculative_equals_vanilla_greedy() {
11594        // Speculative decode and the wgpu token graph are mutually
11595        // exclusive; a leaked CMF_GPU=wgpu from a parallel gpu test
11596        // would silently disable drafting. Pin the graph off.
11597        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
11598        let run = |spec: bool| {
11599            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
11600            p.sampler_config.temperature = 0.0;
11601            attach_test_mtp(&mut p);
11602            p.speculative = spec;
11603            let r = p.generate("abcdef", 12, None, None).unwrap();
11604            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
11605        };
11606        let (vanilla, d0, _) = run(false);
11607        let (spec, d1, a1) = run(true);
11608        assert_eq!(d0, 0, "vanilla path must not draft");
11609        assert!(d1 > 0, "speculative path must draft");
11610        assert_eq!(
11611            vanilla, spec,
11612            "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
11613        );
11614    }
11615
11616    #[test]
11617    fn speculative_accepts_constant_oracle() {
11618        // See speculative_equals_vanilla_greedy: pin the wgpu graph off.
11619        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
11620        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
11621        p.sampler_config.temperature = 0.0;
11622        p.sampler_config.repetition_penalty = 1.0;
11623        // Constant lm_head → every logit equal → both the main model and
11624        // the draft head argmax to token 0: acceptance must be 100%.
11625        p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
11626        attach_test_mtp(&mut p);
11627        p.speculative = true;
11628        let r = p.generate("abcd", 10, None, None).unwrap();
11629        assert!(r.mtp_drafted > 0);
11630        assert_eq!(
11631            r.mtp_accepted, r.mtp_drafted,
11632            "constant logits → every draft accepted"
11633        );
11634        // Ties resolve to the same token in both the main and draft
11635        // heads — the sequence is one repeated token.
11636        assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
11637    }
11638
11639    #[test]
11640    fn empty_prompt_is_an_error_not_a_panic() {
11641        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
11642        let r = p.generate("", 4, None, None);
11643        assert!(r.is_err(), "empty prompt must be a clean error");
11644    }
11645
11646    #[test]
11647    fn every_token_enters_kv_exactly_once() {
11648        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
11649        // Greedy so no RNG variance; byte tokenizer → 3 prompt tokens.
11650        p.sampler_config.temperature = 0.0;
11651        let r = p.generate("abc", 2, None, None).unwrap();
11652        assert_eq!(r.prompt_tokens, 3);
11653        // prompt(3) + first sampled token forwarded before second logits:
11654        // step0 samples from prefill hidden (no extra forward), then
11655        // forwards t1 → cache 4; step1 samples, loop ends (max_tokens).
11656        assert_eq!(
11657            p.kv_cache.seq_len(),
11658            3 + r.tokens_generated - 1,
11659            "each token must be cached exactly once (v1 cached the last prompt token twice)"
11660        );
11661    }
11662
11663    #[test]
11664    fn generation_is_reproducible_with_seed() {
11665        let run = || {
11666            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
11667            p.generate("hello", 8, None, None).unwrap().token_ids
11668        };
11669        assert_eq!(run(), run());
11670    }
11671
11672    #[test]
11673    fn resetting_sampler_restarts_the_seeded_stream() {
11674        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
11675        let config = SamplerConfig {
11676            seed: Some(1234),
11677            ..SamplerConfig::default()
11678        };
11679        p.set_sampler_config(config.clone());
11680        let first = p.generate("hello", 8, None, None).unwrap().token_ids;
11681        p.set_sampler_config(config);
11682        let second = p.generate("hello", 8, None, None).unwrap().token_ids;
11683        assert_eq!(first, second);
11684    }
11685
11686    #[test]
11687    fn eviction_bounds_the_cache() {
11688        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
11689        p.kv_cache.max_seq_len = 6;
11690        p.sampler_config.temperature = 0.0;
11691        let _ = p.generate("abcd", 12, None, None).unwrap();
11692        assert!(
11693            p.kv_cache.seq_len() <= 6 + 1,
11694            "cache must stay bounded by max_seq_len (got {})",
11695            p.kv_cache.seq_len()
11696        );
11697    }
11698
11699    #[test]
11700    fn confidence_matches_tokens_and_is_a_probability() {
11701        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
11702        p.sampler_config.temperature = 0.0;
11703        p.sampler_config.repetition_penalty = 1.0;
11704        let r = p.generate("abcd", 10, None, None).unwrap();
11705        assert_eq!(
11706            r.token_confidence.len(),
11707            r.token_ids.len(),
11708            "one confidence per emitted token"
11709        );
11710        for &c in &r.token_confidence {
11711            assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
11712        }
11713        // top1_prob is a valid softmax probability.
11714        let logits = [1.0f32, 3.0, 0.5, 3.0];
11715        let p0 = top1_prob_t(&logits, 1, 1.0);
11716        let p1 = top1_prob_t(&logits, 3, 1.0);
11717        assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
11718        assert!(p0 > 0.0 && p0 < 1.0);
11719        // Calibration temperature > 1 softens an over-confident peak.
11720        let sharp = top1_prob_t(&logits, 1, 1.0);
11721        let soft = top1_prob_t(&logits, 1, 2.0);
11722        assert!(soft < sharp, "higher temperature lowers peak confidence");
11723    }
11724
11725    #[test]
11726    fn trace_is_opt_in_and_parallels_the_output() {
11727        // Off by default: the runtime is silent unless observation asked.
11728        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
11729        p.sampler_config.temperature = 0.0;
11730        p.sampler_config.repetition_penalty = 1.0;
11731        let r = p.generate("abcd", 10, None, None).unwrap();
11732        assert!(r.traces.is_empty(), "trace must be empty unless enabled");
11733
11734        // On: exactly one row per emitted token, aligned with the output.
11735        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
11736        p.sampler_config.temperature = 0.0;
11737        p.sampler_config.repetition_penalty = 1.0;
11738        p.set_trace(true);
11739        let r = p.generate("abcd", 10, None, None).unwrap();
11740        assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
11741        for (i, tr) in r.traces.iter().enumerate() {
11742            assert_eq!(tr.t, i, "trace index is sequential");
11743            assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
11744            assert_eq!(
11745                tr.confidence, r.token_confidence[i],
11746                "trace confidence matches the confidence channel"
11747            );
11748            // No dynamic router in this pipeline → no skill, no coherence.
11749            assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
11750        }
11751    }
11752
11753    #[test]
11754    fn explain_prefill_logits_match_greedy_first_token() {
11755        // `cortiq explain` shows the next-token distribution from
11756        // prefill_next_logits; its argmax must equal what greedy generate
11757        // actually emits first — otherwise explain would lie.
11758        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
11759        p.sampler_config.temperature = 0.0;
11760        p.sampler_config.repetition_penalty = 1.0;
11761        let ids = p.tokenizer.encode("abcd");
11762        let logits = p.prefill_next_logits(&ids, None);
11763        let argmax = logits
11764            .iter()
11765            .enumerate()
11766            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11767            .unwrap()
11768            .0 as u32;
11769        let r = p.generate("abcd", 1, None, None).unwrap();
11770        assert_eq!(
11771            argmax, r.token_ids[0],
11772            "explain preview must match greedy emit"
11773        );
11774    }
11775
11776    #[test]
11777    fn laguna_shared_expert_is_unconditionally_added() {
11778        let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
11779        let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
11780        let zero_dense = || DenseFfn {
11781            gate_proj: matrix(vec![0.0; 4]),
11782            up_proj: matrix(vec![0.0; 4]),
11783            down_proj: matrix(vec![0.0; 4]),
11784            act: Act::Silu,
11785            down_t: None,
11786            segs: Vec::new(),
11787        };
11788        let shared = DenseFfn {
11789            gate_proj: identity(),
11790            up_proj: identity(),
11791            down_proj: identity(),
11792            act: Act::Silu,
11793            down_t: None,
11794            segs: Vec::new(),
11795        };
11796        let x = [1.0, 2.0];
11797        let expected = dense_ffn(&shared, &x, None);
11798        let moe = MoeFfn {
11799            router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
11800            experts: vec![zero_dense()],
11801            top_k: 1,
11802            norm_topk_prob: true,
11803            router_sigmoid: true,
11804            expert_bias: None,
11805            routed_scaling: 1.0,
11806            route_tau: None,
11807            shared: Some((shared, None)),
11808            stats: std::cell::RefCell::new(Vec::new()),
11809            act_sq: std::cell::RefCell::new(Vec::new()),
11810            act_rows: std::cell::RefCell::new(Vec::new()),
11811            mask: None,
11812            per_expert_scale: None,
11813            router_input_norm: false,
11814            resonance: None,
11815        };
11816        let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
11817        for (actual, expected) in actual.iter().zip(expected) {
11818            assert!((actual - expected).abs() < 1e-6);
11819        }
11820    }
11821}