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    /// A GPU graph failure is distinct from a user/request cancellation.
92    /// Graph code sets this before raising the cooperative cancel flag so the
93    /// generation API can return an error instead of reporting a successful
94    /// `finish_reason: cancelled` result.
95    graph_failed: std::sync::atomic::AtomicBool,
96    /// Token ids currently materialized in the KV cache (the forwarded
97    /// prompt + all generated tokens except the last, which is sampled
98    /// but not yet forwarded). Lets the next generate call prefill only
99    /// the suffix when a chat app resends the whole history.
100    pub kv_history: Vec<u32>,
101    /// KDA geometry (Kimi Linear / Kimi-K3) — shared by every Kda layer.
102    pub kda_cfg: Option<crate::linear_core::KdaCfg>,
103    /// Gemma-3n stack (AltUp/LAuReL/PLE/KV-sharing): its own forward —
104    /// weights.layers stays empty, the KV caches are the shared ones.
105    pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
106    /// DeepSeek-V4 runs its own stack too: its hidden state is `hc_mult`
107    /// copies of a vector, so no loop written for a single residual
108    /// stream can carry it.
109    pub dsv4: Option<
110        Box<(
111            crate::dsv4::Dsv4Globals,
112            Vec<crate::dsv4::Dsv4Layer>,
113            crate::dsv4::Dsv4Cfg,
114            crate::dsv4::Dsv4State,
115        )>,
116    >,
117    /// DeepSeek-V4.1 owns the shared CED/CSA2 attention state, raw Engram
118    /// lookup and four-stream mHC handoff. It cannot use the V4 cache
119    /// layout, so it has a dedicated executor and state tuple.
120    pub dsv41: Option<
121        Box<(
122            crate::dsv41::Dsv41Globals,
123            Vec<crate::dsv41::Dsv41Layer>,
124            crate::dsv41::Dsv41Cfg,
125            crate::dsv41::Dsv41State,
126        )>,
127    >,
128    /// Optional V4.1 vision tower. Text-only files leave this unset.
129    pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
130    /// Prepared image rows consumed by the next V4.1 prefill.
131    dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
132    /// Qwen3.8-Flash-Next owns four residual streams plus QSA/PLE state;
133    /// the generic single-residual layer loop cannot represent it.
134    pub qwen4_exp: Option<
135        Box<(
136            crate::qwen4_exp::Globals,
137            Vec<crate::qwen4_exp::Layer>,
138            crate::qwen4_exp::Cfg,
139            crate::qwen4_exp::State,
140        )>,
141    >,
142    /// DeepSeek-V4's own speculation stack: three draft modules, each a full
143    /// layer, plus a confidence head on the last. Empty when the file has
144    /// none, which is the only signal the decode path needs.
145    pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
146    /// The draft's per-sequence state (KV rings, captured trunk hidden).
147    pub dspark: Option<crate::dsv4::DsparkState>,
148    /// Drafts awaiting their verdict: (position, proposals, still matching,
149    /// accepted so far).
150    pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
151    /// Accepted prefix length of every graded draft.
152    pub dspark_hist: Vec<usize>,
153    /// The real tokens the drafts were graded against — a degenerate,
154    /// repeating output would make any acceptance number meaningless, and
155    /// the cheapest guard against believing one is to count them.
156    pub dspark_real: Vec<u32>,
157    /// The trunk's expert picks for the last few tokens, per layer. The
158    /// union over a window of them is what a batched verify would have to
159    /// read, and the ratio to the pick count is all it could save.
160    pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
161    /// (unique, total) expert picks per draft, trunk side and draft side.
162    pub dspark_exp: Vec<(usize, usize, usize, usize)>,
163    /// Wall time spent in the deliberately out-of-core draft. Kept separate
164    /// from trunk decode so block batching can be judged without conflating
165    /// it with GPU chain variance.
166    pub dspark_draft_ns: u128,
167    /// LFM2 short-convolution geometry (present when the model has
168    /// `ShortConv` mixer layers).
169    pub short_conv_cfg: Option<ShortConvCfg>,
170    /// Multi-token-prediction head (None = absent).
171    pub mtp: Option<MtpModule>,
172    /// Speculative decode via MTP (greedy only; `CMF_MTP=0` disables).
173    pub speculative: bool,
174    rng: SplitMix64,
175    sampler_scratch: SamplerScratch,
176    /// Speculative SAMPLING state (graph_spec_step, temperature > 0): the
177    /// correction token a rejected draft produced — committed by the loop
178    /// top in place of a fresh draw — and the per-round draft
179    /// distributions / target scratch, reused so a round allocates
180    /// nothing at the vocab size.
181    spec_forced: Option<u32>,
182    spec_q: Vec<Vec<f32>>,
183    spec_p: Vec<f32>,
184    spec_res: Vec<f32>,
185    /// The same three for the sparse chain (top-k configs).
186    spec_qs: Vec<sampler::Sparse>,
187    spec_ps: sampler::Sparse,
188    spec_ress: sampler::Sparse,
189    /// Which arm the MTP draft block runs on this generation: Some(true)
190    /// = the whole-token graph (device attention, one submit a step),
191    /// Some(false) = the per-op path; None = not decided yet. Decided
192    /// on the first draft and held, because the two arms keep the MTP
193    /// KV in different places (device mirror vs the CPU cache) and a
194    /// mid-run switch would read the wrong one.
195    mtp_graph_mode: Option<bool>,
196    /// The Metal verify graph of the round in flight, between its sync
197    /// (logits read) and the commit that replays the accepted prefix.
198    #[cfg(target_os = "macos")]
199    metal_verify: Option<MetalVerifyPending>,
200    /// Precomputed RoPE inverse frequencies [head_dim/2]. Arc: the
201    /// forward path clones a handle to escape the &mut self borrow —
202    /// cloning the table itself was a per-forward allocation.
203    pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
204    /// Reusable norm buffers for the decode hot path (roadmap §3 P0:
205    /// steady-state forward should not heap-allocate). Disjoint field
206    /// from `weights`/`kv_cache`, so split borrows keep working.
207    ws: ForwardScratch,
208    /// Persistent worker pool (None = serial; see CMF_THREADS).
209    pool: Option<std::sync::Arc<Pool>>,
210    // ── Dynamic per-token skill routing (spec §9, claim 14/16) ──
211    /// Source model, retained so a skill switch can re-resolve the
212    /// touched layers' FFN tensors (Mapped = mmap pointers, cheap).
213    pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
214    /// Masks present → weights are dequantized f32 (rebuild path).
215    pub(crate) dyn_force_f32: bool,
216    /// Per-skill FFN layers actually replaced (derived from tensors, not
217    /// the meta `layers` field — ru2 replaces down_proj in 0..23 while
218    /// its meta says [20..23]). None = skill touches non-FFN tensors →
219    /// ineligible for cheap dynamic switching (honest refusal).
220    pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
221    /// Currently overlaid skill (index into model.header.skills); None =
222    /// backbone. Set at load time to the statically-overlaid skill so
223    /// `set_active_skill(None)` correctly reverts it (else a static
224    /// skill would silently persist — the union-diff assumes dyn_active
225    /// always mirrors the live overlay). Switched by `set_active_skill`.
226    pub(crate) dyn_active: Option<usize>,
227    /// Pipeline was loaded with a soft blend (materialized working
228    /// tensors, not a single skill index) → dynamic routing refuses:
229    /// there is no single index to revert the blend from.
230    pub(crate) dyn_blend_loaded: bool,
231    /// Layer whose post-residual hidden feeds the router φ (shared by
232    /// swarm skills). None = φ capture off.
233    pub(crate) dyn_phi_layer: Option<usize>,
234    /// EMA of φ at `dyn_phi_layer` over the decode window (on-policy).
235    dyn_phi_ema: Vec<f32>,
236    dyn_phi_seen: usize,
237    /// Hysteresis router driving per-token skill switches during decode
238    /// (None = static/no dynamic routing). Taken out during generation.
239    pub dyn_router: Option<crate::swarm::DynRouter>,
240    /// O(1) Nyström attention setting (CLI/env/header-hint resolved by
241    /// the caller; None = plain cache attention everywhere).
242    o1_cfg: Option<crate::nystrom::O1Cfg>,
243    /// Bumped once per collecting→sealed transition — the GPU state mirror
244    /// re-uploads when it sees a new epoch (each fresh sealed state).
245    o1_epoch: u64,
246    /// Per-layer o1 flags derived from `o1_cfg` (Full layers only).
247    o1_flags: Vec<bool>,
248    /// Emit a structured per-token trace (B4 telemetry channel). Off by
249    /// default — the runtime is silent unless observation is requested.
250    trace: bool,
251    /// Confidence-calibration temperature (B1): reported probability is
252    /// softmax(logits / calib_temp). 1.0 = raw. Set from header.calibration.
253    calib_temp: f32,
254    /// Process-unique id keying this pipeline's device KV mirrors.
255    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
256    graph_kv_id: u64,
257    /// Decode asks the token graph to also run final-norm + lm_head on
258    /// the device (drops the separate per-op lm_head round trip).
259    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
260    graph_want_logits: bool,
261    /// NLL quality gates require the graph's fused head rather than silently
262    /// accepting a CPU head fallback. Generation keeps the historical
263    /// best-effort `graph_want_logits` behavior.
264    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
265    graph_head_required: bool,
266    /// Logits the graph produced for the token just forwarded (taken by
267    /// the decode loop; None = compute on the CPU path).
268    graph_logits: Option<Vec<f32>>,
269    /// Token embeddings are multiplied by this at input (Gemma: √hidden).
270    pub embed_multiplier: f32,
271    /// Attention score scale (1/√head_dim unless the arch overrides —
272    /// Gemma's query_pre_attn_scalar).
273    pub attn_scale: f32,
274    /// Sliding-window attention: (window, every-Nth-layer-is-global
275    /// pattern) — Gemma-3.
276    pub swa: Option<(usize, usize)>,
277    /// Explicit local/global schedule for architectures that cannot be
278    /// represented by Gemma's every-Nth-global convention.
279    pub sliding_layers: Option<Vec<bool>>,
280    /// RoPE table of the sliding (local) layers, when they use their
281    /// own base frequency (Gemma-3: 10k local vs 1M global).
282    pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
283    pub rotary_dim_local: Option<usize>,
284    pub rope_scale: f32,
285    pub rope_scale_local: f32,
286    /// Gemma-4: global layers run their own geometry — (head_dim,
287    /// num_kv_heads); sliding layers keep the base fields.
288    pub global_attn: Option<(usize, usize)>,
289    /// Gemma-4: the global layers' proportional RoPE table (len
290    /// global_head_dim/2, zero-padded tail = identity rotation).
291    pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
292    /// Scale-less RMS normalization of V heads before caching (Gemma-4).
293    pub attn_v_norm: bool,
294    /// HunYuan dense: per-head q/k norm runs after RoPE (see the arch flag).
295    pub qk_norm_after_rope: bool,
296    /// Final-logit soft-capping C: logits = C·tanh(logits/C) (Gemma-4).
297    pub final_softcap: Option<f32>,
298    /// Cortiq Embryo hierarchical head: cluster matrix [C, hidden]. The
299    /// flat logits h·Eᵀ are turned into the two-level log-probabilities
300    /// log softmax_c(h·Cᵀ)[c(v)] + log softmax_{s∈c(v)}(h·E_c(v)ᵀ)[v].
301    pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
302    /// Gemma-2 attention-logit soft-capping (0.0 = off).
303    pub attn_softcap: f32,
304    /// Compute per-token confidence (a full-vocab softmax each
305    /// token). On by default; `bench --core` turns it off to match
306    /// llama-bench's core timing.
307    confidence_on: bool,
308    /// Test-only one-shot forward failure, scoped to this pipeline so
309    /// parallel scoring tests cannot consume one another's injection.
310    #[cfg(test)]
311    nll_test_fail_at: Option<usize>,
312    /// Test-only route override; avoids mutating the process-wide
313    /// `CMF_PREFILL` environment variable while forcing the serial path.
314    #[cfg(test)]
315    nll_test_force_serial: bool,
316}
317
318#[cfg(target_os = "macos")]
319impl Drop for Pipeline {
320    fn drop(&mut self) {
321        crate::gpu::kv_mirror_drop(self.graph_kv_id);
322    }
323}
324
325/// Model weights. Matrices are `QTensor` (owned f32 for small models
326/// and tests — bit-identical to the historical paths — or quantized
327/// bytes zero-copy from the CMF mmap for big models). 1-D norms are
328/// always small and stay f32.
329pub struct PipelineWeights {
330    /// Embedding table: [vocab_size, hidden_size]
331    pub embed_tokens: QTensor,
332    /// Per-layer weights
333    pub layers: Vec<LayerWeights>,
334    /// LM head: [vocab_size, hidden_size]
335    pub lm_head: QTensor,
336    /// Final norm: [hidden_size]
337    pub final_norm: Vec<f32>,
338}
339
340/// One transformer layer: shared norms + MLP, attention by kind.
341pub struct LayerWeights {
342    pub input_norm: Vec<f32>,
343    /// The pre-FFN norm (`post_attention_layernorm` classically;
344    /// `pre_feedforward_layernorm` on Gemma-2/3 sandwich layers).
345    pub post_norm: Vec<f32>,
346    /// Gemma-2/3 sandwich: norm applied to the ATTENTION OUTPUT before
347    /// its residual add (`post_attention_layernorm` there).
348    pub attn_out_norm: Option<Vec<f32>>,
349    /// Gemma-4: the whole layer output is multiplied by this scalar.
350    pub layer_scale: Option<f32>,
351    /// Gemma-2/3 sandwich: norm applied to the FFN OUTPUT before its
352    /// residual add (`post_feedforward_layernorm`).
353    pub ffn_out_norm: Option<Vec<f32>>,
354    pub ffn: FfnKind,
355    pub attn: AttnKind,
356}
357
358/// FFN gate activation: SiLU (SwiGLU family) or tanh-GELU (Gemma's
359/// GeGLU). A property of the model, carried on every FFN triple.
360#[derive(Clone, Copy, PartialEq, Debug, Default)]
361pub enum Act {
362    #[default]
363    Silu,
364    GeluTanh,
365    /// Kimi-K3 SituAndMul: BOTH halves transform —
366    /// a = β·tanh(g/β)·σ(g), up' = linβ·tanh(u/linβ) (linβ>0), out = a·up'.
367    Situ {
368        beta: f32,
369        linear_beta: f32,
370    },
371}
372
373impl Act {
374    pub fn from_arch(name: &str) -> Self {
375        if name == "gelu_tanh" {
376            Self::GeluTanh
377        } else {
378            Self::Silu
379        }
380    }
381
382    /// Arch-driven constructor (activation name + situ betas).
383    pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
384        match arch.hidden_act.as_str() {
385            "situ" => Self::Situ {
386                beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
387                linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
388            },
389            other => Self::from_arch(other),
390        }
391    }
392
393    #[inline]
394    pub fn apply(self, x: f32) -> f32 {
395        match self {
396            Self::Silu => inference::silu(x),
397            Self::GeluTanh => inference::gelu_tanh(x),
398            Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
399        }
400    }
401
402    /// Gated combine — the FFN contract. Situ transforms the UP half
403    /// too, so callers must use this instead of apply(g)·u.
404    #[inline]
405    pub fn combine(self, g: f32, u: f32) -> f32 {
406        match self {
407            Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
408                self.apply(g) * (linear_beta * (u / linear_beta).tanh())
409            }
410            _ => self.apply(g) * u,
411        }
412    }
413}
414
415/// Dense gated triple — the FFN of a dense layer or of one expert.
416pub struct DenseFfn {
417    pub gate_proj: QTensor,
418    pub up_proj: QTensor,
419    pub down_proj: QTensor,
420    /// Gate activation (SiLU default; Gemma: tanh-GELU).
421    pub act: Act,
422    /// `down_proj` stored transposed (`[inter, hidden]`), when the file
423    /// carries it. Only the per-token sparse path reads it: a neuron's
424    /// down weights are a contiguous ROW there, so the token's chosen
425    /// neurons are the only bytes touched. `None` = the ordinary layout,
426    /// and the sparse path stays off.
427    pub down_t: Option<QTensor>,
428    /// Task tubes (spec: defragged task-conditional width). The three
429    /// matrices above are the CORE — the neurons every task computes;
430    /// each tube is an independently quantized slice of the SAME layer
431    /// holding the neurons only some tasks need. A tube is a normal
432    /// tensor triple, so every kernel runs it unchanged, and the bytes
433    /// of an inactive tube are never read. Empty = ordinary dense FFN.
434    pub segs: Vec<FfnSeg>,
435}
436
437/// One task tube: a contiguous slice of a layer's FFN neurons, stored
438/// as its own `[w, hidden]` / `[hidden, w]` triple. `start` is the
439/// neuron's index in the layer's FULL space (core first, then tubes in
440/// order) — the bit a task mask sets to switch this tube on.
441pub struct FfnSeg {
442    pub gate: QTensor,
443    pub up: QTensor,
444    pub down: QTensor,
445    pub start: usize,
446    pub width: usize,
447}
448
449/// FFN operator of a layer, decided by tensor presence at load time
450/// (router `mlp.gate.weight` in the directory = MoE layer).
451pub enum FfnKind {
452    Dense(DenseFfn),
453    /// Mixture-of-Experts (Qwen2-MoE / Qwen3-MoE): softmax over ALL
454    /// expert logits → top-k, optional renorm; experts stay quantized
455    /// in mmap — only the selected ones are touched per token.
456    Moe(MoeFfn),
457    /// Gemma-4 MoE: a dense MLP branch AND a routed-expert branch in
458    /// the SAME layer, each with its own norm sandwich. The dense
459    /// branch reads the pre-FFN-normed input; the expert branch (and
460    /// the router) read the RAW residual through `pre_norm_2`:
461    ///   d = post_norm_1(dense(x̂));  m = post_norm_2(Σwₑ·FFNₑ(pre_norm_2(h)))
462    ///   ffn_out = d + m   (the caller's ffn_out_norm + residual follow)
463    DenseMoe(Box<DenseMoeFfn>),
464}
465
466/// Gemma-4 dual-branch FFN (see `FfnKind::DenseMoe`).
467pub struct DenseMoeFfn {
468    pub dense: DenseFfn,
469    pub moe: MoeFfn,
470    /// post_feedforward_layernorm_1 — dense-branch output norm.
471    pub post_norm_1: Vec<f32>,
472    /// pre_feedforward_layernorm_2 — expert-branch input norm (applied
473    /// to the RAW residual, not the pre-FFN-normed activation).
474    pub pre_norm_2: Vec<f32>,
475    /// post_feedforward_layernorm_2 — expert-branch output norm.
476    pub post_norm_2: Vec<f32>,
477}
478
479pub struct MoeFfn {
480    /// Router `mlp.gate.weight` [num_experts, hidden].
481    pub router: QTensor,
482    pub experts: Vec<DenseFfn>,
483    pub top_k: usize,
484    pub norm_topk_prob: bool,
485    /// Router scores per-expert with a sigmoid (LFM2-MoE / DeepSeek-V3
486    /// `noaux_tc`) instead of a softmax over all experts (Qwen).
487    pub router_sigmoid: bool,
488    /// Per-expert selection bias `mlp.expert_bias` [num_experts]
489    /// (LFM2-MoE): added to the sigmoid scores for the top-k CHOICE only;
490    /// the gathered weights use the unbiased scores. None = no bias.
491    pub expert_bias: Option<Vec<f32>>,
492    /// Top-k weights are multiplied by this after the optional renorm
493    /// (LFM2-MoE `routed_scaling_factor`; 1.0 = off).
494    pub routed_scaling: f32,
495    /// Adaptive routing (CMF_MOE_TAU, opt-in): keep the smallest
496    /// prefix of the top-k whose renormalized mass reaches τ —
497    /// confident tokens touch 1–2 experts, flat ones keep all k.
498    /// MoE decode is memory-bound, so skipped experts are skipped
499    /// weight traffic. None = classic fixed top-k (bit-identical).
500    pub route_tau: Option<f32>,
501    /// Always-on shared expert. Qwen2-MoE carries an additional sigmoid
502    /// gate; Laguna adds the shared expert unconditionally (`None`).
503    pub shared: Option<(DenseFfn, Option<QTensor>)>,
504    /// Expert-selection counters (truncated Fisher B-field of claim 12:
505    /// routing frequency during calibration). Filled by every forward,
506    /// read by the CLI via CMF_MOE_STATS. RefCell: decode is single-threaded.
507    pub stats: std::cell::RefCell<Vec<u64>>,
508    /// Per-CHANNEL sum of squares of this FFN's input, accumulated over a
509    /// calibration run (`CMF_RMS_TRACE`). These are the RMS activation
510    /// traces AWNP needs: raw weight magnitude says every channel matters
511    /// equally, and the question AWNP asks is whether the ACTIVATIONS
512    /// disagree. Off unless the env var is set — an f64 add per channel
513    /// per token is cheap, but not free.
514    pub act_sq: std::cell::RefCell<Vec<f64>>,
515    /// Raw FFN-input rows captured for the layers named by `CMF_ACT_DUMP`
516    /// (`"9,19"`). AWNP is nullspace PROJECTION: after dropping channels the
517    /// survivors are refitted to absorb what was removed, and how much they
518    /// can absorb depends on the activation COVARIANCE, not on per-channel
519    /// RMS. Per-channel numbers can only bound the cost from above.
520    pub act_rows: std::cell::RefCell<Vec<f32>>,
521    /// Task mask over routed experts (DTG-MA over MoE, claim-12 B-field
522    /// applied): `false` experts are excluded from selection, the
523    /// softmax renormalizes over the allowed set. Built by the loader
524    /// from CMF_MOE_MASK=<stats.json> + CMF_MOE_MASK_COVER. None = all.
525    pub mask: Option<Vec<bool>>,
526    /// Gemma-4: per-expert weight scale applied AFTER the top-k renorm
527    /// (`router.per_expert_scale`). None = 1.0 everywhere.
528    pub per_expert_scale: Option<Vec<f32>>,
529    /// Gemma-4: the router reads a SCALE-LESS rms-norm of its input
530    /// (the constant gain router.scale·√hidden is folded into the
531    /// router weights at convert time).
532    pub router_input_norm: bool,
533    /// Cortiq Embryo: resonance routing (P1) — the "logits" are
534    /// bias_e − ‖(x−μ_e) − U_eᵀU_e(x−μ_e)‖², argmax = the expert whose
535    /// descriptor reconstructs the input best. `router` is a placeholder.
536    pub resonance: Option<Resonance>,
537}
538
539/// Per-expert resonance descriptors of one MoE layer (`mlp.desc.*`).
540pub struct Resonance {
541    /// [E, hidden]
542    pub mu: Vec<f32>,
543    /// [E, k, hidden] orthonormal directions (k may be 0)
544    pub u: Vec<f32>,
545    pub k: usize,
546    /// [E] selection bias (loss-free balancing, trained online)
547    pub bias: Vec<f32>,
548}
549
550impl Resonance {
551    /// Routing scores for one input row (higher = better).
552    pub fn scores(&self, x: &[f32], out: &mut [f32]) {
553        let h = x.len();
554        let ne = out.len();
555        for e in 0..ne {
556            let mu = &self.mu[e * h..(e + 1) * h];
557            let mut d2 = 0.0f32;
558            for j in 0..h {
559                let d = x[j] - mu[j];
560                d2 += d * d;
561            }
562            let mut proj = 0.0f32;
563            for i in 0..self.k {
564                let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
565                let mut p = 0.0f32;
566                for j in 0..h {
567                    p += (x[j] - mu[j]) * u[j];
568                }
569                proj += p * p;
570            }
571            out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
572        }
573    }
574}
575
576/// Attention operator of a layer. Extension point: new operators are
577/// new variants here + a forward in their own module.
578pub enum AttnKind {
579    /// GQA softmax attention (+ optional Qwen3.5 qk-norm / output gate).
580    Full {
581        wq: QTensor,
582        wk: QTensor,
583        wv: QTensor,
584        wo: QTensor,
585        q_norm: Option<Vec<f32>>,
586        k_norm: Option<Vec<f32>>,
587        output_gate: bool,
588        /// Laguna: a separate softplus projection applied to the attention
589        /// output before O. The bool means one scalar per head (broadcast
590        /// across head_dim); false means one scalar per element.
591        softplus_gate: Option<(QTensor, bool)>,
592        /// Qwen2-family projection biases (q, k, v).
593        bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
594    },
595    /// Canonical linear core (VMF phase attention).
596    Linear(VmfPhaseWeights),
597    /// Faithful vendor linear operator (Qwen3.5 GatedDeltaNet).
598    LinearGdn(GdnWeights),
599    /// LFM2 gated short-convolution mixer (no KV cache; conv ring state
600    /// lives in the layer's `linear_state`).
601    ShortConv(ShortConvWeights),
602    /// DeepSeek-V2 Multi-head Latent Attention. v1 executes it as
603    /// expand-to-MHA: the latent is projected per token, K/V expand to
604    /// every head and live in the ordinary cache (K head layout
605    /// [rope | nope] so the standard partial rotary covers the shared
606    /// rope key; V rows are zero-padded to the K head_dim and the pad
607    /// is sliced off before O). Latent-resident cache is a later
608    /// optimization, not a semantic change.
609    Mla(Box<MlaWeights>),
610    /// Kimi Delta Attention (Kimi Linear / Kimi-K3): per-channel decayed
611    /// delta rule, separate q/k/v short convs, sigmoid-gated output norm.
612    /// State lives in the layer's `linear_state` (no KV cache).
613    Kda(Box<crate::linear_core::KdaWeights>),
614}
615
616/// DeepSeek-V2 MLA projections (see `AttnKind::Mla`).
617pub struct MlaWeights {
618    /// `[nh·(rope+nope), hidden]` (or `[…, q_lora]` when compressed) —
619    /// the converter permutes each head rope-first so rotary_dim =
620    /// qk_rope works unchanged.
621    pub q_proj: QTensor,
622    /// Compressed q (K3/V3 class): x → q_a `[q_lora, hidden]` →
623    /// rms(q_a_norm) → q_proj (= q_b). None = direct q (V2-Lite).
624    pub q_a: Option<QTensor>,
625    pub q_a_norm: Option<Vec<f32>>,
626    /// `kv_a_proj_with_mqa` `[lora + rope, hidden]` (latent first).
627    pub kv_a: QTensor,
628    /// RMS-norm weights over the latent (`kv_a_layernorm`, [lora]).
629    pub kv_a_norm: Vec<f32>,
630    /// `[nh·(nope+v), lora]` — per head [k_nope | v].
631    pub kv_b: QTensor,
632    /// `[hidden, nh·v]`.
633    pub o_proj: QTensor,
634    pub nh: usize,
635    pub qk_rope: usize,
636    pub qk_nope: usize,
637    pub v_dim: usize,
638    pub lora: usize,
639    /// Softmax scale (1/√(rope+nope), YaRN-mscale-corrected at load).
640    pub scale: f32,
641    /// Kimi Linear NoPE: skip the rotary entirely (layout unchanged).
642    pub nope: bool,
643}
644
645/// Multi-token-prediction head (DeepSeek/Qwen style, spec §2.1):
646/// `x = eh_proj·[enorm(embed(next)); hnorm(hidden)]` → one transformer
647/// block over its own KV → shared lm_head. Drafts the token after next;
648/// the main model verifies, so output is exact — MTP only buys speed.
649pub struct MtpModule {
650    pub enorm: Vec<f32>,
651    pub hnorm: Vec<f32>,
652    /// [hidden, 2·hidden]
653    pub eh_proj: QTensor,
654    pub layer: LayerWeights,
655    pub final_norm: Vec<f32>,
656    pub kv: crate::kv_cache::LayerKvCache,
657}
658
659/// A Metal verify graph after its sync: what the commit needs — the
660/// graph (per-layer replay scratch), the GDN layers in encode order (their
661/// CPU states receive the replay), and the attention layers with the CPU
662/// row count they were encoded against (the accepted rows are pulled from
663/// the mirror from there).
664/// One item of the Metal rows-graph plan.
665#[cfg(target_os = "macos")]
666enum MetalRowsItem<'a> {
667    Gdn {
668        run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
669        first: usize,
670    },
671    Attn {
672        l: crate::gpu_metal::AttnGpuLayer<'a>,
673        li: usize,
674        q_norm: Option<&'a [f32]>,
675        k_norm: Option<&'a [f32]>,
676        output_gate: bool,
677    },
678}
679
680#[cfg(target_os = "macos")]
681struct MetalVerifyPending {
682    graph: crate::gpu_metal::VerifyGraph,
683    gdn_layers: Vec<usize>,
684    attn_layers: Vec<(usize, usize)>,
685}
686
687#[cfg(target_os = "macos")]
688enum MetalRowsRun {
689    /// Capability/preflight refusal before a command buffer was committed.
690    Declined,
691    /// A graph was admitted and then failed; callers must clear the sequence
692    /// rather than replaying it through CPU/serial state.
693    Failed,
694    Completed(MetalVerifyPending),
695}
696
697#[cfg(target_os = "macos")]
698enum MetalPrefillOutcome {
699    Declined,
700    Failed,
701    Completed(Vec<f32>),
702}
703
704#[cfg(target_os = "macos")]
705enum MetalBatchNllOutcome {
706    Declined,
707    Failed(String),
708    Completed(f64, usize),
709}
710
711/// The speculation trial's phases (see the decode loop): four timed
712/// speculative rounds, eight timed plain tokens, then the faster arm
713/// until a re-check.
714#[derive(Clone, Copy)]
715enum SpecTrial {
716    Spec {
717        t0: std::time::Instant,
718        gen0: usize,
719        rounds: usize,
720    },
721    Plain {
722        t0: std::time::Instant,
723        gen0: usize,
724    },
725    Decided {
726        spec: bool,
727        recheck_at: usize,
728    },
729}
730
731/// The speculation monitor: exponential averages of a round's wall time
732/// and of the tokens it produced, and the plain token's wall time — the
733/// three numbers the keep/stop rule needs. A round pays when
734/// `tokens_per_round · plain_ms > round_ms · 1.03`. The one-shot trial
735/// (four rounds against eight tokens) mis-called prose: the first rounds
736/// after a prompt are formulaic and accept well, the body does not (an
737/// essay measured 39 against a plain 44.8 with the trial saying
738/// "speculate"), so the rule now runs on EVERY round and stops after four
739/// consecutive losing rounds; a stopped speculation is retried 128 tokens
740/// later.
741#[derive(Default, Clone, Copy)]
742struct SpecMon {
743    round_ms: f64,
744    tokens: f64,
745    plain_ms: f64,
746    n: u32,
747    fails: u32,
748}
749
750impl SpecMon {
751    fn round(&mut self, dt_ms: f64, produced: usize) {
752        self.n += 1;
753        if self.n == 1 {
754            return; // round 1 pays the batch scratch and the draft mirror
755        }
756        let a = if self.n == 2 { 1.0 } else { 0.3 };
757        self.round_ms += a * (dt_ms - self.round_ms);
758        self.tokens += a * (produced as f64 - self.tokens);
759    }
760    fn pays(&self) -> bool {
761        self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
762    }
763}
764
765/// Result of a generation call.
766pub struct GenerateResult {
767    pub text: String,
768    pub token_ids: Vec<u32>,
769    pub prompt_tokens: usize,
770    pub tokens_generated: usize,
771    pub finish_reason: String,
772    /// Speculative-decode stats (0/0 when MTP is absent or inactive).
773    pub mtp_drafted: usize,
774    pub mtp_accepted: usize,
775    /// Per-generated-token confidence = softmax probability of the token
776    /// that was actually emitted (softmax probability on the chosen state). High =
777    /// the model was sure; low = it was guessing. Same length as the
778    /// generated slice of `token_ids`.
779    pub token_confidence: Vec<f32>,
780    /// Structured per-token telemetry (B4 channel). Empty unless
781    /// `set_trace(true)`; otherwise same length as the generated slice.
782    pub traces: Vec<TokenTrace>,
783}
784
785/// One row of the structured telemetry trace (B4): the model's internal
786/// routing state at the moment a token was emitted. Every field is a
787/// quantity the runtime already computes — nothing is inferred or
788/// estimated (anti-principle: only measured bytes).
789#[derive(Clone, Debug)]
790pub struct TokenTrace {
791    /// 0-based index within the generated slice.
792    pub t: usize,
793    /// The emitted token id.
794    pub token_id: u32,
795    /// Softmax probability on the emitted token — how sure the model was.
796    pub confidence: f32,
797    /// Skill in force while this token was generated (None = backbone).
798    pub active_skill: Option<String>,
799    /// Recon error E = ‖r−BBᵀr‖²/‖φ‖² at the last routing eval — coherence
800    /// with the active skill's subspace (low = coherent). None = no router
801    /// or not yet evaluated.
802    pub recon: Option<f32>,
803    /// The router changed the active skill right after this token (a
804    /// domain boundary crossed under the hysteresis barrier).
805    pub switched: bool,
806}
807
808/// Calibrated softmax probability of `id` under `logits` (the confidence on
809/// the emitted token) — the confidence signal, cheap from logits already
810/// computed for sampling. `temp` is the calibration temperature (B1):
811/// softmax(logits / temp); 1.0 = raw.
812#[cfg_attr(not(test), allow(dead_code))]
813fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
814    let t = if temp > 1e-3 { temp } else { 1.0 };
815    let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
816    let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
817    if sum > 0.0 {
818        (((logits[id as usize] - max) / t).exp()) / sum
819    } else {
820        0.0
821    }
822}
823
824/// prefill-GEMM enabled? (CMF_PREFILL=seq — emergency fallback to the
825/// sequential path.)
826fn prefill_batched() -> bool {
827    std::env::var("CMF_PREFILL")
828        .map(|v| v != "seq")
829        .unwrap_or(true)
830}
831
832/// Decide the graph NLL route without conflating graph quality with the
833/// optional native-Metal fused head. A hidden-state graph remains a valid
834/// quality route on Vulkan/Wgpu; only native Metal requires graph logits.
835#[inline]
836fn nll_graph_policy(
837    unmasked: bool,
838    prefer_graph: bool,
839    native_metal: bool,
840) -> (bool, bool) {
841    let graph_quality = unmasked && prefer_graph;
842    let fused_head_quality = graph_quality && native_metal;
843    (graph_quality, fused_head_quality)
844}
845
846/// Input to the layer-major batched span walk: token ids (embeds itself,
847/// full-stack and coordinator prefill) or ready boundary hiddens (the
848/// network worker's side of a split).
849#[derive(Clone, Copy)]
850enum PrefillIn<'a> {
851    Ids(&'a [u32]),
852    Hidden(&'a [f32]),
853}
854
855/// The batched prefill walks `weights.layers`. Architectures that load
856/// their own stack (gemma-3n's AltUp replicas, DeepSeek-V4's hyper-
857/// connections) leave that empty and must go position by position — asking
858/// otherwise indexes an empty vector, which is a panic rather than a
859/// fallback. Every call site goes through here so the next such
860/// architecture is one line, not four.
861impl Pipeline {
862    fn can_prefill_batched(&self) -> bool {
863        #[cfg(test)]
864        let force_serial = self.nll_test_force_serial;
865        #[cfg(not(test))]
866        let force_serial = false;
867        prefill_batched() && !force_serial && !self.weights.layers.is_empty()
868    }
869
870    /// The backend's automatic capacity split for a mapped transformer.
871    /// Kept as a method so prefill and decode use the exact same boundary.
872    fn automatic_gpu_prefix(&self) -> Option<usize> {
873        let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
874        crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
875    }
876}
877
878/// Prefill chunk (positions per batched pass). On macOS the AMX GEMM
879/// path wants tall panels — M=48 starves the matrix units (ggml uses
880/// ubatch 512); elsewhere the historical 48 stays. CMF_PREFILL_CHUNK
881/// overrides. Pub: the network split MUST chunk identically to the
882/// local path — panel width reorders float accumulation, so a different
883/// chunk is a different (equally valid) generation.
884pub fn prefill_chunk() -> usize {
885    if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
886        .ok()
887        .and_then(|v| v.parse::<usize>().ok())
888    {
889        return n.max(1);
890    }
891    if cfg!(target_os = "macos") {
892        512
893    } else if cfg!(target_arch = "aarch64") {
894        // Mobile: big enough to feed the batched attend (gate b ≥ 32)
895        // and the blocked SDOT GEMM without the memory of 512.
896        256
897    } else {
898        48
899    }
900}
901
902/// Number of prompt rows that have a real teacher-forced next-token pair in a
903/// prefill span.  The final prompt row has no successor token, so it must not
904/// be handed to the MTP warm-up.  Keeping this arithmetic in one helper makes
905/// the full-chunk and tail-chunk boundaries explicit for both the graph and
906/// CPU implementations.
907#[inline]
908fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
909    if end <= start || start >= input_len {
910        return 0;
911    }
912    let rows = (end.min(input_len) - start).min(input_len - start);
913    if end < input_len {
914        rows
915    } else {
916        rows.saturating_sub(1)
917    }
918}
919
920/// Callback for streaming tokens. Return `false` to cancel.
921pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
922
923impl Pipeline {
924    /// Clear all per-sequence state, including backend device mirrors.
925    ///
926    /// The host KV/history buffers are only half of the request lifecycle on
927    /// wgpu: GDN/O(1) state and cached graph bind groups are keyed by the
928    /// pipeline id and otherwise survive a pooled request.  Keep every fresh
929    /// sequence entry point on this one reset path so a new request cannot
930    /// inherit the prior request's device state.
931    fn clear_sequence_state(&mut self) {
932        self.kv_cache.clear();
933        self.kv_history.clear();
934        if let Some(b) = &mut self.dsv41 {
935            b.3.clear();
936        }
937        crate::gpu::graph_kv_reset(self.graph_kv_id);
938        // MTP is detached from `self` for the duration of generation, so its
939        // device mirror is not covered by the trunk reset above.  Reset the
940        // derived id as well: a failed/aborted warm-up must never leave a
941        // mirror that a later request can mistake for a current MTP cache.
942        crate::gpu::graph_kv_reset(self.mtp_kv_id());
943    }
944
945    /// Finish a generation lifecycle after the MTP/router owners were
946    /// detached.  Every terminal path must put those owners back before the
947    /// pooled pipeline can serve another request.  Graph side channels and
948    /// device mirrors are cleared on errors and cancellations; a successful
949    /// generation keeps its decode-ready host cache for KV reuse.
950    fn finish_generation(
951        &mut self,
952        mtp: &mut Option<MtpModule>,
953        router: &mut Option<crate::swarm::DynRouter>,
954        clear_sequence: bool,
955    ) {
956        // A dynamic route may have switched the overlay before the terminal
957        // path. Restore the backbone while the detached router is still
958        // available, because set_active_skill also owns the overlay reset.
959        if router.is_some() {
960            let _ = self.set_active_skill(None);
961        }
962        if clear_sequence {
963            self.clear_sequence_state();
964            if let Some(m) = mtp.as_mut() {
965                // The MTP owner is detached while generation runs, so the
966                // trunk reset above cannot clear its host cache.  Drop its
967                // partial rows before reattaching it to the pooled pipeline;
968                // the next request must start from the same empty anchor on
969                // CPU and on the device mirror.
970                m.kv.clear();
971            }
972            if let Some(m) = self.mtp.as_mut() {
973                // A non-speculative request leaves the configured MTP owner
974                // attached.  Clear that dormant cache too when a shared
975                // generation failure/cancellation resets the sequence.
976                m.kv.clear();
977            }
978        }
979        self.graph_want_logits = false;
980        self.graph_head_required = false;
981        self.graph_logits = None;
982        self.graph_failed
983            .store(false, std::sync::atomic::Ordering::Relaxed);
984        self.cancel
985            .store(false, std::sync::atomic::Ordering::Relaxed);
986        self.dyn_router = router.take().or(self.dyn_router.take());
987        self.mtp = mtp.take().or(self.mtp.take());
988        self.mtp_graph_mode = None;
989        self.spec_forced = None;
990    }
991
992    /// Consume a graph failure reported by a forward that returns only a
993    /// hidden vector.  `forward_ids` is a public Result API, so it must not
994    /// turn the graph's zero hidden sentinel into a valid lm_head result.
995    fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
996        if self
997            .graph_failed
998            .swap(false, std::sync::atomic::Ordering::Relaxed)
999        {
1000            self.cancel
1001                .store(false, std::sync::atomic::Ordering::Relaxed);
1002            self.clear_sequence_state();
1003            self.graph_logits = None;
1004            self.graph_want_logits = false;
1005            self.graph_head_required = false;
1006            return Err(format!("GPU graph failed during {phase} at position {pos}"));
1007        }
1008        Ok(())
1009    }
1010
1011    #[cfg(target_os = "macos")]
1012    fn fail_metal_graph(&mut self, reason: &str) {
1013        crate::pipeline::METAL_GRAPH_ERRORS
1014            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1015        self.clear_sequence_state();
1016        self.graph_logits = None;
1017        self.graph_failed
1018            .store(true, std::sync::atomic::Ordering::Relaxed);
1019        self.cancel
1020            .store(true, std::sync::atomic::Ordering::Relaxed);
1021        tracing::error!("native Metal TokenGraph failed closed: {reason}");
1022    }
1023
1024    /// Start an NLL/PPL request with all graph side channels in a known
1025    /// state.  A graph failure also raises the cooperative cancel bit; it is
1026    /// consumed here and that graph-induced bit is cleared so an independent
1027    /// request can be reused.  A caller-owned cancellation remains intact.
1028    fn nll_begin(&mut self) -> Result<(), String> {
1029        if self
1030            .graph_failed
1031            .swap(false, std::sync::atomic::Ordering::Relaxed)
1032        {
1033            self.cancel
1034                .store(false, std::sync::atomic::Ordering::Relaxed);
1035            self.clear_sequence_state();
1036            self.graph_logits = None;
1037            self.graph_want_logits = false;
1038            self.graph_head_required = false;
1039            return Err("GPU graph failed before NLL scoring".to_string());
1040        }
1041        self.clear_sequence_state();
1042        self.graph_logits = None;
1043        self.graph_want_logits = false;
1044        self.graph_head_required = false;
1045        Ok(())
1046    }
1047
1048    /// End an NLL/PPL request, including the side channels that are not part
1049    /// of the host KV cache.  This is intentionally explicit instead of
1050    /// relying on a tuple/sentinel return: callers must see every failure.
1051    fn nll_end(&mut self) {
1052        self.clear_sequence_state();
1053        self.graph_logits = None;
1054        self.graph_want_logits = false;
1055        self.graph_head_required = false;
1056        self.graph_failed
1057            .store(false, std::sync::atomic::Ordering::Relaxed);
1058    }
1059
1060    /// Check the graph failure channel at a scoring boundary and leave the
1061    /// pipeline reusable when the device path failed.
1062    fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1063        #[cfg(test)]
1064        if self.nll_test_fail_at == Some(pos) {
1065            self.nll_test_fail_at = None;
1066            self.graph_failed
1067                .store(true, std::sync::atomic::Ordering::Relaxed);
1068            self.cancel
1069                .store(true, std::sync::atomic::Ordering::Relaxed);
1070        }
1071        if self
1072            .graph_failed
1073            .swap(false, std::sync::atomic::Ordering::Relaxed)
1074        {
1075            self.cancel
1076                .store(false, std::sync::atomic::Ordering::Relaxed);
1077            self.clear_sequence_state();
1078            self.graph_logits = None;
1079            self.graph_want_logits = false;
1080            return Err(format!(
1081                "GPU graph failed during NLL {phase} at position {pos}"
1082            ));
1083        }
1084        Ok(())
1085    }
1086
1087    /// Map a virtual layer index to its physical weight index.
1088    /// Looped Transformer (Nanbeige 4.2): 22 physical layers × 2 loops = 44 virtual;
1089    /// virtual layer 23 maps back to physical layer 1 (23 % 22 = 1).
1090    #[inline]
1091    pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1092        virtual_idx % self.physical_layers
1093    }
1094
1095    /// True when `virtual_idx` is the last layer of a loop iteration
1096    /// (used for loop_final_norm insertion).
1097    #[inline]
1098    pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1099        self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1100    }
1101
1102    /// Build a pipeline from parts (used by the loader and tests).
1103    #[allow(clippy::too_many_arguments)]
1104
1105    /// Whole-block q1 token graph on the GPU (macOS/Metal): the run of
1106    /// consecutive q1 layers — GDN *and* full attention — starting at
1107    /// `start` executes as few command buffers as the CPU truly needs.
1108    /// Hidden stays device-resident across every layer; the only syncs
1109    /// are before each CPU attend (it needs q/k/v and owns the KV
1110    /// cache) and the final hidden readback. Recurrent states
1111    /// round-trip through shared memory (the CPU stays their owner, so
1112    /// every other path remains coherent). Returns the first layer
1113    /// index NOT covered (== `start` → refused, caller falls through
1114    /// to the per-layer CPU path).
1115    /// Should prefill run position-by-position through the GPU token
1116    /// graph instead of the batched CPU chunk-GEMM? True for q1 GDN
1117    /// hybrids on native Metal: their chunk prefill is walled by the
1118    /// sequential scalar recurrence, so the graph's decode rate wins.
1119    /// NOT for Looped Transformers, despite the per-chunk loop_final_norm
1120    /// sync: the chunk-GEMM amortizes each weight over the whole chunk,
1121    /// which the per-position graph cannot (Nanbeige 4.2 on M4, 512-token
1122    /// prompt: 85 tok/s chunked vs 14 through the graph).
1123    #[cfg(target_os = "macos")]
1124    fn graph_prefill_preferred(&self) -> bool {
1125        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1126        if !crate::gpu::enabled_here()
1127            || !graph_force
1128            || std::env::var("CMF_GPU_BLOCK")
1129                .map(|v| v == "0")
1130                .unwrap_or(false)
1131            // CMF_PREFILL_GRAPH=0: the chunked prefill (GEMM projections,
1132            // CPU recurrence) instead of the per-position token graph.
1133            || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1134        {
1135            return false;
1136        }
1137        self.weights
1138            .layers
1139            .iter()
1140            .any(|lw| {
1141                matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1142            })
1143    }
1144
1145    #[cfg(not(target_os = "macos"))]
1146    fn graph_prefill_preferred(&self) -> bool {
1147        // Discrete-GPU wgpu whole-token graph: GDN layers carry recurrent state
1148        // (conv ring + delta-rule S) resident on the GPU. A batched CPU prefill
1149        // builds that state on the CPU only, leaving the GPU buffers zeroed at
1150        // decode → garbage. Route GDN-hybrid prefill through the graph one
1151        // position at a time so the resident state is seeded exactly as decode
1152        // will read it. Pure-attention models keep the batched CPU prefill (its
1153        // KV mirror re-syncs from the CPU cache, so no seeding gap).
1154        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1155        if !graph_on || !crate::gpu::enabled_here() {
1156            return false;
1157        }
1158        // The descriptor-aware Prism graph now carries both the FWHT/affine
1159        // transforms and resident GDN state, so it is also the exact prefill
1160        // path for this model.  Keeping it here (rather than falling through
1161        // to the CPU chunk walk) is required for a long prompt to seed the
1162        // same device state that decode consumes.
1163        // O(1) needs the CPU prefill: the q-trace that seals the Nyström
1164        // skeleton is recorded there and nowhere else. The GDN half of
1165        // the hybrid loses nothing — the graph's first decode creates
1166        // its (ring, S) entries seeded from `cpu_state`, the same
1167        // handoff every graph run relies on when the entry is fresh.
1168        // Without this line the two designs collide on hybrids and o1
1169        // never becomes graph-portable: prefill through the graph
1170        // records no trace, so views stay None forever.
1171        if self.o1_active() {
1172            return false;
1173        }
1174        self.weights
1175            .layers
1176            .iter()
1177            .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1178    }
1179
1180    #[cfg(target_os = "macos")]
1181    fn q1_graph_gpu(
1182        &mut self,
1183        start: usize,
1184        upto: Option<usize>,
1185        position: usize,
1186        h: &mut [f32],
1187    ) -> usize {
1188        let _mt0 = std::time::Instant::now(); // CMF_METAL_HOSTPROF
1189        use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1190        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1191        if self.attn_softcap > 0.0 // capped scores: no graph kernel — CPU path
1192            || !crate::gpu::enabled_here()
1193            || !graph_force
1194            || std::env::var("CMF_GPU_BLOCK")
1195                .map(|v| v == "0")
1196                .unwrap_or(false)
1197        {
1198            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1199                eprintln!(
1200                    "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
1201                    self.attn_softcap > 0.0,
1202                    crate::gpu::enabled_here(),
1203                    graph_force,
1204                );
1205            }
1206            if self.graph_head_required {
1207                self.fail_metal_graph("native graph front gate refused");
1208            }
1209            return start;
1210        }
1211        // The graph encodes SiLU FFN and full-context attention with an
1212        // explicit model scale. Architectures with sliding windows,
1213        // sandwich norms or non-SiLU FFNs still fall back to the CPU path.
1214        if self.swa.is_some()
1215            || self.global_attn.is_some()
1216            || self.attention_heads_per_layer.is_some()
1217            || self.attn_v_norm
1218            || self.weights.layers.iter().any(|lw| {
1219                lw.attn_out_norm.is_some()
1220                    || lw.ffn_out_norm.is_some()
1221                    || lw.layer_scale.is_some()
1222                    || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1223            })
1224        {
1225            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1226                eprintln!(
1227                    "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1228                    self.swa.is_some(),
1229                    self.global_attn.is_some(),
1230                    self.attention_heads_per_layer.is_some(),
1231                    self.attn_v_norm,
1232                    (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1233                );
1234            }
1235            if self.graph_head_required {
1236                self.fail_metal_graph("native graph architecture gate refused");
1237            }
1238            return start;
1239        }
1240        // Looped Transformer: the graph covers ALL loop iterations;
1241        // encode_loop_norm is inserted on-device at each boundary.
1242        let limit = upto
1243            .map(|u| u + 1)
1244            .unwrap_or(self.num_layers)
1245            .min(self.num_layers);
1246
1247        enum Item<'a> {
1248            Gdn {
1249                run: Vec<GdnGpuLayer<'a>>,
1250                first: usize,
1251            },
1252            Attn {
1253                l: AttnGpuLayer<'a>,
1254                li: usize,
1255                q_norm: Option<&'a [f32]>,
1256                k_norm: Option<&'a [f32]>,
1257                output_gate: bool,
1258                bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1259                /// Attend on the device too (no sync): F32 KV, no
1260                /// o1/bias, dims inside the kernels' contract.
1261                full_gpu: bool,
1262            },
1263        }
1264
1265        // Device-attend KERNEL contract, shared by every Full layer. The
1266        // hd>128 default-off POLICY is applied after the scan: it was
1267        // measured on dense models, and a MoE plan inverts it — with the
1268        // experts on device each CPU-attend sandwich costs a
1269        // commit+wait, ~30 submits/token (W2 on M4: 14.7 tok/s
1270        // sandwiched vs 27.1 device-attend vs 18.8 pure CPU).
1271        let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1272        let attend_contract = attend_mode != "0"
1273            && attend_mode != "off"
1274            && self.head_dim % 4 == 0
1275            && self.head_dim <= 256
1276            && self.rotary_dim >= 2
1277            && self.rotary_dim <= self.head_dim
1278            && (self.rotary_dim / 2) % 32 == 0
1279            && self.num_kv_heads > 0
1280            && self.num_heads % self.num_kv_heads == 0;
1281
1282        let mut plan: Vec<Item> = Vec::new();
1283        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1284        // Break-reason diagnostics ride the same env as the plan summary.
1285        let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1286        let mut scan = start;
1287        while scan < limit {
1288            let lw = &self.weights.layers[self.phys_layer(scan)];
1289            let ffn = match &lw.ffn {
1290                FfnKind::Dense(d) if d.segs.is_empty() => {
1291                    let (Some(g), Some(u), Some(dn)) = (
1292                        d.gate_proj.metal_graph_parts(),
1293                        d.up_proj.metal_graph_parts(),
1294                        d.down_proj.metal_graph_parts(),
1295                    ) else {
1296                        if block_diag {
1297                            eprintln!(
1298                                "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1299                            );
1300                        }
1301                        break;
1302                    };
1303                    MetalFfn::Dense {
1304                        gate: g,
1305                        up: u,
1306                        down: dn,
1307                    }
1308                }
1309                FfnKind::Moe(m) => {
1310                    let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1311                        if block_diag {
1312                            eprintln!(
1313                                "block-graph: L{scan} MoE outside the graph contract — run ends"
1314                            );
1315                        }
1316                        break;
1317                    };
1318                    if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1319                        model_ref.get_or_insert_with(|| model.clone());
1320                    }
1321                    MetalFfn::Moe(moe)
1322                }
1323                _ => {
1324                    if block_diag {
1325                        eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1326                    }
1327                    break;
1328                }
1329            };
1330            match &lw.attn {
1331                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1332                    let parts = (
1333                        w.in_proj_qkv.metal_graph_parts(),
1334                        w.in_proj_z.metal_graph_parts(),
1335                        w.in_proj_a.f32_parts(),
1336                        w.in_proj_b.f32_parts(),
1337                        w.out_proj.metal_graph_parts(),
1338                    );
1339                    let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1340                        if block_diag {
1341                            eprintln!(
1342                                "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1343                                w.in_proj_qkv.metal_graph_parts().is_some(),
1344                                w.in_proj_z.metal_graph_parts().is_some(),
1345                                w.in_proj_a.f32_parts().is_some(),
1346                                w.in_proj_b.f32_parts().is_some(),
1347                                w.out_proj.metal_graph_parts().is_some(),
1348                            );
1349                        }
1350                        break;
1351                    };
1352                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1353                        model_ref.get_or_insert_with(|| model.clone());
1354                    }
1355                    let gl = GdnGpuLayer {
1356                        attn_norm: &lw.input_norm,
1357                        post_norm: &lw.post_norm,
1358                        qkv,
1359                        z,
1360                        a,
1361                        b,
1362                        out,
1363                        ffn,
1364                        conv1d: &w.conv1d,
1365                        a_log: &w.a_log,
1366                        dt_bias: &w.dt_bias,
1367                        gnorm: &w.norm,
1368                    };
1369                    match plan.last_mut() {
1370                        Some(Item::Gdn { run, .. }) => run.push(gl),
1371                        _ => plan.push(Item::Gdn {
1372                            run: vec![gl],
1373                            first: scan,
1374                        }),
1375                    }
1376                }
1377                AttnKind::Full {
1378                    wq,
1379                    wk,
1380                    wv,
1381                    wo,
1382                    q_norm,
1383                    k_norm,
1384                    output_gate,
1385                    softplus_gate: None,
1386                    bias,
1387                } if !self.kv_cache.layers[scan].o1_sealed()
1388                    // Sealed o1 stays plannable when the Metal o1 port
1389                    // is on: full_gpu attends through the device state,
1390                    // and any refusal falls to the sandwich, whose CPU
1391                    // core routes sealed layers through the nystrom step.
1392                    || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1393                {
1394                    let parts = (
1395                        wq.metal_graph_parts(),
1396                        wk.metal_graph_parts(),
1397                        wv.metal_graph_parts(),
1398                        wo.metal_graph_parts(),
1399                    );
1400                    let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1401                        break;
1402                    };
1403                    if let QTensor::Mapped { model, .. } = wq {
1404                        model_ref.get_or_insert_with(|| model.clone());
1405                    }
1406                    let cache = &self.kv_cache.layers[scan];
1407                    // O(1) layer on Metal: the device attends through the
1408                    // sealed Nystrom state (opt-in while the port proves
1409                    // itself). Unsealed -> sandwich path = the CPU o1 step.
1410                    let o1_metal = cache.o1.is_some()
1411                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1412                        && cache.o1_views().is_some();
1413                    let full_gpu = attend_contract
1414                        && cache.mode == crate::kv_cache::KvMode::F32
1415                        && (cache.o1.is_none() || o1_metal)
1416                        && bias.is_none()
1417                        && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1418                        && pk.1 == self.num_kv_heads * self.head_dim
1419                        && pv.1 == self.num_kv_heads * self.head_dim
1420                        && po.2 == self.num_heads * self.head_dim;
1421                    plan.push(Item::Attn {
1422                        l: AttnGpuLayer {
1423                            attn_norm: &lw.input_norm,
1424                            post_norm: &lw.post_norm,
1425                            wq: pq,
1426                            wk: pk,
1427                            wv: pv,
1428                            wo: po,
1429                            ffn,
1430                        },
1431                        li: scan,
1432                        q_norm: q_norm.as_deref(),
1433                        k_norm: k_norm.as_deref(),
1434                        output_gate: *output_gate,
1435                        bias: bias
1436                            .as_ref()
1437                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1438                        full_gpu,
1439                    });
1440                }
1441                _ => break,
1442            }
1443            scan += 1;
1444        }
1445        let Some(model) = model_ref else {
1446            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1447                eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1448            }
1449            if self.graph_head_required {
1450                self.fail_metal_graph("native graph has no mapped model reference");
1451            }
1452            return start;
1453        };
1454        if plan.is_empty() {
1455            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1456                eprintln!("q1-graph: empty plan at layer {start}");
1457            }
1458            if self.graph_head_required {
1459                self.fail_metal_graph("native graph plan is empty");
1460            }
1461            return start;
1462        }
1463        let has_moe = plan.iter().any(|it| match it {
1464            Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1465            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1466        });
1467        let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1468        let dev_attend = attend_contract
1469            && (self.head_dim <= 128
1470                || has_moe
1471                // A GDN hybrid attends on a quarter of its layers: the
1472                // hd>128 caution was measured on pure-dense models where
1473                // gqa_attend dominates, and on Qwen3.8-27B (hd 256, 48
1474                // GDN + 16 attn) the sandwich costs 2x the whole decode
1475                // (1.2 vs 2.21 tok/s measured before the arena fix).
1476                || (self.head_dim <= 256 && has_gdn)
1477                || attend_mode == "force"
1478                || attend_mode == "256");
1479        if !dev_attend {
1480            for it in &mut plan {
1481                if let Item::Attn { li, full_gpu, .. } = it {
1482                    // The hd>128 policy is about gqa_attend; an o1 layer
1483                    // attends through its own kernel set.
1484                    let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1485                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1486                    if !keep_o1 {
1487                        *full_gpu = false;
1488                    }
1489                }
1490            }
1491        }
1492        if std::env::var("CMF_GRAPH_DBG").is_ok() {
1493            use std::sync::atomic::{AtomicBool, Ordering};
1494            static SAID: AtomicBool = AtomicBool::new(false);
1495            if !SAID.swap(true, Ordering::Relaxed) {
1496                let fg = plan
1497                    .iter()
1498                    .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1499                    .count();
1500                let att = plan
1501                    .iter()
1502                    .filter(|it| matches!(it, Item::Attn { .. }))
1503                    .count();
1504                eprintln!(
1505                    "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1506                    plan.len(),
1507                    self.head_dim,
1508                    self.rotary_dim,
1509                    self.num_kv_heads,
1510                    self.num_heads,
1511                );
1512            }
1513        }
1514        let dims = GraphDims {
1515            hidden: self.hidden_size,
1516            eps: self.rms_eps as f32,
1517            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1518        };
1519        let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1520            if self.graph_head_required {
1521                self.fail_metal_graph("native TokenGraph allocation refused");
1522            }
1523            return start;
1524        };
1525        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1526            nv: cfg.num_v_heads,
1527            nk: cfg.num_k_heads,
1528            dk: cfg.key_head_dim,
1529            dv: cfg.value_head_dim,
1530            kk: cfg.conv_kernel,
1531            hidden: self.hidden_size,
1532            inter: self.intermediate_size,
1533            c_dim: cfg.conv_dim(),
1534            eps: cfg.rms_eps as f32,
1535            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1536        });
1537        // Validate the whole plan BEFORE encoding anything: after the
1538        // first sync a refused layer would leave the token
1539        // half-executed, so truncate to the provably encodable prefix.
1540        let mut valid = 0usize;
1541        let mut end = start;
1542        crate::gpu::stageprof(1, _mt0.elapsed()); // конец планирования
1543        if std::env::var("CMF_PLAN_DUMP").is_ok() {
1544            static ONCE: std::sync::Once = std::sync::Once::new();
1545            ONCE.call_once(|| {
1546                for it in &plan {
1547                    match it {
1548                        Item::Gdn { first, run } => {
1549                            eprintln!("plan: Gdn first={first} len={}", run.len())
1550                        }
1551                        Item::Attn { li, full_gpu, .. } => {
1552                            eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1553                        }
1554                    }
1555                }
1556            });
1557        }
1558        for item in &plan {
1559            let ok = match item {
1560                Item::Gdn { run, .. } => gcfg
1561                    .as_ref()
1562                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1563                    .unwrap_or(false),
1564                Item::Attn { l, .. } => graph.attn_ok(l),
1565            };
1566            if !ok {
1567                if block_diag {
1568                    eprintln!(
1569                        "block-graph: plan item {} ({}) failed graph preflight",
1570                        valid,
1571                        match item {
1572                            Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1573                            Item::Attn { li, .. } => format!("Attn L{li}"),
1574                        }
1575                    );
1576                }
1577                break;
1578            }
1579            valid += 1;
1580            end += match item {
1581                Item::Gdn { run, .. } => run.len(),
1582                Item::Attn { .. } => 1,
1583            };
1584        }
1585        plan.truncate(valid);
1586        if plan.is_empty() {
1587            if self.graph_head_required {
1588                self.fail_metal_graph("native graph preflight produced no valid items");
1589            }
1590            return start;
1591        }
1592
1593        if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
1594            self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
1595            return start;
1596        }
1597
1598        let inv_freq = self.inv_freq.clone();
1599        let pool = self.pool.clone();
1600        let (nh, nkv, hd, hs, rd, eps) = (
1601            self.num_heads,
1602            self.num_kv_heads,
1603            self.head_dim,
1604            self.hidden_size,
1605            self.rotary_dim,
1606            self.rms_eps,
1607        );
1608        let norm_style = self.norm_style;
1609        let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1610        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1611        let kv_id = self.graph_kv_id;
1612        // GDN runs whose states await readback after the next sync
1613        // (device-attended layers add no sync, so several may stack).
1614        let mut pending: Vec<(usize, usize)> = Vec::new();
1615        // Device-attended layers: their K/V/imp are pulled from the
1616        // mirror after the final sync.
1617        let mut dev_attn: Vec<usize> = Vec::new();
1618        for item in &plan {
1619            let _xt0 = std::time::Instant::now();
1620            let _xkind: u32 = match item {
1621                Item::Gdn { .. } => 2,
1622                Item::Attn { .. } => 3,
1623            };
1624            // Looped Transformer: insert on-device norm at loop boundaries.
1625            if self.loop_final_norm {
1626                let item_start = match item {
1627                    Item::Gdn { first, .. } => *first,
1628                    Item::Attn { li, .. } => *li,
1629                };
1630                if item_start > start && self.is_loop_end(item_start - 1) {
1631                    graph.encode_loop_norm(&self.weights.final_norm);
1632                }
1633            }
1634            match item {
1635                Item::Gdn { run, first } => {
1636                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1637                        if l.linear_state.len() != want {
1638                            l.linear_state = vec![0f32; want];
1639                        }
1640                    }
1641                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1642                        .iter()
1643                        .map(|l| l.linear_state.as_slice())
1644                        .collect();
1645                    let _ig = std::time::Instant::now();
1646                    if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1647                        // Unreachable: the plan was validated above.
1648                        tracing::error!("q1 graph: GDN run refused after validation");
1649                        return start;
1650                    }
1651                    // Early commit: the GPU starts the run while the
1652                    // CPU encodes the next layer (nothing to wait on).
1653                    graph.commit_kind = 2;
1654                    graph.commit();
1655                    crate::gpu::stageprof(0, _ig.elapsed());
1656                    pending.push((*first, run.len()));
1657                }
1658                Item::Attn {
1659                    l,
1660                    li,
1661                    q_norm,
1662                    k_norm,
1663                    output_gate,
1664                    bias,
1665                    full_gpu,
1666                } => {
1667                    let _ia = std::time::Instant::now();
1668                    // ── Fully device-resident attention: no sync at all.
1669                    if *full_gpu {
1670                        let cache = &self.kv_cache.layers[*li];
1671                        let o1p = if cache.o1.is_some() {
1672                            match cache.o1_views() {
1673                                Some(views) => Some(crate::gpu::O1AttnParams {
1674                                    views,
1675                                    epoch: self.o1_epoch,
1676                                }),
1677                                // Sealed state gone mid-run: sandwich.
1678                                None => None,
1679                            }
1680                        } else {
1681                            None
1682                        };
1683                        let o1_layer = cache.o1.is_some();
1684                        if o1_layer && o1p.is_none() {
1685                            // fall to the sandwich (CPU o1 step)
1686                        }
1687                        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1688                        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1689                        let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1690                        let p = crate::gpu::AttnDeviceParams {
1691                            kv_id,
1692                            layer: *li,
1693                            nh,
1694                            nkv,
1695                            hd,
1696                            rd,
1697                            position,
1698                            scale: self.attn_scale,
1699                            eps: eps as f32,
1700                            gemma,
1701                            late_qk_norm: self.qk_norm_after_rope,
1702                            output_gate: *output_gate,
1703                            q_norm: *q_norm,
1704                            k_norm: *k_norm,
1705                            inv_freq: &inv_freq,
1706                            cpu_k,
1707                            cpu_v,
1708                            cpu_stored,
1709                            o1: o1p,
1710                        };
1711                        let o1_bad = o1_layer && p.o1.is_none();
1712                        if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1713                        {
1714                            // o1 layers leave no mirror row to pull.
1715                            if p.o1.is_none() {
1716                                dev_attn.push(*li);
1717                            }
1718                            graph.commit_kind = 3;
1719                            graph.commit();
1720                            // The footer below is skipped by `continue`:
1721                            // account the device-attn item here or its
1722                            // cost hides from the stage profile entirely.
1723                            crate::gpu::stageprof(_xkind, _xt0.elapsed());
1724                            continue;
1725                        }
1726                        // Mirror refused (nothing encoded) → sandwich.
1727                    }
1728                    graph.encode_attn_prefix(l);
1729                    if let Err(err) = graph.sync_checked() {
1730                        self.fail_metal_graph(&err);
1731                        return start;
1732                    }
1733                    if !pending.is_empty() {
1734                        let idxs: Vec<usize> =
1735                            pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1736                        let mut outs: Vec<&mut [f32]> = self
1737                            .kv_cache
1738                            .layers
1739                            .iter_mut()
1740                            .enumerate()
1741                            .filter(|(i, _)| idxs.binary_search(i).is_ok())
1742                            .map(|(_, s)| s.linear_state.as_mut_slice())
1743                            .collect();
1744                        graph.read_states(&mut outs);
1745                    }
1746                    let mut q_raw = attention::take_buf(l.wq.1);
1747                    let mut k = attention::take_buf(l.wk.1);
1748                    let mut v = attention::take_buf(l.wv.1);
1749                    graph.read_qkv(&mut q_raw, &mut k, &mut v);
1750                    let cfg = QwenAttnCfg {
1751                        num_heads: nh,
1752                        num_kv_heads: nkv,
1753                        head_dim: hd,
1754                        hidden_size: hs,
1755                        position,
1756                        inv_freq: &inv_freq,
1757                        rotary_dim: rd,
1758                        scale: self.attn_scale,
1759                        softcap: self.attn_softcap,
1760                        window: None,
1761                        v_norm: false,
1762                        qk_norm_after_rope: self.qk_norm_after_rope,
1763                        q_norm: *q_norm,
1764                        k_norm: *k_norm,
1765                        output_gate: *output_gate,
1766                        softplus_gate: None,
1767                        rope_scale: 1.0,
1768                        bias: *bias,
1769                        rms_eps: eps,
1770                        norm_style,
1771                        pool: pool.as_deref(),
1772                    };
1773                    // CMF_ATTN_ORACLE=1: diff the device attend against
1774                    // this CPU attend on identical inputs (bring-up).
1775                    let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1776                        || std::env::var("CMF_ATTN_DUMP").is_ok();
1777                    let _ = full_gpu;
1778                    let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1779                    let mut ao = attention::qwen_attention_core(
1780                        q_raw,
1781                        k,
1782                        v,
1783                        &mut self.kv_cache.layers[*li],
1784                        &cfg,
1785                    );
1786                    // CMF_ATTN_DUMP=<dir>: this token's rope'd Q and the layer's whole
1787                    // K/V cache as raw f32 (offline attention-statistics probes:
1788                    // block bounds, mass concentration). Needs CMF_GPU_ATTEND=0.
1789                    if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1790                        if let Some((qr0, k0, v0)) = oracle_in.clone() {
1791                            let (cq, _cg, _ck, _cv) =
1792                                attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1793                            let cache = &self.kv_cache.layers[*li];
1794                            let n = cache.head_keys(0).len() / hd;
1795                            let mut bytes: Vec<u8> = Vec::new();
1796                            for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1797                                bytes.extend_from_slice(&v.to_le_bytes());
1798                            }
1799                            for v in &cq {
1800                                bytes.extend_from_slice(&v.to_le_bytes());
1801                            }
1802                            for g in 0..nkv {
1803                                for v in cache.head_keys(g) {
1804                                    bytes.extend_from_slice(&v.to_le_bytes());
1805                                }
1806                            }
1807                            for g in 0..nkv {
1808                                for v in cache.head_values(g) {
1809                                    bytes.extend_from_slice(&v.to_le_bytes());
1810                                }
1811                            }
1812                            let _ =
1813                                std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1814                        }
1815                    }
1816                    if let Some((qr0, k0, v0)) =
1817                        oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
1818                    {
1819                        let (cq, _cg, ck, cv) =
1820                            attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1821                        let mut h_now = vec![0f32; hs];
1822                        graph.read_h(&mut h_now);
1823                        let cache = &self.kv_cache.layers[*li];
1824                        let n_after = cache.head_keys(0).len() / hd;
1825                        // A sealed O(1) cache may have no dense current-row
1826                        // entry. The oracle is a debug probe, so let it see
1827                        // zero stored exact rows instead of underflowing.
1828                        let stored = n_after.saturating_sub(1);
1829                        let cpu_k: Vec<&[f32]> = (0..nkv)
1830                            .map(|g| &cache.head_keys(g)[..stored * hd])
1831                            .collect();
1832                        let cpu_v: Vec<&[f32]> = (0..nkv)
1833                            .map(|g| &cache.head_values(g)[..stored * hd])
1834                            .collect();
1835                        let p = crate::gpu::AttnDeviceParams {
1836                            kv_id,
1837                            layer: *li,
1838                            nh,
1839                            nkv,
1840                            hd,
1841                            rd,
1842                            position,
1843                            scale: self.attn_scale,
1844                            eps: eps as f32,
1845                            gemma,
1846                            late_qk_norm: self.qk_norm_after_rope,
1847                            output_gate: *output_gate,
1848                            q_norm: *q_norm,
1849                            k_norm: *k_norm,
1850                            inv_freq: &inv_freq,
1851                            cpu_k,
1852                            cpu_v,
1853                            cpu_stored: stored,
1854                            o1: None,
1855                        };
1856                        if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1857                            let md = |a: &[f32], b: &[f32]| {
1858                                a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
1859                            };
1860                            let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1861                            eprintln!(
1862                                "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}",
1863                                nn(&cq),
1864                                md(&cq, &dq),
1865                                nn(&ck),
1866                                md(&ck, &dk),
1867                                nn(&cv),
1868                                md(&cv, &dv),
1869                                nn(&ao),
1870                                md(&ao, &dao)
1871                            );
1872                        } else {
1873                            eprintln!("attn-oracle L{li}: device probe declined");
1874                        }
1875                    }
1876                    graph.encode_attn_suffix(l, &ao);
1877                    // Early commit: the GPU starts O+FFN while the CPU
1878                    // encodes the following GDN run / attention prefix.
1879                    graph.commit();
1880                    attention::recycle_buf(&mut ao);
1881                }
1882            }
1883
1884            crate::gpu::stageprof(_xkind, _xt0.elapsed());
1885        }
1886        // Ride the final norm + lm_head in the same command buffer when
1887        // this run reaches the model's end and the caller wants logits:
1888        // the separate per-op lm_head submit (a full round trip) folds
1889        // into the sync that already happens here.
1890        let mut lm_rows = None;
1891        if self.graph_want_logits
1892            && upto.is_none()
1893            && end == self.num_layers
1894            && std::env::var("CMF_GPU_LMHEAD")
1895                .map(|v| v != "0")
1896                .unwrap_or(true)
1897        {
1898            if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
1899                if graph.lm_head_ok(lm) {
1900                    graph.encode_lm_head(&self.weights.final_norm, lm);
1901                    lm_rows = Some(lm.1);
1902                }
1903            }
1904        }
1905        if self.graph_head_required && lm_rows.is_none() {
1906            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1907            self.fail_metal_graph("fused graph head was requested but not encodable");
1908            return start;
1909        }
1910        let _sy0 = std::time::Instant::now();
1911        if let Err(err) = graph.sync_checked() {
1912            self.fail_metal_graph(&err);
1913            return start;
1914        }
1915        let _rs0 = std::time::Instant::now();
1916        if !pending.is_empty() {
1917            let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1918            let mut outs: Vec<&mut [f32]> = self
1919                .kv_cache
1920                .layers
1921                .iter_mut()
1922                .enumerate()
1923                .filter(|(i, _)| idxs.binary_search(i).is_ok())
1924                .map(|(_, s)| s.linear_state.as_mut_slice())
1925                .collect();
1926            graph.read_states(&mut outs);
1927        }
1928        if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1929            use std::sync::atomic::{AtomicU64, Ordering};
1930            static SY: AtomicU64 = AtomicU64::new(0);
1931            static RS: AtomicU64 = AtomicU64::new(0);
1932            static N: AtomicU64 = AtomicU64::new(0);
1933            SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1934            RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1935            let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1936            if n % 100 == 0 {
1937                eprintln!(
1938                    "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1939                    SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1940                    RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1941                );
1942            }
1943        }
1944        if let Some(rows) = lm_rows {
1945            crate::gpu::hostprof_encode_done(_mt0);
1946            let mut lg = attention::take_buf(rows.min(self.vocab_size));
1947            graph.read_logits(&mut lg);
1948            crate::gpu::hostprof_total(_mt0);
1949            lg.resize(self.vocab_size, 0.0);
1950            if let Some(c) = self.final_softcap {
1951                for l in lg.iter_mut() {
1952                    *l = c * (*l / c).tanh();
1953                }
1954            }
1955            self.graph_logits = Some(lg);
1956        }
1957        graph.read_h(h);
1958        if self.graph_head_required && self.graph_logits.is_none() {
1959            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1960            self.fail_metal_graph("fused graph head completed without logits readback");
1961            return start;
1962        }
1963        METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1964        METAL_GRAPH_LAYERS.fetch_add(
1965            end.saturating_sub(start) as u64,
1966            std::sync::atomic::Ordering::Relaxed,
1967        );
1968        if self.graph_head_required {
1969            METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1970        }
1971        // Device-attended layers: replay the CPU bookkeeping — append
1972        // the mirror's new K/V row (rope'd on the GPU) into the owner
1973        // cache, then bank this token's attention-importance mass.
1974        for li in dev_attn {
1975            let mut krow = attention::take_buf(nkv * hd);
1976            let mut vrow = attention::take_buf(nkv * hd);
1977            if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1978                let cache = &mut self.kv_cache.layers[li];
1979                cache.append(&krow, &vrow, &[]);
1980                let n = cache.seq_len;
1981                let mut imp = attention::take_buf(n);
1982                crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1983                cache.accumulate_imp(&imp);
1984                attention::recycle_buf(&mut imp);
1985            }
1986            attention::recycle_buf(&mut krow);
1987            attention::recycle_buf(&mut vrow);
1988        }
1989        end
1990    }
1991
1992    pub fn new(
1993        tokenizer: Tokenizer,
1994        weights: PipelineWeights,
1995        hidden_size: usize,
1996        intermediate_size: usize,
1997        num_heads: usize,
1998        num_kv_heads: usize,
1999        head_dim: usize,
2000        num_layers: usize,
2001        physical_layers: usize,
2002        loop_final_norm: bool,
2003        vocab_size: usize,
2004        rms_eps: f64,
2005        rope_base: f32,
2006        norm_style: NormStyle,
2007        max_seq_len: usize,
2008        sampler_config: SamplerConfig,
2009    ) -> Self {
2010        let rng = match sampler_config.seed {
2011            Some(s) => SplitMix64::new(s),
2012            None => SplitMix64::from_entropy(),
2013        };
2014        let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
2015        let pool = Pool::from_env();
2016        if let Some(p) = &pool {
2017            tracing::info!("worker pool: {} threads", p.n_workers());
2018        }
2019        Self {
2020            gpu_plan: None,
2021            tokenizer: std::sync::Arc::new(tokenizer),
2022            kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
2023            sampler_config,
2024            weights,
2025            hidden_size,
2026            intermediate_size,
2027            num_heads,
2028            num_kv_heads,
2029            head_dim,
2030            num_layers,
2031            physical_layers,
2032            loop_final_norm,
2033            vocab_size,
2034            rms_eps,
2035            rope_base,
2036            norm_style,
2037            rotary_dim: head_dim,
2038            attention_heads_per_layer: None,
2039            vmf_cfg: None,
2040            gdn_cfg: None,
2041            kda_cfg: None,
2042            g3n: None,
2043            dsv4: None,
2044            dsv41: None,
2045            dsv41_vision: None,
2046            dsv41_prefill: None,
2047            qwen4_exp: None,
2048            dsv4_mtp: Vec::new(),
2049            dspark: None,
2050            dspark_pending: Vec::new(),
2051            dspark_hist: Vec::new(),
2052            dspark_real: Vec::new(),
2053            dspark_trunk_picks: Vec::new(),
2054            dspark_exp: Vec::new(),
2055            dspark_draft_ns: 0,
2056            logit_multiplier: None,
2057            cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2058            graph_failed: std::sync::atomic::AtomicBool::new(false),
2059            kv_history: Vec::new(),
2060            short_conv_cfg: None,
2061            mtp: None,
2062            speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
2063            rng,
2064            sampler_scratch: SamplerScratch::default(),
2065            spec_forced: None,
2066            spec_q: Vec::new(),
2067            spec_p: Vec::new(),
2068            spec_res: Vec::new(),
2069            spec_qs: Vec::new(),
2070            spec_ps: Vec::new(),
2071            spec_ress: Vec::new(),
2072            mtp_graph_mode: None,
2073            #[cfg(target_os = "macos")]
2074            metal_verify: None,
2075            inv_freq,
2076            ws: ForwardScratch::new(hidden_size),
2077            pool,
2078            model: None,
2079            dyn_force_f32: false,
2080            dyn_skill_layers: Vec::new(),
2081            dyn_active: None,
2082            dyn_blend_loaded: false,
2083            dyn_phi_layer: None,
2084            dyn_phi_ema: Vec::new(),
2085            dyn_phi_seen: 0,
2086            dyn_router: None,
2087            o1_cfg: None,
2088            o1_epoch: 0,
2089            o1_flags: Vec::new(),
2090            trace: false,
2091            calib_temp: 1.0,
2092            confidence_on: true,
2093            embed_multiplier: 1.0,
2094            attn_scale: 1.0 / (head_dim as f32).sqrt(),
2095            swa: None,
2096            sliding_layers: None,
2097            inv_freq_local: None,
2098            rotary_dim_local: None,
2099            rope_scale: 1.0,
2100            rope_scale_local: 1.0,
2101            global_attn: None,
2102            inv_freq_global: None,
2103            attn_v_norm: false,
2104            qk_norm_after_rope: false,
2105            final_softcap: None,
2106            head_clusters: None,
2107            attn_softcap: 0.0,
2108            graph_want_logits: false,
2109            graph_head_required: false,
2110            graph_logits: None,
2111            graph_kv_id: {
2112                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2113                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2114            },
2115            #[cfg(test)]
2116            nll_test_fail_at: None,
2117            #[cfg(test)]
2118            nll_test_force_serial: false,
2119        }
2120    }
2121
2122    /// Enable/disable per-layer O(1) Nyström attention. Only Full
2123    /// layers are eligible (a linear layer keeps its own operator).
2124    /// Applies to generation (`generate*`/`forward_ids`): the prompt
2125    /// pass stays exact, then the state seals after prefill or at the
2126    /// deferred skeleton-safe boundary for short prompts; decode runs on
2127    /// the O(1) state. Teacher-forced scoring (`ppl_ids`) intentionally
2128    /// stays exact.
2129    pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
2130        if let Some(c) = &cfg {
2131            if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
2132                tracing::error!(
2133                    "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
2134                    c.w,
2135                    c.sink
2136                );
2137                self.o1_flags.clear();
2138                self.o1_cfg = None;
2139                return;
2140            }
2141        }
2142        self.o1_flags = match &cfg {
2143            Some(c) => {
2144                let mut flags = c.layer_flags(self.num_layers);
2145                for (li, f) in flags.iter_mut().enumerate() {
2146                    if *f
2147                        && !matches!(
2148                            self.weights.layers[self.phys_layer(li)].attn,
2149                            AttnKind::Full { .. }
2150                        )
2151                    {
2152                        *f = false;
2153                    }
2154                }
2155                flags
2156            }
2157            None => Vec::new(),
2158        };
2159        if let Some(c) = &cfg {
2160            let n = self.o1_flags.iter().filter(|&&f| f).count();
2161            tracing::info!(
2162                "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2163                self.num_layers,
2164                c.m,
2165                c.w,
2166                c.sink,
2167                c.rect
2168            );
2169        }
2170        self.o1_cfg = cfg;
2171    }
2172
2173    /// True when at least one layer runs the O(1) kernel.
2174    pub fn o1_active(&self) -> bool {
2175        self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2176    }
2177
2178    /// Whether generation's prompt ingest is routed through the whole-token
2179    /// graph.  The bench uses this to label the measured generation prefill
2180    /// honestly; keep the predicate in Pipeline so CLI labels cannot drift
2181    /// from the production route.
2182    pub fn generation_graph_prefill(&self) -> bool {
2183        let graph = self.graph_prefill_preferred();
2184        // On wgpu, an active MTP head now consumes the trunk's graph batches
2185        // and warms its own block from those returned rows.  The selected
2186        // generation measurement is therefore the batched path, even though
2187        // the underlying GDN model still satisfies the graph-prefill
2188        // predicate.  Keep the CLI label tied to the actual route.  Native
2189        // Metal has a separate prefill-batch arm and retains its historical
2190        // label here.
2191        #[cfg(not(target_os = "macos"))]
2192        if graph
2193            && self.mtp.is_some()
2194            && std::env::var("CMF_BATCH_K")
2195                .ok()
2196                .and_then(|v| v.parse::<usize>().ok())
2197                .is_some_and(|k| k > 0)
2198            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2199        {
2200            return false;
2201        }
2202        graph
2203    }
2204
2205    /// Device-side O(1) mirrors currently uploaded for this pipeline's
2206    /// sequence.  The count/bytes are zero before seal or after a fresh
2207    /// reset; callers use this to distinguish logical host state from the
2208    /// GPU allocation that actually serves decode.
2209    pub fn o1_device_stats(&self) -> (usize, u64) {
2210        crate::gpu::o1_device_stats(self.graph_kv_id)
2211    }
2212
2213    /// Arm query collection on the o1 layers (fresh prompt pass).
2214    /// Reset the o1 layers to Collecting for a fresh sequence. Pub for the
2215    /// network split: each side runs the o1 lifecycle over ITS OWN layers
2216    /// (begin before prefill, seal at the prefill barrier).
2217    pub fn o1_begin(&mut self) {
2218        self.o1_begin_with_prefix(None);
2219    }
2220
2221    /// Arm collection and optionally request a positive calibration prefix.
2222    /// The effective barrier is always at least the skeleton-safe floor, so
2223    /// a short requested prefix cannot create an exact-only runtime state.
2224    pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2225        if let Some(c) = &self.o1_cfg {
2226            let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2227            let boundary = requested_prefix.map(|p| {
2228                p.max(
2229                    crate::nystrom::o1_deferred_boundary(w, sink)
2230                        .expect("o1 config boundary validated in set_o1"),
2231                )
2232            });
2233            for (li, &f) in self.o1_flags.iter().enumerate() {
2234                if f {
2235                    self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2236                }
2237            }
2238        }
2239    }
2240
2241    /// Effective deferred boundary for a positive prefix request.
2242    fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2243        self.o1_cfg.as_ref().and_then(|c| {
2244            crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2245                .map(|floor| requested_prefix.max(floor))
2246        })
2247    }
2248
2249    fn o1_note_transition(&mut self) {
2250        // Drain every layer's one-shot bit before publishing one pipeline
2251        // epoch. `any()` would short-circuit on the first layer and leak the
2252        // remaining bits into later forwards, causing one epoch per layer.
2253        let mut transitioned = false;
2254        for (li, &flagged) in self.o1_flags.iter().enumerate() {
2255            if flagged {
2256                transitioned |= self.kv_cache.layers[li].take_o1_transition();
2257            }
2258        }
2259        if transitioned {
2260            self.o1_epoch = self.o1_epoch.wrapping_add(1);
2261        }
2262    }
2263
2264    fn o1_pending(&self) -> bool {
2265        self.o1_flags.iter().enumerate().any(|(li, &f)| {
2266            f && self.kv_cache.layers[li].seq_len > 0
2267                && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2268        })
2269    }
2270
2271    fn o1_fail(&mut self, err: String) {
2272        tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2273        self.clear_sequence_state();
2274        self.graph_failed
2275            .store(true, std::sync::atomic::Ordering::Relaxed);
2276        self.cancel
2277            .store(true, std::sync::atomic::Ordering::Relaxed);
2278    }
2279
2280    /// Seal participating layers while retaining the exact state when the
2281    /// prompt is below the deferred boundary. A split worker may have
2282    /// collecting layers outside its owned span; zero-depth layers remain
2283    /// armed and are intentionally skipped until their peer runs them.
2284    pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2285        if self.o1_cfg.is_none() {
2286            return Ok(false);
2287        }
2288        let mut participating = false;
2289        for li in 0..self.num_layers {
2290            if !self.o1_flags.get(li).copied().unwrap_or(false) {
2291                continue;
2292            }
2293            if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2294                return Err(err);
2295            }
2296            if self.kv_cache.layers[li].seq_len == 0 {
2297                continue;
2298            }
2299            participating = true;
2300            let num_heads = self.layer_num_heads(li);
2301            self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2302        }
2303        self.o1_note_transition();
2304        for li in 0..self.num_layers {
2305            if self.o1_flags.get(li).copied().unwrap_or(false) {
2306                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2307                    return Err(err);
2308                }
2309            }
2310        }
2311        Ok(participating
2312            && (0..self.num_layers).all(|li| {
2313                !self.o1_flags.get(li).copied().unwrap_or(false)
2314                    || self.kv_cache.layers[li].seq_len == 0
2315                    || self.kv_cache.layers[li].o1_sealed()
2316            }))
2317    }
2318
2319    /// Complete a deferred boundary after a full position/span forward.
2320    /// This is the pipeline owner for epoch publication and failure cleanup.
2321    fn o1_progress(&mut self) {
2322        if !self.o1_active() {
2323            return;
2324        }
2325        for li in 0..self.num_layers {
2326            if self.o1_flags.get(li).copied().unwrap_or(false) {
2327                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2328                    self.o1_fail(err);
2329                    return;
2330                }
2331            }
2332        }
2333        // A qwen_attention row can seal in the middle of a complete layer
2334        // walk. Consume its transition even though the pending boundary has
2335        // already disappeared from the cache.
2336        self.o1_note_transition();
2337        if !self.o1_pending() {
2338            return;
2339        }
2340        if let Err(err) = self.o1_seal_checked() {
2341            self.o1_fail(err);
2342        }
2343    }
2344
2345    /// Turn a deferred O(1) failure raised by a hidden-only forward into the
2346    /// Result error its public batch/span caller must return. The failure
2347    /// path already cleared host/device sequence state; consume only the
2348    /// side-channel marker here and leave the pipeline reusable.
2349    fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
2350        if self
2351            .graph_failed
2352            .swap(false, std::sync::atomic::Ordering::Relaxed)
2353        {
2354            self.cancel
2355                .store(false, std::sync::atomic::Ordering::Relaxed);
2356            self.clear_sequence_state();
2357            return Err(format!("{phase}: deferred O(1) transition failed"));
2358        }
2359        Ok(())
2360    }
2361
2362    /// Freeze landmarks + skeleton state after the prompt pass and drop
2363    /// the o1 layers' full KV; decode then runs `step()` per token.
2364    /// Pub for the network split (see `o1_begin`).
2365    pub fn o1_seal(&mut self) {
2366        if let Err(err) = self.o1_seal_checked() {
2367            self.o1_fail(err);
2368        }
2369    }
2370
2371    /// Enable/disable the structured per-token telemetry trace (B4).
2372    pub fn set_trace(&mut self, on: bool) {
2373        self.trace = on;
2374    }
2375
2376    /// Replace all request-scoped sampler options and reset the random stream.
2377    /// This is required for deterministic `seed` semantics in pooled servers.
2378    pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2379        self.rng = match config.seed {
2380            Some(seed) => SplitMix64::new(seed),
2381            None => SplitMix64::from_entropy(),
2382        };
2383        self.sampler_config = config;
2384    }
2385
2386    /// Toggle the per-token confidence reduction (a full-vocab
2387    /// softmax each token). `bench --core` turns it off so the timed
2388    /// loop matches llama-bench's core contract; the result's
2389    /// `confidence` vec is empty while off.
2390    pub fn set_confidence(&mut self, on: bool) {
2391        self.confidence_on = on;
2392    }
2393
2394    /// Set the confidence-calibration temperature (B1). Values ≤0 are
2395    /// clamped to raw (1.0).
2396    pub fn set_calib_temp(&mut self, t: f32) {
2397        self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2398    }
2399
2400    /// The active calibration temperature (1.0 = raw probability).
2401    pub fn calib_temp(&self) -> f32 {
2402        self.calib_temp
2403    }
2404
2405    /// Partial rotary (Qwen3.5): rotate only the first `rotary_dim` dims;
2406    /// the frequency table is rebuilt over the rotary dims.
2407    pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2408        self.rotary_dim = rotary_dim.min(self.head_dim);
2409        self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2410    }
2411
2412    fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2413        QwenAttnCfg {
2414            num_heads: self.num_heads,
2415            num_kv_heads: self.num_kv_heads,
2416            head_dim: self.head_dim,
2417            hidden_size: self.hidden_size,
2418            position,
2419            inv_freq: &self.inv_freq,
2420            rotary_dim: self.rotary_dim,
2421            scale: self.attn_scale,
2422            softcap: self.attn_softcap,
2423            window: None,
2424            v_norm: false,
2425            qk_norm_after_rope: self.qk_norm_after_rope,
2426            q_norm: None,
2427            k_norm: None,
2428            output_gate: false,
2429            softplus_gate: None,
2430            rope_scale: self.rope_scale,
2431            bias: None,
2432            rms_eps: self.rms_eps,
2433            norm_style: self.norm_style,
2434            pool: self.pool.as_deref(),
2435        }
2436    }
2437
2438    /// Generate text from a plain-text prompt. Streams tokens via `on_token`.
2439    pub fn generate(
2440        &mut self,
2441        prompt: &str,
2442        max_tokens: usize,
2443        task_mask: Option<&TaskMask>,
2444        on_token: Option<TokenCallback>,
2445    ) -> Result<GenerateResult, String> {
2446        let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2447        self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2448    }
2449
2450    /// Generate from a V4.1 multimodal prompt prepared by the vision module.
2451    /// Vision rows are encoded once and fed through the same bounded token walk as text.
2452    pub fn generate_from_vl(
2453        &mut self,
2454        input: &crate::dsv41_vision::PreparedVlInputs,
2455        max_tokens: usize,
2456        task_mask: Option<&TaskMask>,
2457        on_token: Option<TokenCallback>,
2458    ) -> Result<GenerateResult, String> {
2459        let Some(dsv41) = &self.dsv41 else {
2460            return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
2461        };
2462        if input.token_ids.is_empty() {
2463            return Err("empty V4.1 multimodal prompt".into());
2464        }
2465        if input.token_types.len() != input.token_ids.len() {
2466            return Err(format!(
2467                "V4.1 token type count {} != token count {}",
2468                input.token_types.len(),
2469                input.token_ids.len()
2470            ));
2471        }
2472        let dim = dsv41.2.dim;
2473        let mut embeddings = vec![None; input.token_ids.len()];
2474        let mut participates = vec![true; input.token_ids.len()];
2475        if !input.images.is_empty() {
2476            let vision = self
2477                .dsv41_vision
2478                .as_ref()
2479                .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
2480            for image in &input.images {
2481                let end = image.start.saturating_add(image.types.len());
2482                if end > input.token_ids.len() {
2483                    return Err(format!(
2484                        "V4.1 image span {}..{} exceeds prompt length {}",
2485                        image.start,
2486                        end,
2487                        input.token_ids.len()
2488                    ));
2489                }
2490                let mut span = vec![0.0f32; image.types.len() * dim];
2491                vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
2492                for (offset, &kind) in image.types.iter().enumerate() {
2493                    let pos = image.start + offset;
2494                    if input.token_types[pos] != kind {
2495                        return Err(format!(
2496                            "V4.1 image type mismatch at position {pos}: {} != {kind}",
2497                            input.token_types[pos]
2498                        ));
2499                    }
2500                    embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
2501                    participates[pos] = false;
2502                }
2503            }
2504        }
2505        for (pos, &kind) in input.token_types.iter().enumerate() {
2506            if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
2507                return Err(format!("V4.1 text position {pos} has an image embedding"));
2508            }
2509            if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
2510                return Err(format!("V4.1 image position {pos} has no image embedding"));
2511            }
2512        }
2513        self.dsv41_prefill = Some((embeddings, participates));
2514        let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
2515        self.dsv41_prefill = None;
2516        result
2517    }
2518
2519    /// `None` when the mask forbids nothing (see `TaskMask::fully_open`).
2520    fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
2521        m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
2522    }
2523
2524    /// Generate from prepared token ids (e.g. a chat template).
2525    ///
2526    /// With an MTP head, greedy generation without a task mask takes the
2527    /// speculative path: the MTP module drafts the token after next and
2528    /// the main model verifies both in one fused two-position forward
2529    /// (weights streamed once). The output is EXACTLY the vanilla greedy
2530    /// sequence — a rejected draft is rolled back — MTP only buys speed.
2531    pub fn generate_from_ids(
2532        &mut self,
2533        input_ids: &[u32],
2534        max_tokens: usize,
2535        task_mask: Option<&TaskMask>,
2536        mut on_token: Option<TokenCallback>,
2537    ) -> Result<GenerateResult, String> {
2538        if std::env::var("CMF_TRACE_H").is_ok() {
2539            eprintln!("input_ids: {input_ids:?}");
2540        }
2541        if input_ids.is_empty() {
2542            return Err("empty prompt: nothing to generate from".to_string());
2543        }
2544        // A prior graph failure is terminal for that sequence but must not
2545        // poison the next independent request.  Keep this flag separate from
2546        // the externally-owned cooperative cancel bit.
2547        self.graph_failed
2548            .store(false, std::sync::atomic::Ordering::Relaxed);
2549        // A mask that forbids nothing still costs every fused path and
2550        // whole-token graph, all of which are gated on `is_none()`. A
2551        // narrowed file whose one segment is always on carries exactly
2552        // such a mask — drop it here rather than pay 5x for a no-op.
2553        let task_mask = self.drop_open_mask(task_mask);
2554
2555        // Cross-turn KV reuse: a chat app resends the whole history
2556        // every turn; when the new ids strictly EXTEND what the cache
2557        // already holds, prefill only the tail — turn latency stays
2558        // proportional to the new text instead of the whole session.
2559        // Extension-only (no rollback), so it is exact for every layer
2560        // kind including recurrent state; MTP/o1/task-mask runs keep
2561        // the fresh-sequence path. CMF_KV_REUSE=0 disables.
2562        let reuse_from = {
2563            let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
2564            let h = &self.kv_history;
2565            if on
2566                && task_mask.is_none()
2567                && self.mtp.is_none()
2568                && self.o1_cfg.is_none()
2569                && self.dsv41.is_none()
2570                && !h.is_empty()
2571                && h.len() < input_ids.len()
2572                && input_ids[..h.len()] == h[..]
2573            {
2574                h.len()
2575            } else {
2576                0
2577            }
2578        };
2579        if reuse_from == 0 {
2580            // Fresh sequence — the cache holds absolute positions.
2581            self.clear_sequence_state();
2582        } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
2583            eprintln!(
2584                "kv-reuse: {} of {} prompt positions already cached",
2585                reuse_from,
2586                input_ids.len()
2587            );
2588        }
2589        crate::gpu::graph_race_begin_generation();
2590        // Optional bounded calibration prefix. Keep the requested value
2591        // even when it is longer than the prompt; the collecting layer will
2592        // defer at the effective boundary and remain exact for short input.
2593        let o1_prefill = if self.o1_active() && task_mask.is_none() {
2594            std::env::var("CMF_O1_PREFILL")
2595                .ok()
2596                .and_then(|v| v.parse::<usize>().ok())
2597                .filter(|&p| p > 0)
2598        } else {
2599            None
2600        };
2601        if task_mask.is_none() {
2602            self.o1_begin_with_prefix(o1_prefill);
2603        }
2604
2605        // Speculative decode is off under o1: a rejected draft can't be
2606        // rolled back out of the far accumulators / ring window (the
2607        // Nyström insertion is irreversible by design).
2608        // The wgpu token graph owns a device K/V mirror that speculative
2609        // rollback would desync — the two are mutually exclusive.
2610        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2611        // Graph speculative decode (`CMF_GRAPH_SPEC=1`): the MTP head
2612        // drafts, ONE batched graph submit verifies the whole chain.
2613        //
2614        // It now PAYS on Qwen3.6-27B / RTX 5090 — 51.1 tok/s against a
2615        // plain 49.4 at k=3, medians of three, 89% of drafts accepted,
2616        // and the greedy continuation is byte-identical to the plain
2617        // path. That took the batch matvec sharing its nibble unpack
2618        // across the batch (`CMF_MV_BK=2`); before it, the same round
2619        // measured 43.6, an 11% LOSS, which is what the earlier note
2620        // here described.
2621        //
2622        // Still opt-in. One model's win is not a default: the verify
2623        // rides `gdn_spec_restore` and a batched frame whose numerics
2624        // are the batch kernels', and that has to be shown on more than
2625        // one architecture before every greedy decode takes it.
2626        // Greedy (with or without penalties) verifies by argmax equality.
2627        // Sampling (temperature > 0) can go through speculative SAMPLING —
2628        // draft from the MTP head's own post-chain distribution, accept
2629        // with min(1, p/q), correct from max(0, p − q); the emitted stream
2630        // is distributed exactly as the plain sampler's — but it is
2631        // OPT-IN (`CMF_GRAPH_SPEC_SAMPLE=1`): measured on Qwen3.8-27B /
2632        // RTX 5090 at the instruct row (0.7 / 0.80 / 20 / presence 1.5)
2633        // it decoded 19-22 tok/s against a plain 40 — nine post-chain
2634        // distributions a round plus a lower acceptance than greedy's,
2635        // against a verify that costs 2.7 single tokens. The greedy arms
2636        // pay +10%; the sampling arm needs a cheaper verify first.
2637        let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2638            || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2639        // ON by default for greedy on the wgpu graph: with the draft on
2640        // the graph and the verify bit-exact, it measured 58.7 tok/s
2641        // against a plain 48.1 on Qwen3.8-27B q4tp / RTX 5090 (k=4) and
2642        // 51.1 against 49.4 on Qwen3.6-27B, and a round that stops
2643        // paying turns itself off below (acceptance watchdog).
2644        // `CMF_GRAPH_SPEC=0` disables; `=1` was the old opt-in spelling.
2645        // …but only where the batched verify has its register-blocked
2646        // kernel: q4tp dense FFNs (graph kind 6). q4t and q8_2f verify
2647        // through tile GEMMs today and measured a LOSS (q8_2f 22 against
2648        // 29 tok/s), the 2-bit plane the same; those stay opt-in
2649        // (`CMF_GRAPH_SPEC=1`).
2650        // …at least in nine dense FFNs of ten: a healed file carries its
2651        // last two layers at q8_2f, and two tile-GEMM verifies among 64 do
2652        // not change the arithmetic (measured: the healed q4tp file
2653        // decodes at the plain file's rate and would otherwise sit out).
2654        let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2655        for lw in &self.weights.layers {
2656            if let FfnKind::Dense(d) = &lw.ffn {
2657                dense_n += 1;
2658                if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2659                    && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2660                    && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2661                {
2662                    dense_q4tp += 1;
2663                }
2664            }
2665        }
2666        let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2667        // Penalties break the draft head's agreement with the trunk (a
2668        // 1.1 repetition penalty measured 2 of 16 accepted): not by
2669        // default there either.
2670        let penalized = self.sampler_config.repetition_penalty != 1.0
2671            || self.sampler_config.presence_penalty != 0.0
2672            || !self.sampler_config.suppress_tokens.is_empty();
2673        // …and not on wgpu-over-Metal: the batched verify graph there
2674        // returned 0 accepted drafts and garbage text on a GDN hybrid
2675        // (16.08, Qwen3.5-0.8B) while Vulkan is bit-exact; the Mac's
2676        // default backend is native Metal without a batch graph anyway.
2677        #[cfg(feature = "gpu")]
2678        let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2679        #[cfg(not(feature = "gpu"))]
2680        let metal_wgpu = false;
2681        let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2682        let spec_wanted = match spec_env.as_deref() {
2683            Some("0") => false,
2684            Some(_) => {
2685                if metal_wgpu {
2686                    tracing::warn!(
2687                        "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2688                         verified on this backend (garbage measured on Qwen3.5-0.8B)"
2689                    );
2690                }
2691                true
2692            }
2693            None => spec_default_ok && !penalized && !metal_wgpu,
2694        };
2695        // Native Metal: the b-row verify graph (`try_batch_graph_metal`)
2696        // stands where the wgpu batch graph stands on discrete cards.
2697        #[cfg(target_os = "macos")]
2698        let metal_graph = crate::gpu::q1_force()
2699            && crate::gpu::enabled_here()
2700            && std::env::var("CMF_GPU_BLOCK")
2701                .map(|v| v != "0")
2702                .unwrap_or(true);
2703        #[cfg(not(target_os = "macos"))]
2704        let metal_graph = false;
2705        let graph_spec = self.speculative
2706            && (graph_on || metal_graph)
2707            && self.mtp.is_some()
2708            && task_mask.is_none()
2709            && !self.o1_active()
2710            && spec_sampling_ok
2711            && spec_wanted;
2712        // GDN hybrids sit the fused-pair speculation out by default: the
2713        // recurrence is sequential, so the pair lane cannot parallelize
2714        // (the bench's own Pair line reads fused 1.28x TWO singles on the
2715        // 35B) and the draft's full-vocab head rides on top — measured 2x
2716        // SLOWER end to end (16.1 vs 32.4 tok/s on the 48-core stand).
2717        // CMF_MTP=1 forces it back for study.
2718        let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2719        let spec_active = self.speculative
2720            && self.mtp.is_some()
2721            && task_mask.is_none()
2722            && !self.o1_active()
2723            && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2724        // The MTP module is detached during generation so its mutable
2725        // state does not fight the borrow on `self`.
2726        let mut mtp = if spec_active { self.mtp.take() } else { None };
2727        if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2728            eprintln!(
2729                "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2730                mtp.is_some(),
2731                self.speculative,
2732                self.sampler_config.temperature < 1e-6,
2733            );
2734        }
2735        if let Some(m) = &mut mtp {
2736            m.kv.clear();
2737            // The MTP block's own device mirror starts over with its cache.
2738            crate::gpu::graph_kv_reset(self.mtp_kv_id());
2739            self.mtp_graph_mode = None;
2740        }
2741        // Dynamic router detached during decode (same borrow trick as MTP).
2742        // Speculative decode and dynamic routing are mutually exclusive
2743        // for now — the fused-pair path doesn't carry per-token φ.
2744        let mut router = if mtp.is_none() {
2745            self.dyn_router.take()
2746        } else {
2747            None
2748        };
2749        if let Some(r) = &mut router {
2750            r.reset(); // active=backbone, matching a fresh overlay
2751            self.dyn_phi_seen = 0; // fresh φ EMA per generation
2752            let _ = self.set_active_skill(None);
2753        }
2754
2755        let mut all_ids = input_ids.to_vec();
2756        let mut generated = 0usize;
2757        let mut finish_reason = "max_tokens".to_string();
2758        let mut drafted = 0usize;
2759        let mut accepted = 0usize;
2760        // DeepSeek-V4's draft quality is strongly content-dependent.  Two
2761        // consecutive paid rounds with no extra token put it on a bounded
2762        // cooldown; predictable text keeps batching, ordinary prose falls
2763        // back to the exact walk instead of paying a slow draft forever.
2764        // Local to one generation so one difficult request cannot poison the
2765        // next one, and deliberately automatic — this is not a user knob.
2766        let mut dsv4_spec_bad = 0usize;
2767        let mut dsv4_spec_retry_at = 0usize;
2768        let mut confidence: Vec<f32> = Vec::new();
2769        let trace_on = self.trace;
2770        let calib_temp = self.calib_temp;
2771        let mut traces: Vec<TokenTrace> = Vec::new();
2772
2773        // ── Prefill: forward each prompt token once, KEEP the last hidden.
2774        //    Dense prefill runs in fused pairs (weights streamed once per
2775        //    two positions — bit-identical to sequential, proven by the
2776        //    pair tests). With MTP: warm the draft head on
2777        //    (hidden_p, token_{p+1}) pairs.
2778        let mut hidden = vec![0.0f32; self.hidden_size];
2779        let mut pos = reuse_from;
2780        // lm_head-in-graph is only sound when the very next logits
2781        // consumer is this loop's own (MTP and skill routing interleave
2782        // other forwards / can swap lm_head between forward and sample).
2783        // CMF_GPU_LMHEAD=0 keeps lm_head off the graph: the token reads back
2784        // the 8 KB hidden instead of ~1 MB of logits, and the head runs on
2785        // the host. A probe for how much of the graph's fixed per-token cost
2786        // is the logits readback (the layer sweep puts that fixed part at
2787        // 3.88 ms of an 18.5 ms frame).
2788        let fuse_lm = mtp.is_none()
2789            && router.is_none()
2790            && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2791        self.graph_logits = None;
2792        self.graph_want_logits = false;
2793        let _tpf = std::time::Instant::now();
2794        let batch_k = std::env::var("CMF_BATCH_K")
2795            .ok()
2796            .and_then(|v| v.parse::<usize>().ok())
2797            .unwrap_or(0);
2798        // DeepSeek-V4 owns a separate hyper-connection stack. Route it
2799        // before the generic prefill choices: those correctly reject an
2800        // empty `weights.layers`, but their final per-position fallback used
2801        // to consume the whole prompt before `dsv4::forward_chunk` could see
2802        // it. The batch implementation therefore existed without a live
2803        // production entry point.
2804        //
2805        // Bounded chunks preserve cancellation responsiveness. Only the
2806        // prompt's final chunk asks for logits; every earlier head projection
2807        // would produce 129 280 values that no caller reads.
2808        while self.qwen4_exp.is_some()
2809            && mtp.is_none()
2810            && pos < input_ids.len()
2811            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2812        {
2813            let token_id = input_ids[pos];
2814            let want_logits = pos + 1 == input_ids.len();
2815            let mut lg = Vec::new();
2816            if let Some(b) = &mut self.qwen4_exp {
2817                crate::qwen4_exp::forward_token(
2818                    &b.0,
2819                    &b.1,
2820                    &b.2,
2821                    &mut b.3,
2822                    token_id,
2823                    pos,
2824                    &self.inv_freq,
2825                    self.pool.as_deref(),
2826                    &mut lg,
2827                    want_logits,
2828                );
2829            }
2830            if want_logits {
2831                self.graph_logits = Some(lg);
2832            }
2833            pos += 1;
2834            hidden.fill(0.0);
2835        }
2836        while self.dsv4.is_some()
2837            && mtp.is_none()
2838            && pos < input_ids.len()
2839            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2840        {
2841            let end = (pos + prefill_chunk()).min(input_ids.len());
2842            let ids: Vec<u32> = input_ids[pos..end].to_vec();
2843            let mut lg = Vec::new();
2844            if let Some(b) = &mut self.dsv4 {
2845                let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2846                crate::dsv4::forward_chunk(
2847                    g,
2848                    layers,
2849                    &cfg,
2850                    st,
2851                    &ids,
2852                    pos,
2853                    &self.inv_freq,
2854                    self.pool.as_deref(),
2855                    &mut lg,
2856                    end == input_ids.len(),
2857                );
2858            }
2859            if end == input_ids.len() {
2860                self.graph_logits = Some(lg);
2861            }
2862            pos = end;
2863            hidden = vec![0.0; self.hidden_size];
2864        }
2865        let dsv41_prefill = self.dsv41_prefill.take();
2866        while self.dsv41.is_some()
2867            && mtp.is_none()
2868            && pos < input_ids.len()
2869            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2870        {
2871            let end = (pos + prefill_chunk()).min(input_ids.len());
2872            let ids: Vec<u32> = input_ids[pos..end].to_vec();
2873            let mut lg = Vec::new();
2874            if let Some(b) = &mut self.dsv41 {
2875                let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
2876                if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
2877                    crate::dsv41::forward_chunk_masked_with_embeddings(
2878                        g,
2879                        layers,
2880                        cfg,
2881                        st,
2882                        &ids,
2883                        pos,
2884                        &embeddings[pos..end],
2885                        &participates[pos..end],
2886                        self.pool.as_deref(),
2887                        &mut lg,
2888                    );
2889                } else {
2890                    crate::dsv41::forward_chunk(
2891                        g,
2892                        layers,
2893                        cfg,
2894                        st,
2895                        &ids,
2896                        pos,
2897                        self.pool.as_deref(),
2898                        &mut lg,
2899                    );
2900                }
2901            }
2902            if end == input_ids.len() {
2903                self.graph_logits = Some(lg);
2904            }
2905            pos = end;
2906            hidden = vec![0.0; self.hidden_size];
2907        }
2908        // With dynamic routing, prefill sequentially so the φ hook fires
2909        // over the PROMPT — the router enters decode with a warm φ (the
2910        // fused-pair path skips the per-layer φ capture). o1 layers
2911        // collect their query trace in both the single and pair paths.
2912        let dyn_prefill = router.is_some();
2913        // Optional bounded calibration prefix for generation.  The normal
2914        // O(1) path seals after the full prompt; this explicit knob instead
2915        // runs only the requested prefix through exact attention, seals the
2916        // Nyström state, and streams the rest of the prompt through the same
2917        // O(1) step used by decode.  It keeps the O(1) layers' Q trace and
2918        // temporary full KV bounded by the prefix while leaving the default
2919        // full-prompt quality profile untouched.
2920        let o1_prefill_limit = o1_prefill
2921            .and_then(|requested| self.o1_effective_boundary(requested))
2922            .map(|boundary| boundary.min(input_ids.len()));
2923        let mut o1_sealed = false;
2924        if let Some(limit) = o1_prefill_limit {
2925            // Reuse the exact batched prefix machinery when available; it
2926            // records the same per-position Q trace as the full prefill.
2927            if self.can_prefill_batched() && limit > 2 {
2928                let chunk = prefill_chunk();
2929                let hs = self.hidden_size;
2930                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2931                    let end = (pos + chunk).min(limit);
2932                    let hb = self.prefill_batch(&input_ids[pos..end], pos);
2933                    hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2934                    pos = end;
2935                }
2936            } else {
2937                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2938                    hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
2939                    pos += 1;
2940                }
2941            }
2942            if pos >= limit {
2943                o1_sealed = match self.o1_seal_checked() {
2944                    Ok(sealed) => sealed,
2945                    Err(err) => {
2946                        self.finish_generation(&mut mtp, &mut router, true);
2947                        return Err(err);
2948                    }
2949                };
2950                tracing::info!(
2951                    "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
2952                    o1_prefill.unwrap_or(0),
2953                    self.o1_effective_boundary(o1_prefill.unwrap_or(0))
2954                        .unwrap_or(limit),
2955                    limit,
2956                    input_ids.len()
2957                );
2958            }
2959        }
2960        // q1 hybrids on Metal: the per-position GPU token graph beats
2961        // the CPU chunk-GEMM (whose wall is the sequential scalar GDN
2962        // recurrence), so prefill goes position-by-position through the
2963        // same graph as decode. Pure-attention models keep the batched
2964        // path — there the chunk-GEMM amortization wins.
2965        let graph_prefill = self.graph_prefill_preferred();
2966        // Native Metal, q4tp GDN hybrids: the prompt through the b-row
2967        // rows graph — projections as GEMMs over up to 512 positions, the
2968        // GDN recurrence in registers on the device, K/V rows appended by
2969        // the chunk — instead of one token-graph submit per position (the
2970        // 27B: 8 tok/s → GEMM-bound). The MTP warm-up rows come out of one
2971        // batched run of the block per chunk. Any refusal leaves the rest
2972        // of the prompt to the sequential paths below.
2973        #[cfg(target_os = "macos")]
2974        if task_mask.is_none()
2975            && !dyn_prefill
2976            && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
2977            && crate::gpu::enabled_here()
2978            && self.gdn_cfg.is_some()
2979            && self.g3n.is_none()
2980            && input_ids.len() > 8
2981            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2982            && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
2983        {
2984            let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
2985                .ok()
2986                .and_then(|v| v.parse().ok())
2987                .filter(|&v| (16..=512).contains(&v))
2988                .unwrap_or(256);
2989            let hs = self.hidden_size;
2990            let _tp = std::time::Instant::now();
2991            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2992                let end = (pos + chunk).min(input_ids.len());
2993                let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
2994                    MetalPrefillOutcome::Completed(hb) => hb,
2995                    MetalPrefillOutcome::Declined => break,
2996                    MetalPrefillOutcome::Failed => {
2997                        self.finish_generation(&mut mtp, &mut router, true);
2998                        return Err("ordinary Metal prefill failed after admission".into());
2999                    }
3000                };
3001                if let Some(m) = &mut mtp {
3002                    let n_pairs = if end < input_ids.len() {
3003                        end - pos
3004                    } else {
3005                        end - pos - 1
3006                    };
3007                    if n_pairs > 0 {
3008                        let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
3009                            .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
3010                            .collect();
3011                        if !self.mtp_warm_batch_metal(m, &pairs, pos) {
3012                            for (j, (h, t)) in pairs.iter().enumerate() {
3013                                let h = h.to_vec();
3014                                let _ = self.mtp_step(m, &h, *t, pos + j);
3015                            }
3016                        }
3017                    }
3018                }
3019                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3020                pos = end;
3021            }
3022            if std::env::var("CMF_PREFILL_PROF").is_ok() {
3023                eprintln!(
3024                    "metal-prefill: {} of {} tokens in {:.1} ms",
3025                    pos,
3026                    input_ids.len(),
3027                    _tp.elapsed().as_secs_f64() * 1e3
3028                );
3029            }
3030        }
3031        if task_mask.is_none()
3032            && !dyn_prefill
3033            && !graph_prefill
3034            && self.can_prefill_batched()
3035            && self.g3n.is_none()
3036            && o1_prefill.is_none()
3037            && input_ids.len() > 2
3038        {
3039            // Production prefill = the same chunked prefill-GEMM that
3040            // bench/PPL measure (roadmap §3 P0: generation used to warm
3041            // the prompt with the slower pair path — the published
3042            // prefill number didn't match real TTFT). MTP warm-up reads
3043            // each position's hidden straight from the chunk result.
3044            let chunk = prefill_chunk();
3045            let hs = self.hidden_size;
3046            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3047                let end = (pos + chunk).min(input_ids.len());
3048                let hb = self.prefill_batch(&input_ids[pos..end], pos);
3049                if let Some(m) = &mut mtp {
3050                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3051                        .ok()
3052                        .and_then(|v| v.parse().ok())
3053                        .unwrap_or(0);
3054                    for p in pos..end {
3055                        if p + 1 < input_ids.len() {
3056                            if probe >= 1 && p + 2 < input_ids.len() {
3057                                // Teacher-forced chain acceptance (see the
3058                                // tail loop's twin): the warm-up row stays,
3059                                // the chain's rows roll back.
3060                                let (d1, mut hx) = self.mtp_step_h(
3061                                    m,
3062                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
3063                                    input_ids[p + 1],
3064                                    p,
3065                                );
3066                                let mut ok = d1 == input_ids[p + 2];
3067                                Self::chain_probe_note(0, ok);
3068                                let mut d_prev = d1;
3069                                let mut extra = 0usize;
3070                                for j in 1..probe {
3071                                    if p + 2 + j >= input_ids.len() {
3072                                        break;
3073                                    }
3074                                    let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
3075                                    extra += 1;
3076                                    ok = ok && dj == input_ids[p + 2 + j];
3077                                    Self::chain_probe_note(j, ok);
3078                                    d_prev = dj;
3079                                    hx = hj;
3080                                }
3081                                m.kv.truncate_last(extra);
3082                            } else {
3083                                let _ = self.mtp_step(
3084                                    m,
3085                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
3086                                    input_ids[p + 1],
3087                                    p,
3088                                );
3089                            }
3090                        }
3091                    }
3092                }
3093                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3094                pos = end;
3095            }
3096        }
3097        let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
3098        if task_mask.is_none()
3099            && !dyn_prefill
3100            && !graph_prefill
3101            && !pair_off
3102            && self.pair_supported()
3103            && o1_prefill.is_none()
3104        {
3105            while pos + 1 < input_ids.len()
3106                && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3107            {
3108                let e1 = self.embed_single(input_ids[pos]);
3109                let e2 = self.embed_single(input_ids[pos + 1]);
3110                let (h1, h2) = self.forward_pair(&e1, &e2, pos);
3111                // Both prefill tokens are real → commit lane-2 states.
3112                self.commit_linear_scratch();
3113                if let Some(m) = &mut mtp {
3114                    let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
3115                    if pos + 2 < input_ids.len() {
3116                        let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3117                            .ok()
3118                            .and_then(|v| v.parse().ok())
3119                            .unwrap_or(0);
3120                        if probe >= 1 && pos + 3 < input_ids.len() {
3121                            // Same teacher-forced chain table as the tail
3122                            // loop below, fed from the pair path that owns
3123                            // most prefill positions.
3124                            let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
3125                            let mut ok = d1 == input_ids[pos + 3];
3126                            Self::chain_probe_note(0, ok);
3127                            let mut d_prev = d1;
3128                            let mut extra = 0usize;
3129                            for j in 1..probe {
3130                                if pos + 3 + j >= input_ids.len() {
3131                                    break;
3132                                }
3133                                let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
3134                                extra += 1;
3135                                ok = ok && dj == input_ids[pos + 3 + j];
3136                                Self::chain_probe_note(j, ok);
3137                                d_prev = dj;
3138                                hx = hj;
3139                            }
3140                            m.kv.truncate_last(extra);
3141                        } else {
3142                            let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
3143                        }
3144                    }
3145                }
3146                hidden = h2;
3147                pos += 2;
3148            }
3149        }
3150        // Batched GPU prefill for the wgpu decode graph (GDN hybrids): K prompt
3151        // positions per submit — projections/FFN as GEMMs (weight once per K),
3152        // attention/GDN looped inside — instead of one whole-graph submit per
3153        // position. Falls through to the per-position graph on any refusal.
3154        // Batched prefill is opt-in (CMF_BATCH_K>0). Default 0 = per-position
3155        // graph prefill. (Steady-state decode is provably identical either way —
3156        // token-graph submit and lm_head both unchanged — so this only trades
3157        // prefill wall.)
3158        // A bounded O(1) prefix is the one post-seal prompt interval: only
3159        // admit its batch when the device O(1) route is explicitly enabled and
3160        // every sealed layer exposes a portable view. The same batch size and
3161        // refusal behavior remain the ordinary controls/comparator.
3162        let o1_batch_ready = o1_sealed
3163            && o1_prefill.is_some()
3164            && mtp.is_none()
3165            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
3166            && (0..self.num_layers).all(|li| {
3167                let cache = &self.kv_cache.layers[self.phys_layer(li)];
3168                cache.o1.is_none() || cache.o1_views().is_some()
3169            });
3170        // The ordinary graph-prefill route can share each completed trunk
3171        // chunk with an attached MTP head.  Keep chain probing on its
3172        // established per-position path: the probe deliberately needs every
3173        // teacher-forced draft row and its rollback table.
3174        let mtp_batch_prefill = mtp.is_some()
3175            && graph_prefill
3176            && task_mask.is_none()
3177            && !dyn_prefill
3178            && !self.o1_active()
3179            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
3180        if batch_k > 0
3181            && (graph_prefill || o1_batch_ready)
3182            && task_mask.is_none()
3183            && (!self.o1_active() || o1_batch_ready)
3184            && (mtp.is_none() || mtp_batch_prefill)
3185            && !dyn_prefill
3186            && pos + 1 < input_ids.len()
3187        {
3188            let hs = self.hidden_size;
3189            let chunk = batch_k;
3190            while pos < input_ids.len() {
3191                let end = (pos + chunk).min(input_ids.len());
3192                let bk = end - pos;
3193                let mut hiddens = vec![0f32; bk * hs];
3194                for (j, &id) in input_ids[pos..end].iter().enumerate() {
3195                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3196                }
3197                let positions: Vec<usize> = (pos..end).collect();
3198                let t_chunk = std::time::Instant::now();
3199                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3200                let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
3201                if std::env::var("CMF_GRAPH_PROF").is_ok() {
3202                    let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3203                    eprintln!(
3204                        "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3205                        if o1_batch_ready {
3206                            "o1"
3207                        } else if mtp_batch_prefill {
3208                            "ordinary_mtp"
3209                        } else {
3210                            "ordinary"
3211                        },
3212                        bk as f64 / (ms / 1000.0)
3213                    );
3214                }
3215                {
3216                    use std::sync::atomic::{AtomicBool, Ordering};
3217                    static SAID: AtomicBool = AtomicBool::new(false);
3218                    if !SAID.swap(true, Ordering::Relaxed) {
3219                        if ok_b {
3220                            tracing::info!(
3221                                "batched prefill: ACTIVE mode={} (k={bk})",
3222                                if o1_batch_ready {
3223                                    "o1"
3224                                } else if mtp_batch_prefill {
3225                                    "ordinary_mtp"
3226                                } else {
3227                                    "ordinary"
3228                                }
3229                            );
3230                        } else {
3231                            tracing::warn!("batched prefill {:?} — per-position graph", outcome);
3232                        }
3233                    }
3234                }
3235                if ok_b {
3236                    if mtp_batch_prefill {
3237                        let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
3238                        if n_pairs > 0 {
3239                            // `hiddens` is owned by this chunk, so materialize
3240                            // row slices before borrowing the detached MTP
3241                            // module.  The last prompt row has no successor;
3242                            // the helper above is the single source of that
3243                            // boundary rule.
3244                            let rows: Vec<Vec<f32>> = (0..n_pairs)
3245                                .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
3246                                .collect();
3247                            let pairs: Vec<(&[f32], u32)> = rows
3248                                .iter()
3249                                .enumerate()
3250                                .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
3251                                .collect();
3252                            if std::env::var("CMF_GRAPH_PROF").is_ok() {
3253                                eprintln!(
3254                                    "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
3255                                    pos,
3256                                    n_pairs,
3257                                    pos + n_pairs - 1,
3258                                );
3259                            }
3260                            let warm_error = if let Some(m) = mtp.as_mut() {
3261                                self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
3262                            } else {
3263                                None
3264                            };
3265                            if let Some(err) = warm_error {
3266                                // The trunk batch was already admitted.  A
3267                                // failed MTP warm-up therefore clears both
3268                                // mirrors and exits; continuing would pair a
3269                                // current trunk state with a stale MTP cache.
3270                                self.finish_generation(&mut mtp, &mut router, true);
3271                                return Err(err.to_string());
3272                            }
3273                        }
3274                    }
3275                    hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
3276                    pos = end;
3277                } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
3278                    // A failed batch may have advanced a device recurrent
3279                    // state (ordinary GDN or sealed O(1)). A CPU fallback
3280                    // would then observe stale accumulators, so clear the
3281                    // request state and make the failure explicit.
3282                    self.finish_generation(&mut mtp, &mut router, true);
3283                    return Err(if o1_batch_ready {
3284                        "sealed O(1) batch graph failed after admission".to_string()
3285                    } else {
3286                        "ordinary recurrent batch graph failed after admission".to_string()
3287                    });
3288                } else {
3289                    break; // unsupported → per-position graph handles the rest
3290                }
3291            }
3292        }
3293        while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3294            self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
3295            hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
3296            if let Some(m) = &mut mtp {
3297                if pos + 1 < input_ids.len() {
3298                    // `CMF_MTP_CHAIN_PROBE=k`: teacher-forced acceptance of a
3299                    // CHAINED draft — iterate the head on its own hidden k
3300                    // deep and score every depth against the prompt's real
3301                    // continuation. The economics of a k-token speculative
3302                    // round stand or fall on this table.
3303                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3304                        .ok()
3305                        .and_then(|v| v.parse().ok())
3306                        .unwrap_or(0);
3307                    if probe >= 1 && pos + 2 < input_ids.len() {
3308                        let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
3309                        let mut ok = d1 == input_ids[pos + 2];
3310                        Self::chain_probe_note(0, ok);
3311                        let mut d_prev = d1;
3312                        let mut extra = 0usize;
3313                        for j in 1..probe {
3314                            if pos + 2 + j >= input_ids.len() {
3315                                break;
3316                            }
3317                            let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
3318                            extra += 1;
3319                            ok = ok && dj == input_ids[pos + 2 + j];
3320                            Self::chain_probe_note(j, ok);
3321                            d_prev = dj;
3322                            hx = hj;
3323                        }
3324                        // The chain's rows are speculation, not the prompt —
3325                        // keep only the warmup row the plain path would add.
3326                        m.kv.truncate_last(extra);
3327                    } else {
3328                        let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
3329                    }
3330                }
3331            }
3332            pos += 1;
3333        }
3334        if std::env::var("CMF_PREFILL_PROF").is_ok() {
3335            eprintln!(
3336                "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
3337                input_ids.len(),
3338                _tpf.elapsed().as_secs_f64() * 1000.0
3339            );
3340        }
3341        if self
3342            .graph_failed
3343            .swap(false, std::sync::atomic::Ordering::Relaxed)
3344        {
3345            // MTP is detached for speculative generation.  Restore the
3346            // module before returning the terminal graph error; otherwise a
3347            // failed request would silently remove the head from a pooled
3348            // pipeline and the next request would lose its configured route.
3349            self.finish_generation(&mut mtp, &mut router, true);
3350            return Err("GPU token graph failed during prefill".to_string());
3351        }
3352        // Cancelled mid-prefill: the cache holds a partial prompt —
3353        // drop the reuse history and return an empty generation.
3354        if self
3355            .cancel
3356            .swap(false, std::sync::atomic::Ordering::Relaxed)
3357        {
3358            // A cancelled prefill can already have advanced the device
3359            // mirror. Drop the whole partial sequence so a pooled pipeline
3360            // cannot carry that state into its next request.
3361            self.finish_generation(&mut mtp, &mut router, true);
3362            return Ok(GenerateResult {
3363                text: String::new(),
3364                token_ids: Vec::new(),
3365                prompt_tokens: input_ids.len(),
3366                tokens_generated: 0,
3367                finish_reason: "cancelled".to_string(),
3368                mtp_drafted: 0,
3369                mtp_accepted: 0,
3370                token_confidence: Vec::new(),
3371                traces: Vec::new(),
3372            });
3373        }
3374
3375        // Prompt absorbed → freeze the o1 layers' skeletons; from here
3376        // every decode step on those layers is O(W + m·dv + m²).
3377        if !o1_sealed {
3378            match self.o1_seal_checked() {
3379                Ok(_) => {}
3380                Err(err) => {
3381                    self.finish_generation(&mut mtp, &mut router, true);
3382                    return Err(err);
3383                }
3384            }
3385        }
3386
3387        // Commit one token: push, check EOS, stream. Returns false = stop.
3388        macro_rules! commit {
3389            ($id:expr) => {{
3390                all_ids.push($id);
3391                generated += 1;
3392                if self.tokenizer.is_eos($id) {
3393                    finish_reason = "stop".to_string();
3394                    false
3395                } else {
3396                    let token_text = self.tokenizer.decode_token($id);
3397                    let mut go = true;
3398                    if let Some(ref mut cb) = on_token {
3399                        if !cb(&token_text) {
3400                            finish_reason = "cancelled".to_string();
3401                            go = false;
3402                        }
3403                    }
3404                    go
3405                }
3406            }};
3407        }
3408
3409        // Speculation is decided by MEASUREMENT, not by an acceptance
3410        // model. A k=4 round costs ~3.8 plain tokens on the 5090 (draft
3411        // 6.6 + verify 66.6 + commit 4.8 ms against a 20.6 ms token), so it
3412        // pays only when the head lands ~2.8 of 4 — predictable text (code,
3413        // structured output) does, free prose often does not, and the
3414        // ratio at which the two cross depends on the card and the context
3415        // depth. So: four speculative rounds timed, then eight plain
3416        // tokens timed, and the faster arm runs until a re-check 256
3417        // tokens later (context growth moves the balance). The trial
3418        // costs at most a few tokens of the slower arm per 256.
3419        let mut spec_trial = SpecTrial::Spec {
3420            t0: std::time::Instant::now(),
3421            gen0: generated,
3422            rounds: 0,
3423        };
3424        let mut spec_mon = SpecMon::default();
3425        let mut spec_watchdog_off = false;
3426        // ── Decode ──
3427        let mut next_pos = input_ids.len();
3428        'decode: while generated < max_tokens {
3429            if self
3430                .graph_failed
3431                .swap(false, std::sync::atomic::Ordering::Relaxed)
3432            {
3433                // Keep the detached MTP module attached after a terminal
3434                // graph error so the pipeline can be reused for a fresh
3435                // sequence.  `clear_sequence_state` only clears mirrors and
3436                // host KV; it cannot recover a module dropped here.
3437                self.finish_generation(&mut mtp, &mut router, true);
3438                return Err("GPU token graph failed during decode".to_string());
3439            }
3440            if self
3441                .cancel
3442                .swap(false, std::sync::atomic::Ordering::Relaxed)
3443            {
3444                finish_reason = "cancelled".to_string();
3445                break 'decode;
3446            }
3447            // A rejected speculative draft already drew this position's
3448            // token from the residual distribution (graph_spec_step); it
3449            // is committed as-is — sampling again from the row's logits
3450            // would bias the stream toward the target's mode.
3451            let forced = self.spec_forced.take();
3452            let mut logits = match (forced, self.graph_logits.take()) {
3453                (Some(_), _) => Vec::new(),
3454                (None, Some(lg)) => lg,
3455                (None, None) => {
3456                    inference::rms_norm_into(
3457                        &hidden,
3458                        &self.weights.final_norm,
3459                        self.rms_eps,
3460                        self.norm_style,
3461                        &mut self.ws.n1,
3462                    );
3463                    self.lm_head_forward(&self.ws.n1)
3464                }
3465            };
3466            // CMF_LOGIT_DUMP=<path>: the first decode step's hidden + logits
3467            // as raw f32 (hidden first) — cross-backend numerics diffing.
3468            if generated
3469                == std::env::var("CMF_LOGIT_DUMP_STEP")
3470                    .ok()
3471                    .and_then(|v| v.parse().ok())
3472                    .unwrap_or(0)
3473            {
3474                if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
3475                    let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
3476                    for v in hidden.iter().chain(logits.iter()) {
3477                        bytes.extend_from_slice(&v.to_le_bytes());
3478                    }
3479                    if let Err(e) = std::fs::write(&path, &bytes) {
3480                        eprintln!("logit dump: failed to write {path}: {e}");
3481                        self.finish_generation(&mut mtp, &mut router, true);
3482                        return Err(format!("logit dump write failed: {e}"));
3483                    }
3484                }
3485            }
3486            let t_next = match forced {
3487                Some(c) => c,
3488                None => sampler::sample_with_scratch_pool(
3489                    &logits,
3490                    &self.sampler_config,
3491                    &all_ids,
3492                    &mut self.rng,
3493                    &mut self.sampler_scratch,
3494                    self.pool.as_deref(),
3495                ),
3496            };
3497            if self.confidence_on {
3498                confidence.push(if logits.is_empty() {
3499                    0.0
3500                } else {
3501                    sampler::top1_prob_pool(
3502                        self.pool.as_deref(),
3503                        &mut self.sampler_scratch,
3504                        &logits,
3505                        t_next,
3506                        calib_temp,
3507                    )
3508                });
3509            }
3510            if !logits.is_empty() {
3511                attention::recycle_buf(&mut logits);
3512            }
3513            if trace_on {
3514                // active_skill = the overlay in force while this token was
3515                // generated; recon/switched are filled after the post-emit
3516                // routing eval below (freshest coherence for this token).
3517                let skill = router.as_ref().and_then(|r| r.active_id());
3518                traces.push(TokenTrace {
3519                    t: generated,
3520                    token_id: t_next,
3521                    confidence: confidence.last().copied().unwrap_or(0.0),
3522                    active_skill: skill,
3523                    recon: None,
3524                    switched: false,
3525                });
3526            }
3527            if !commit!(t_next) {
3528                break 'decode;
3529            }
3530            if generated >= max_tokens {
3531                break 'decode;
3532            }
3533
3534            if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
3535                // Say it ONCE, loudly: past this point the model keeps
3536                // talking but has lost half its context, and on a GDN
3537                // hybrid the graph's device state goes stale on top. The
3538                // Qwen3.8 bring-up spent a day reading this cliff as
3539                // three different model bugs.
3540                static SAID: std::sync::Once = std::sync::Once::new();
3541                SAID.call_once(|| {
3542                    tracing::warn!(
3543                        "KV cache full at {} positions — evicting half; quality \
3544                         will degrade. Raise CMF_MAX_SEQ.",
3545                        self.kv_cache.max_seq_len,
3546                    );
3547                });
3548                let keep = (self.kv_cache.max_seq_len / 2).max(1);
3549                self.kv_cache.evict(keep);
3550            }
3551
3552            // Advance the speculation trial: plain-phase accounting and
3553            // the periodic re-check happen here, on every token.
3554            if graph_spec {
3555                match spec_trial {
3556                    SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
3557                        spec_mon.plain_ms =
3558                            t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
3559                        let keep = spec_mon.pays();
3560                        tracing::info!(
3561                            "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3562                            spec_mon.tokens,
3563                            spec_mon.round_ms,
3564                            spec_mon.plain_ms,
3565                            if keep { "speculating" } else { "plain" }
3566                        );
3567                        spec_mon.fails = 0;
3568                        spec_trial = SpecTrial::Decided {
3569                            spec: keep,
3570                            recheck_at: if keep { usize::MAX } else { generated + 128 },
3571                        };
3572                    }
3573                    SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
3574                        spec_mon.n = 0;
3575                        spec_trial = SpecTrial::Spec {
3576                            t0: std::time::Instant::now(),
3577                            gen0: generated,
3578                            rounds: 0,
3579                        };
3580                    }
3581                    _ => {}
3582                }
3583                spec_watchdog_off = matches!(
3584                    spec_trial,
3585                    SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
3586                );
3587            }
3588            match &mut mtp {
3589                // ── Graph speculation: chain-draft, batch-verify on device ──
3590                #[cfg(feature = "gpu")]
3591                Some(m)
3592                    if graph_spec
3593                        && !spec_watchdog_off
3594                        && generated + 1 < max_tokens
3595                        && next_pos > 0 =>
3596                {
3597                    let t_round = std::time::Instant::now();
3598                    if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
3599                        m,
3600                        &hidden,
3601                        t_next,
3602                        next_pos,
3603                        &mut drafted,
3604                        &mut accepted,
3605                        &mut all_ids,
3606                    ) {
3607                        next_pos = n_pos;
3608                        hidden = new_h;
3609                        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3610                            eprintln!(
3611                                "spec-round wall {:.1} ms → {} tokens",
3612                                t_round.elapsed().as_secs_f64() * 1e3,
3613                                extra.len() + 1
3614                            );
3615                        }
3616                        // One speculative round done: the monitor counts it
3617                        // (round 1 untimed — it pays the batch scratch and
3618                        // the draft mirror), and the trial advances.
3619                        spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
3620                        // the round's tokens land in `generated` below; the
3621                        // plain phase must start counting AFTER them
3622                        spec_trial = Self::spec_trial_round(
3623                            spec_trial,
3624                            &mut spec_mon,
3625                            generated + extra.len() + 1,
3626                        );
3627                        let mut stopped = false;
3628                        for &id in &extra {
3629                            if self.confidence_on {
3630                                confidence.push(0.0);
3631                            }
3632                            if !commit!(id) {
3633                                stopped = true;
3634                                break;
3635                            }
3636                        }
3637                        if stopped {
3638                            break 'decode;
3639                        }
3640                        continue 'decode;
3641                    }
3642                    if self
3643                        .graph_failed
3644                        .swap(false, std::sync::atomic::Ordering::Relaxed)
3645                    {
3646                        // `graph_spec_step` may have detached MTP while a
3647                        // warm-up was in flight.  Do not reinterpret its
3648                        // terminal device failure as a plain decode step;
3649                        // restore the head, clear both mirrors, and surface
3650                        // one explicit error to the caller.
3651                        self.finish_generation(&mut mtp, &mut router, true);
3652                        return Err("GPU MTP graph failed during speculative decode".to_string());
3653                    }
3654                    // Declined (batch graph refused): plain forward below —
3655                    // and a round that produced one token for the trial's
3656                    // ledger, so a graph that keeps refusing is measured out
3657                    // like a head that keeps missing (it was spinning
3658                    // forever on a file whose batch graph declines).
3659                    // A declined round is not a cheap one-token round — it
3660                    // is a verify that does not exist for this file (a
3661                    // healed q8_2f tail measured 760 drafts, 0 accepted, 33
3662                    // against 48.8 tok/s while the monitor called the draft
3663                    // alone "paying"). Count it as the losing streak in one.
3664                    spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
3665                    spec_mon.tokens = 0.0;
3666                    spec_mon.fails = 3;
3667                    spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
3668                    hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
3669                    next_pos += 1;
3670                    continue 'decode;
3671                }
3672                // ── Speculative: draft t+2, verify in a fused pair ──
3673                Some(m) if !graph_spec && generated + 1 < max_tokens => {
3674                    let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
3675                    drafted += 1;
3676                    let emb1 = self.embed_single(t_next);
3677                    let emb2 = self.embed_single(draft);
3678                    let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
3679
3680                    inference::rms_norm_into(
3681                        &h1,
3682                        &self.weights.final_norm,
3683                        self.rms_eps,
3684                        self.norm_style,
3685                        &mut self.ws.n1,
3686                    );
3687                    let mut logits1 = self.lm_head_forward(&self.ws.n1);
3688                    let t_after = sampler::sample_with_scratch_pool(
3689                        &logits1,
3690                        &self.sampler_config,
3691                        &all_ids,
3692                        &mut self.rng,
3693                        &mut self.sampler_scratch,
3694                        self.pool.as_deref(),
3695                    );
3696                    if self.confidence_on {
3697                        confidence.push(sampler::top1_prob_pool(
3698                            self.pool.as_deref(),
3699                            &mut self.sampler_scratch,
3700                            &logits1,
3701                            t_after,
3702                            calib_temp,
3703                        ));
3704                    }
3705                    attention::recycle_buf(&mut logits1);
3706                    if trace_on {
3707                        // Speculative decode is mutually exclusive with
3708                        // dynamic routing (router is None here) — no skill.
3709                        traces.push(TokenTrace {
3710                            t: generated,
3711                            token_id: t_after,
3712                            confidence: confidence.last().copied().unwrap_or(0.0),
3713                            active_skill: None,
3714                            recon: None,
3715                            switched: false,
3716                        });
3717                    }
3718                    let stop = !commit!(t_after);
3719
3720                    if t_after == draft {
3721                        accepted += 1;
3722                        self.commit_linear_scratch();
3723                        let _ = self.mtp_step(m, &h1, t_after, next_pos);
3724                        hidden = h2;
3725                        next_pos += 2;
3726                    } else {
3727                        // The draft lane is wrong: roll its KV entry back.
3728                        for layer in &mut self.kv_cache.layers {
3729                            layer.truncate_last(1);
3730                        }
3731                        if !stop {
3732                            let _ = self.mtp_step(m, &h1, t_after, next_pos);
3733                            hidden = self.forward_layers(
3734                                &self.embed_single(t_after),
3735                                next_pos + 1,
3736                                None,
3737                            );
3738                        }
3739                        next_pos += 2;
3740                    }
3741                    if stop {
3742                        break 'decode;
3743                    }
3744                }
3745                // ── Vanilla: forward the sampled token ──
3746                _ => {
3747                    // ── DeepSeek-V4 speculative decode (CMF_DSV4_SPEC=1):
3748                    // draft five on the card, verify batched, commit the
3749                    // accepted prefix. Greedy only; a rejected token's state
3750                    // is restored and replayed, so output equals the walk. ──
3751                    #[cfg(feature = "gpu")]
3752                    if Self::dsv4_spec_on() && self.dsv4.is_some() {
3753                        static SAID: std::sync::Once = std::sync::Once::new();
3754                        SAID.call_once(|| {
3755                            eprintln!(
3756                                "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
3757                                !self.dsv4_mtp.is_empty(),
3758                                task_mask.is_none(),
3759                                router.is_none(),
3760                                !trace_on,
3761                                self.sampler_config.temperature < 1e-6,
3762                                self.sampler_config.repetition_penalty == 1.0,
3763                            );
3764                        });
3765                    }
3766                    #[cfg(feature = "gpu")]
3767                    if Self::dsv4_spec_on()
3768                        && self.dsv4.is_some()
3769                        && !self.dsv4_mtp.is_empty()
3770                        && task_mask.is_none()
3771                        && router.is_none()
3772                        && !trace_on
3773                        && self.sampler_config.temperature < 1e-6
3774                        && self.sampler_config.repetition_penalty == 1.0
3775                        && generated + 1 < max_tokens
3776                        && all_ids.len() >= 2
3777                        && generated >= dsv4_spec_retry_at
3778                    {
3779                        let tip_token = all_ids[all_ids.len() - 2];
3780                        let drafted0 = drafted;
3781                        let round = self.dsv4_spec_step(
3782                            tip_token,
3783                            t_next,
3784                            next_pos,
3785                            max_tokens.saturating_sub(generated),
3786                            &mut drafted,
3787                            &mut accepted,
3788                        );
3789                        if drafted > drafted0 {
3790                            let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
3791                            if useful {
3792                                dsv4_spec_bad = 0;
3793                            } else {
3794                                dsv4_spec_bad += 1;
3795                                if dsv4_spec_bad >= 2 {
3796                                    dsv4_spec_bad = 0;
3797                                    dsv4_spec_retry_at = generated.saturating_add(32);
3798                                    tracing::info!(
3799                                        "dsv4: draft не окупился дважды — точный walk на 32 токена"
3800                                    );
3801                                }
3802                            }
3803                        }
3804                        if let Some((extra, n_pos)) = round {
3805                            next_pos = n_pos;
3806                            let mut stopped = false;
3807                            for &id in &extra {
3808                                if self.confidence_on {
3809                                    confidence.push(0.0);
3810                                }
3811                                if !commit!(id) {
3812                                    stopped = true;
3813                                    break;
3814                                }
3815                            }
3816                            if stopped {
3817                                break 'decode;
3818                            }
3819                            continue 'decode;
3820                        }
3821                    }
3822                    self.graph_want_logits = fuse_lm;
3823                    // Greedy burst (CMF_MULTISTEP, default 8, 1 = off): while
3824                    // nothing observes per-token state — pure argmax sampling,
3825                    // no router/trace/confidence/mask — decode k tokens per
3826                    // submit and commit them wholesale. The trailing normal
3827                    // forward leaves logits for the loop top, as always.
3828                    let mut t_fwd = t_next;
3829                    let pure_greedy = self.sampler_config.temperature < 1e-6
3830                        && self.sampler_config.repetition_penalty == 1.0
3831                        && self.sampler_config.suppress_tokens.is_empty();
3832                    // Off by default: at every k the burst measured at or
3833                    // below the plain path on this graph shape (k=1 loses
3834                    // the argmax dispatches vs a 1 MB readback, k>=8 loses
3835                    // inter-step drains vs the saved sync). Experimental.
3836                    let burst_k = std::env::var("CMF_MULTISTEP")
3837                        .ok()
3838                        .and_then(|v| v.parse::<usize>().ok())
3839                        .unwrap_or(0);
3840                    if pure_greedy
3841                        && burst_k >= 1
3842                        && fuse_lm
3843                        && task_mask.is_none()
3844                        && router.is_none()
3845                        && !trace_on
3846                        && !self.confidence_on
3847                    {
3848                        let mut stopped = false;
3849                        loop {
3850                            let room = max_tokens.saturating_sub(generated);
3851                            if room <= 2 {
3852                                break;
3853                            }
3854                            let k = burst_k.min(room - 1);
3855                            if k < 1 {
3856                                break;
3857                            }
3858                            let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
3859                                if self
3860                                    .graph_failed
3861                                    .swap(false, std::sync::atomic::Ordering::Relaxed)
3862                                {
3863                                    self.finish_generation(&mut mtp, &mut router, true);
3864                                    return Err(
3865                                        "GPU token graph failed during greedy burst".to_string()
3866                                    );
3867                                }
3868                                break;
3869                            };
3870                            next_pos += k;
3871                            for &id in &ids {
3872                                if !commit!(id) {
3873                                    stopped = true;
3874                                    break;
3875                                }
3876                            }
3877                            if stopped {
3878                                break;
3879                            }
3880                            t_fwd = *ids.last().unwrap();
3881                        }
3882                        if stopped {
3883                            break 'decode;
3884                        }
3885                    }
3886                    hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
3887                    next_pos += 1;
3888                    // Dynamic routing: the forward updated φ; ask the
3889                    // router whether to switch skills before the next token.
3890                    if let Some(r) = &mut router {
3891                        let phi = self.dyn_phi_ema.clone();
3892                        let decision = r.step(&phi, generated);
3893                        if let Some(new_active) = decision {
3894                            let _ = self.set_active_skill(new_active);
3895                        }
3896                        // Backfill this token's coherence + switch flag from
3897                        // the just-run eval (freshest measured values).
3898                        if trace_on {
3899                            if let Some(last) = traces.last_mut() {
3900                                let e = r.last_best_e();
3901                                last.recon = e.is_finite().then_some(e);
3902                                last.switched = decision.is_some();
3903                            }
3904                        }
3905                    }
3906                }
3907            }
3908        }
3909
3910        let cancelled = finish_reason == "cancelled";
3911        self.finish_generation(&mut mtp, &mut router, cancelled);
3912
3913        let output_ids = &all_ids[input_ids.len()..];
3914        // Forwarded = prompt + all generated but the LAST sampled token
3915        // (emitted without being fed back). Exact only without MTP —
3916        // reuse is gated off when MTP is active.
3917        let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3918        if cancelled {
3919            self.kv_history.clear();
3920        } else {
3921            self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3922        }
3923        confidence.truncate(output_ids.len()); // guard against any overshoot
3924        traces.truncate(output_ids.len());
3925        Ok(GenerateResult {
3926            text: self.tokenizer.decode(output_ids),
3927            token_ids: output_ids.to_vec(),
3928            prompt_tokens: input_ids.len(),
3929            tokens_generated: generated,
3930            finish_reason,
3931            mtp_drafted: drafted,
3932            mtp_accepted: accepted,
3933            token_confidence: confidence,
3934            traces,
3935        })
3936    }
3937
3938    /// One MTP step: feed `(hidden_p, token_{p+1})` into the draft head,
3939    /// advance its KV cache at position `p`, return the drafted token
3940    /// for position `p+2`.
3941    fn mtp_step(
3942        &mut self,
3943        m: &mut MtpModule,
3944        hidden: &[f32],
3945        next_token: u32,
3946        position: usize,
3947    ) -> u32 {
3948        self.mtp_step_h(m, hidden, next_token, position).0
3949    }
3950
3951    /// Tally for `CMF_MTP_CHAIN_PROBE`: per depth, how often the CHAIN is
3952    /// still an exact prefix of the real continuation. Printed every 128
3953    /// depth-0 samples so a killed run still shows its table.
3954    fn chain_probe_note(depth: usize, prefix_ok: bool) {
3955        use std::sync::Mutex;
3956        static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
3957        let mut t = T.lock().unwrap();
3958        if t.len() <= depth {
3959            t.resize(depth + 1, (0, 0));
3960        }
3961        t[depth].0 += 1;
3962        t[depth].1 += prefix_ok as u64;
3963        if depth == 0 && t[0].0 % 128 == 0 {
3964            let line: Vec<String> = t
3965                .iter()
3966                .enumerate()
3967                .map(|(d, (n, k))| {
3968                    format!(
3969                        "d{}={:.0}%({n})",
3970                        d + 1,
3971                        100.0 * *k as f64 / (*n).max(1) as f64
3972                    )
3973                })
3974                .collect();
3975            eprintln!("mtp-chain: {}", line.join(" "));
3976        }
3977    }
3978
3979    /// `mtp_step` that also hands back the block's own output hidden — the
3980    /// state a CHAINED draft feeds the next step, the way a multi-token
3981    /// speculative round iterates the head on itself.
3982    /// One MTP block step from (trunk hidden, token): the head's LOGITS
3983    /// and the block's own hidden for chaining. The draft is argmax of the
3984    /// logits on the greedy path and a draw from their post-chain
3985    /// distribution on the sampling path.
3986    fn mtp_step_hl(
3987        &mut self,
3988        m: &mut MtpModule,
3989        hidden: &[f32],
3990        next_token: u32,
3991        position: usize,
3992    ) -> (Vec<f32>, Vec<f32>) {
3993        // The graph arm: the MTP block as a one-layer token graph with the
3994        // head fused — device attention over the block's own KV mirror,
3995        // one submit for block + head, hidden and logits back together.
3996        // Decided once per generation (see `mtp_graph_mode`).
3997        #[cfg(target_os = "macos")]
3998        if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
3999            if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
4000                self.mtp_graph_mode = Some(true);
4001                return r;
4002            }
4003            if self.mtp_graph_mode == Some(true) {
4004                tracing::error!("mtp Metal graph failed after admission");
4005                self.clear_sequence_state();
4006                self.graph_failed
4007                    .store(true, std::sync::atomic::Ordering::Relaxed);
4008                self.cancel
4009                    .store(true, std::sync::atomic::Ordering::Relaxed);
4010                return (Vec::new(), Vec::new());
4011            }
4012            self.mtp_graph_mode = Some(false);
4013        }
4014        #[cfg(feature = "gpu")]
4015        if self.mtp_graph_mode != Some(false) {
4016            if !self.mtp_graph_ok(m) {
4017                if self.mtp_graph_mode == Some(true) {
4018                    // A mirror was already admitted, so a capability change
4019                    // cannot safely switch this request to the stale CPU
4020                    // cache.  Keep the same terminal contract as a failed
4021                    // token graph.
4022                    tracing::error!("mtp graph became unavailable after admission");
4023                    self.clear_sequence_state();
4024                    self.graph_failed
4025                        .store(true, std::sync::atomic::Ordering::Relaxed);
4026                    self.cancel
4027                        .store(true, std::sync::atomic::Ordering::Relaxed);
4028                    return (Vec::new(), Vec::new());
4029                }
4030                self.mtp_graph_mode = Some(false);
4031            } else {
4032                if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
4033                    self.mtp_graph_mode = Some(true);
4034                    return r;
4035                }
4036                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4037                    // A token graph can have admitted a persistent MTP/GDN
4038                    // mirror before its readback failed.  The CPU MTP cache
4039                    // is not a valid continuation in that state; leave the
4040                    // flag set so the generation caller returns through its
4041                    // terminal error path instead of silently switching
4042                    // arithmetic.
4043                    return (Vec::new(), Vec::new());
4044                }
4045                // `mtp_graph_ok` was true, so a None here means a refusal or
4046                // failure after graph admission.  Do not fall through to a
4047                // CPU cache whose rows may lag the device mirror.
4048                tracing::error!("mtp graph failed or declined after admission");
4049                self.clear_sequence_state();
4050                self.graph_failed
4051                    .store(true, std::sync::atomic::Ordering::Relaxed);
4052                self.cancel
4053                    .store(true, std::sync::atomic::Ordering::Relaxed);
4054                return (Vec::new(), Vec::new());
4055            }
4056        }
4057        // fc concat order is [enorm(embed); hnorm(hidden)] — EMBEDDING
4058        // FIRST. Verified by the oracle (converter/mtp_oracle.py):
4059        // [emb;hid] → 45.8% acceptance, [hid;emb] → 0.00%.
4060        let e = self.embed_single(next_token);
4061        let mut cat = vec![0.0f32; 2 * self.hidden_size];
4062        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4063        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4064        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4065        let mut x = vec![0.0f32; self.hidden_size];
4066        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4067
4068        // One standard transformer block over the MTP's own cache.
4069        let lw = &m.layer;
4070        inference::rms_norm_into(
4071            &x,
4072            &lw.input_norm,
4073            self.rms_eps,
4074            self.norm_style,
4075            &mut self.ws.n1,
4076        );
4077        let attn = match &lw.attn {
4078            // MLA models carry no MTP head; this path cannot see them.
4079            AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4080            AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4081            AttnKind::Full {
4082                wq,
4083                wk,
4084                wv,
4085                wo,
4086                q_norm,
4087                k_norm,
4088                output_gate,
4089                softplus_gate,
4090                bias,
4091            } => {
4092                let mut cfg = self.attn_cfg(position);
4093                cfg.q_norm = q_norm.as_deref();
4094                cfg.k_norm = k_norm.as_deref();
4095                cfg.output_gate = *output_gate;
4096                cfg.softplus_gate = softplus_gate
4097                    .as_ref()
4098                    .map(|(gate, per_head)| (gate, *per_head));
4099                cfg.bias = bias
4100                    .as_ref()
4101                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4102                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4103            }
4104            AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
4105                unreachable!("MTP block is full attention")
4106            }
4107        };
4108        for (i, &a) in attn.iter().enumerate() {
4109            x[i] += a;
4110        }
4111        inference::rms_norm_into(
4112            &x,
4113            &lw.post_norm,
4114            self.rms_eps,
4115            self.norm_style,
4116            &mut self.ws.p1,
4117        );
4118        let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
4119        for (i, &f) in ffn.iter().enumerate() {
4120            x[i] += f;
4121        }
4122
4123        inference::rms_norm_into(
4124            &x,
4125            &m.final_norm,
4126            self.rms_eps,
4127            self.norm_style,
4128            &mut self.ws.n1,
4129        );
4130        let lg = self.lm_head_forward(&self.ws.n1);
4131        (lg, x)
4132    }
4133
4134    /// `mtp_step_hl` reduced to the greedy draft: argmax of the head.
4135    fn mtp_step_h(
4136        &mut self,
4137        m: &mut MtpModule,
4138        hidden: &[f32],
4139        next_token: u32,
4140        position: usize,
4141    ) -> (u32, Vec<f32>) {
4142        let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
4143        let draft = sampler::argmax(&lg);
4144        attention::recycle_buf(&mut lg);
4145        (draft, x)
4146    }
4147
4148    /// One speculative round for the trial: rounds 1..5 of a `Spec` phase
4149    /// advance it (the monitor already averaged this round); after five,
4150    /// the plain phase runs (once — a known plain rate decides at once);
4151    /// a decided speculation keeps re-checking the rule every round and
4152    /// stops after four losing rounds in a row.
4153    fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
4154        match trial {
4155            SpecTrial::Spec { t0, gen0, rounds } => {
4156                let rounds = rounds + 1;
4157                if rounds >= 5 {
4158                    if mon.plain_ms > 0.0 {
4159                        let keep = mon.pays();
4160                        mon.fails = 0;
4161                        tracing::info!(
4162                            "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4163                            mon.tokens,
4164                            mon.round_ms,
4165                            mon.plain_ms,
4166                            if keep { "speculating" } else { "plain" }
4167                        );
4168                        SpecTrial::Decided {
4169                            spec: keep,
4170                            recheck_at: if keep { usize::MAX } else { generated + 128 },
4171                        }
4172                    } else {
4173                        SpecTrial::Plain {
4174                            t0: std::time::Instant::now(),
4175                            gen0: generated,
4176                        }
4177                    }
4178                } else {
4179                    SpecTrial::Spec { t0, gen0, rounds }
4180                }
4181            }
4182            SpecTrial::Decided { spec: true, .. } => {
4183                if mon.pays() {
4184                    mon.fails = 0;
4185                    trial
4186                } else {
4187                    mon.fails += 1;
4188                    if mon.fails >= 4 {
4189                        tracing::info!(
4190                            "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
4191                            mon.tokens,
4192                            mon.round_ms,
4193                            mon.plain_ms
4194                        );
4195                        SpecTrial::Decided {
4196                            spec: false,
4197                            recheck_at: generated + 128,
4198                        }
4199                    } else {
4200                        trial
4201                    }
4202                }
4203            }
4204            other => other,
4205        }
4206    }
4207
4208    /// The MTP block's device-mirror id: the trunk's id with a high bit,
4209    /// so the (kv_id, layer) mirror keys never collide.
4210    fn mtp_kv_id(&self) -> u64 {
4211        self.graph_kv_id | (1u64 << 40)
4212    }
4213
4214    /// The MTP block's mirror layer index: 0 — its own kv_id keeps it
4215    /// apart from the trunk, and the BATCH graph (the warm-up path) keys
4216    /// its mirrors at layer 0 with no base of its own, so the draft's
4217    /// token graph must key the same slot.
4218    const MTP_LAYER_BASE: usize = 0;
4219
4220    /// The wgpu MTP draft writes speculative rows straight into its device
4221    /// mirror while the CPU owner retains only the real prompt/decode anchor.
4222    /// After verification, move that mirror cursor back to the anchor before
4223    /// replaying accepted pairs.  The next graph append then sees the same
4224    /// contiguous position as the CPU/Metal path without uploading stale
4225    /// speculative rows.
4226    #[cfg(feature = "gpu")]
4227    fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
4228        self.mtp_graph_mode != Some(true)
4229            || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
4230    }
4231
4232    /// A speculative verify graph appends the full `k+1` trunk rows before
4233    /// the acceptance count is known.  GDN state already has a snapshot
4234    /// restore; Full-attention mirrors need the matching logical cursor
4235    /// rewind so the next graph call does not reject an ahead-of-position KV
4236    /// cache after a partial acceptance.
4237    #[cfg(feature = "gpu")]
4238    fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
4239        let mut ok = true;
4240        let mut expected = false;
4241        for li in 0..self.num_layers {
4242            if matches!(
4243                self.weights.layers[self.phys_layer(li)].attn,
4244                AttnKind::Full { .. }
4245            ) {
4246                expected = true;
4247                ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
4248            }
4249        }
4250        !expected || ok
4251    }
4252
4253    /// Count the recurrent layers participating in the trunk verify graph.
4254    /// Snapshot restore is all-or-nothing across that set; deriving the count
4255    /// from the model keeps the restore contract valid for looped models too.
4256    fn graph_gdn_layer_count(&self) -> usize {
4257        (0..self.num_layers)
4258            .filter(|&li| {
4259                matches!(
4260                    &self.weights.layers[self.phys_layer(li)].attn,
4261                    AttnKind::LinearGdn(_)
4262                )
4263            })
4264            .count()
4265    }
4266
4267    /// The block's input from (trunk hidden, token): eh_proj · [enorm(e);
4268    /// hnorm(h)] — the same arithmetic the per-op path starts with.
4269    fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
4270        let e = self.embed_single(next_token);
4271        let mut cat = vec![0.0f32; 2 * self.hidden_size];
4272        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4273        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4274        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4275        let mut x = vec![0.0f32; self.hidden_size];
4276        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4277        x
4278    }
4279
4280    /// Is the MTP block graphable at all (device up, full attention
4281    /// without softplus, dense FFN)? The plan itself is built per call.
4282    #[cfg(feature = "gpu")]
4283    fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
4284        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
4285            return false;
4286        }
4287        if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
4288            || !crate::gpu::enabled_here()
4289            || self.attn_softcap > 0.0
4290            || self.attention_heads_per_layer.is_some()
4291        {
4292            return false;
4293        }
4294        matches!(
4295            &m.layer.attn,
4296            AttnKind::Full {
4297                softplus_gate: None,
4298                ..
4299            }
4300        ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
4301    }
4302
4303    /// Full MTP token-graph eligibility, including the fused lm-head and all
4304    /// block projection weights.  Keep this distinct from the block-only
4305    /// check: prompt warm-up does not need the head, while a draft step does.
4306    #[cfg(feature = "gpu")]
4307    fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
4308        if !self.mtp_block_graph_ok(m) {
4309            return false;
4310        }
4311        let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
4312            return false;
4313        };
4314        let FfnKind::Dense(d) = &m.layer.ffn else {
4315            return false;
4316        };
4317        d.segs.is_empty()
4318            && wq.graph_weight().is_some()
4319            && wk.graph_weight().is_some()
4320            && wv.graph_weight().is_some()
4321            && wo.graph_weight().is_some()
4322            && d.gate_proj.graph_weight().is_some()
4323            && d.up_proj.graph_weight().is_some()
4324            && d.down_proj.graph_weight().is_some()
4325            && self.weights.lm_head.graph_weight().is_some()
4326    }
4327
4328    /// One MTP block step on the wgpu token graph: block + fused head in
4329    /// one submit, the block hidden and the logits read back together.
4330    /// None = the graph cannot take this block (softplus gate, non-dense
4331    /// FFN, unquantized head, no device) — the caller keeps the per-op
4332    /// path for the whole generation.
4333    #[cfg(feature = "gpu")]
4334    fn mtp_step_graph(
4335        &mut self,
4336        m: &mut MtpModule,
4337        hidden: &[f32],
4338        next_token: u32,
4339        position: usize,
4340    ) -> Option<(Vec<f32>, Vec<f32>)> {
4341        if !self.mtp_graph_ok(m) {
4342            return None;
4343        }
4344        let lw = &m.layer;
4345        let AttnKind::Full {
4346            wq,
4347            wk,
4348            wv,
4349            wo,
4350            q_norm,
4351            k_norm,
4352            output_gate,
4353            softplus_gate,
4354            bias,
4355        } = &lw.attn
4356        else {
4357            return None;
4358        };
4359        if softplus_gate.is_some() {
4360            return None;
4361        }
4362        let FfnKind::Dense(d) = &lw.ffn else {
4363            return None;
4364        };
4365        if !d.segs.is_empty() {
4366            return None; // tube layers run on the segmented path
4367        }
4368        // The block's input first: it borrows `self` mutably (embed scratch,
4369        // pool), the plan below borrows the weights immutably.
4370        let mut x = self.mtp_block_input(m, hidden, next_token);
4371        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4372            let (_, i, kind, rs) = t.graph_weight()?;
4373            Some(crate::gpu::GraphW {
4374                idx: i,
4375                kind,
4376                row_scale: rs,
4377                data: &[],
4378                prism: crate::gpu::GraphPrismOp::None,
4379                affine: false,
4380            })
4381        }
4382        let (model, _, _, _) = wq.graph_weight()?;
4383        let model = model.clone();
4384        let (lm_gw, lm_rows) = {
4385            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4386            (
4387                crate::gpu::GraphW {
4388                    idx: i,
4389                    kind,
4390                    row_scale: rs,
4391                    data: &[],
4392                    prism: crate::gpu::GraphPrismOp::None,
4393                    affine: false,
4394                },
4395                self.weights.lm_head.rows(),
4396            )
4397        };
4398        let layer = crate::gpu::GraphLayer {
4399            input_norm: &lw.input_norm,
4400            attn: crate::gpu::GraphAttn::Full {
4401                wq: gw(wq)?,
4402                wk: gw(wk)?,
4403                wv: gw(wv)?,
4404                wo: gw(wo)?,
4405                q_norm: q_norm.as_deref(),
4406                k_norm: k_norm.as_deref(),
4407                late_qk_norm: self.qk_norm_after_rope,
4408                bias: bias
4409                    .as_ref()
4410                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4411                output_gate: *output_gate,
4412                cpu_k: m.kv.k_heads(),
4413                cpu_v: m.kv.v_heads(),
4414            },
4415            post_norm: &lw.post_norm,
4416            ffn: crate::gpu::GraphFfn::Dense {
4417                gate: gw(&d.gate_proj)?,
4418                up: gw(&d.up_proj)?,
4419                down: gw(&d.down_proj)?,
4420            },
4421        };
4422        let nh = self.num_heads;
4423        let (nkv, hd, rd) = self.layer_geom(0);
4424        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4425        let mut logits = Vec::new();
4426        let ok = crate::gpu::forward_token_graph(
4427            &model,
4428            self.mtp_kv_id(),
4429            std::slice::from_ref(&layer),
4430            &[None],
4431            self.o1_epoch,
4432            &self.inv_freq,
4433            &mut x,
4434            nh,
4435            nkv,
4436            hd,
4437            self.attn_scale,
4438            rd,
4439            self.hidden_size,
4440            self.intermediate_size,
4441            position,
4442            self.kv_cache.max_seq_len,
4443            gemma,
4444            self.rms_eps as f32,
4445            Some((&lm_gw, lm_rows)),
4446            &m.final_norm,
4447            &mut logits,
4448            &[],
4449            1,
4450            None,
4451            None,
4452            None,
4453            Self::MTP_LAYER_BASE,
4454            true,
4455        );
4456        match ok {
4457            crate::gpu::TokenGraphOutcome::Completed => {}
4458            crate::gpu::TokenGraphOutcome::Declined => return None,
4459            crate::gpu::TokenGraphOutcome::Failed => {
4460                // The backend has already admitted persistent state.  Keep
4461                // this distinct from a capability refusal so the caller
4462                // cannot switch to the stale CPU MTP cache.
4463                self.clear_sequence_state();
4464                self.graph_failed
4465                    .store(true, std::sync::atomic::Ordering::Relaxed);
4466                self.cancel
4467                    .store(true, std::sync::atomic::Ordering::Relaxed);
4468                return None;
4469            }
4470        }
4471        logits.resize(self.vocab_size, 0.0);
4472        Some((logits, x))
4473    }
4474
4475    /// The warm-ups of one speculative round on the device: every accepted
4476    /// (hidden, token) pair as ONE batched graph run over the MTP block
4477    /// (no head) — its kv_append lands the pairs in the block's mirror.
4478    /// `pairs` are consecutive positions from `first_pos`.  The tri-state
4479    /// result is intentional: a refusal before admission may use the
4480    /// per-row/CPU route, while a failure after admission must terminate the
4481    /// sequence rather than fall through to a stale CPU cache.
4482    #[cfg(feature = "gpu")]
4483    fn mtp_warm_graph(
4484        &mut self,
4485        m: &mut MtpModule,
4486        pairs: &[(&[f32], u32)],
4487        first_pos: usize,
4488    ) -> crate::gpu::BatchGraphOutcome {
4489        if pairs.is_empty() {
4490            return crate::gpu::BatchGraphOutcome::Completed;
4491        }
4492        if !self.mtp_block_graph_ok(m) {
4493            return crate::gpu::BatchGraphOutcome::Declined;
4494        }
4495        let hs = self.hidden_size;
4496        // Block inputs for every pair (eh_proj on the per-op path, one
4497        // matvec each — the plan's own prologue).
4498        let mut hiddens = Vec::with_capacity(pairs.len() * hs);
4499        for (h, t) in pairs {
4500            hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
4501        }
4502        let lw = &m.layer;
4503        let AttnKind::Full {
4504            wq,
4505            wk,
4506            wv,
4507            wo,
4508            q_norm,
4509            k_norm,
4510            output_gate,
4511            bias,
4512            ..
4513        } = &lw.attn
4514        else {
4515            return crate::gpu::BatchGraphOutcome::Declined;
4516        };
4517        let FfnKind::Dense(d) = &lw.ffn else {
4518            return crate::gpu::BatchGraphOutcome::Declined;
4519        };
4520        if !d.segs.is_empty() {
4521            return crate::gpu::BatchGraphOutcome::Declined; // tube layers run on the segmented path
4522        }
4523        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4524            let (_, i, kind, rs) = t.graph_weight()?;
4525            Some(crate::gpu::GraphW {
4526                idx: i,
4527                kind,
4528                row_scale: rs,
4529                data: &[],
4530                prism: crate::gpu::GraphPrismOp::None,
4531                affine: false,
4532            })
4533        }
4534        let Some((model, _, _, _)) = wq.graph_weight() else {
4535            return crate::gpu::BatchGraphOutcome::Declined;
4536        };
4537        let model = model.clone();
4538        let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
4539            gw(wq),
4540            gw(wk),
4541            gw(wv),
4542            gw(wo),
4543            gw(&d.gate_proj),
4544            gw(&d.up_proj),
4545            gw(&d.down_proj),
4546        ) else {
4547            return crate::gpu::BatchGraphOutcome::Declined;
4548        };
4549        let layer = crate::gpu::GraphLayer {
4550            input_norm: &lw.input_norm,
4551            attn: crate::gpu::GraphAttn::Full {
4552                wq: gwq,
4553                wk: gwk,
4554                wv: gwv,
4555                wo: gwo,
4556                q_norm: q_norm.as_deref(),
4557                k_norm: k_norm.as_deref(),
4558                late_qk_norm: self.qk_norm_after_rope,
4559                bias: bias
4560                    .as_ref()
4561                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4562                output_gate: *output_gate,
4563                cpu_k: m.kv.k_heads(),
4564                cpu_v: m.kv.v_heads(),
4565            },
4566            post_norm: &lw.post_norm,
4567            ffn: crate::gpu::GraphFfn::Dense {
4568                gate: gg,
4569                up: gu,
4570                down: gd,
4571            },
4572        };
4573        let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
4574        let nh = self.num_heads;
4575        let (nkv, hd, rd) = self.layer_geom(0);
4576        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4577        crate::gpu::forward_batch_graph(
4578            &model,
4579            self.mtp_kv_id(),
4580            std::slice::from_ref(&layer),
4581            &self.inv_freq,
4582            &mut hiddens,
4583            nh,
4584            nkv,
4585            hd,
4586            rd,
4587            hs,
4588            self.intermediate_size,
4589            &positions,
4590            self.kv_cache.max_seq_len,
4591            gemma,
4592            self.rms_eps as f32,
4593            self.attn_scale,
4594            pairs.len(),
4595            &[],
4596            0,
4597            None,
4598        )
4599    }
4600
4601    /// Complete an MTP warm-up after the batched graph has refused.  A
4602    /// graphable block is retried one row at a time; once any device row has
4603    /// been admitted, a CPU fallback would observe a stale mirror, so every
4604    /// token-graph refusal is terminal.  If the block is not graphable and no
4605    /// mirror exists yet, warming on the CPU is safe and records the CPU mode
4606    /// for the rest of the generation.
4607    #[cfg(feature = "gpu")]
4608    fn mtp_warm_graph_fallback(
4609        &mut self,
4610        m: &mut MtpModule,
4611        pairs: &[(&[f32], u32)],
4612        first_pos: usize,
4613    ) -> bool {
4614        if pairs.is_empty() {
4615            return true;
4616        }
4617        let graphable = self.mtp_block_graph_ok(m);
4618        if !graphable {
4619            // A previously admitted mirror cannot be made coherent by
4620            // appending to the host cache.  The caller turns this into a
4621            // terminal generation error and clears both mirrors.
4622            if self.mtp_graph_mode == Some(true) {
4623                return false;
4624            }
4625            self.mtp_graph_mode = Some(false);
4626            for (j, (h, t)) in pairs.iter().enumerate() {
4627                self.mtp_warm(m, h, *t, first_pos + j);
4628            }
4629            return true;
4630        }
4631
4632        // The batch refusal is recoverable only through the same device
4633        // state.  Keep rows owned until each token graph has completed; a
4634        // None is treated as unsafe because the token-graph API deliberately
4635        // collapses its backend refusal/failure into that result.
4636        for (j, (h, t)) in pairs.iter().enumerate() {
4637            if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
4638                return false;
4639            }
4640        }
4641        self.mtp_graph_mode = Some(true);
4642        true
4643    }
4644
4645    /// Warm a contiguous set of MTP pairs using the existing graph seam, with
4646    /// an all-or-nothing error contract for callers that already admitted the
4647    /// trunk batch.  The non-GPU build keeps the same pair accounting while
4648    /// using the established CPU warm path.
4649    #[cfg(feature = "gpu")]
4650    fn mtp_warm_prefill_pairs(
4651        &mut self,
4652        m: &mut MtpModule,
4653        pairs: &[(&[f32], u32)],
4654        first_pos: usize,
4655    ) -> Result<(), &'static str> {
4656        // Keep unsupported token-graph heads on the established CPU MTP
4657        // route before admitting any block mirror.  Once a device mirror is
4658        // active, the same condition is terminal because CPU rows cannot
4659        // repair its state.
4660        if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
4661            if self.mtp_graph_mode == Some(true) {
4662                return Err("MTP token graph became unavailable after admission");
4663            }
4664            self.mtp_graph_mode = Some(false);
4665            for (j, (h, t)) in pairs.iter().enumerate() {
4666                self.mtp_warm(m, h, *t, first_pos + j);
4667            }
4668            return Ok(());
4669        }
4670        match self.mtp_warm_graph(m, pairs, first_pos) {
4671            crate::gpu::BatchGraphOutcome::Completed => {
4672                if !pairs.is_empty() {
4673                    self.mtp_graph_mode = Some(true);
4674                }
4675                Ok(())
4676            }
4677            crate::gpu::BatchGraphOutcome::Declined => {
4678                if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
4679                    Ok(())
4680                } else {
4681                    Err("MTP warm-up fallback failed after device admission")
4682                }
4683            }
4684            crate::gpu::BatchGraphOutcome::Failed => {
4685                Err("MTP warm batch graph failed after admission")
4686            }
4687        }
4688    }
4689
4690    #[cfg(not(feature = "gpu"))]
4691    fn mtp_warm_prefill_pairs(
4692        &mut self,
4693        m: &mut MtpModule,
4694        pairs: &[(&[f32], u32)],
4695        first_pos: usize,
4696    ) -> Result<(), &'static str> {
4697        for (j, (h, t)) in pairs.iter().enumerate() {
4698            self.mtp_warm(m, h, *t, first_pos + j);
4699        }
4700        Ok(())
4701    }
4702
4703    /// The MTP block alone — advance its KV with a (hidden, token) pair the
4704    /// verify just proved, without paying the head. What keeps the draft's
4705    /// attention context warm between speculative rounds.
4706    fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
4707        let e = self.embed_single(next_token);
4708        let mut cat = vec![0.0f32; 2 * self.hidden_size];
4709        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4710        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4711        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4712        let mut x = vec![0.0f32; self.hidden_size];
4713        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4714        inference::rms_norm_into(
4715            &x,
4716            &m.layer.input_norm,
4717            self.rms_eps,
4718            self.norm_style,
4719            &mut self.ws.n1,
4720        );
4721        let attn = match &m.layer.attn {
4722            AttnKind::Full {
4723                wq,
4724                wk,
4725                wv,
4726                wo,
4727                q_norm,
4728                k_norm,
4729                output_gate,
4730                softplus_gate,
4731                bias,
4732            } => {
4733                let mut cfg = self.attn_cfg(position);
4734                cfg.q_norm = q_norm.as_deref();
4735                cfg.k_norm = k_norm.as_deref();
4736                cfg.output_gate = *output_gate;
4737                cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
4738                cfg.bias = bias
4739                    .as_ref()
4740                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4741                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4742            }
4743            _ => return,
4744        };
4745        let _ = attn;
4746    }
4747
4748    /// Speculative decode ON the wgpu whole-token graph: draft k with the
4749    /// MTP head, verify all of them plus the tip in ONE batched graph
4750    /// submit whose tail folds the head, commit the accepted prefix and
4751    /// roll the GDN state back to the last real position. Greedy only —
4752    /// output equals the plain graph's token for token, the way the DSV4
4753    /// verify equals the walk.
4754    #[cfg(feature = "gpu")]
4755    #[allow(clippy::too_many_arguments)]
4756    fn graph_spec_step(
4757        &mut self,
4758        m: &mut MtpModule,
4759        hidden: &[f32],
4760        t_next: u32,
4761        next_pos: usize,
4762        drafted: &mut usize,
4763        accepted: &mut usize,
4764        // The committed stream (prompt + generated so far, `t_next`
4765        // included): the sampler chain's penalties read it, and the
4766        // sampling arm extends it with the drafts position by position.
4767        all_ids: &mut Vec<u32>,
4768    ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
4769        // 3 is the measured optimum on Qwen3.6-27B / RTX 5090 (medians
4770        // of three, greedy): 51.1 tok/s against a plain 49.4, where k=2
4771        // gives 46.1, k=4 50.0, k=5 47.4, k=6 45.2. Acceptance is 89-91%
4772        // throughout — what turns the curve over is the verify, which
4773        // costs ~7.4 ms per extra position, and the draft ~3 ms a step.
4774        // 4 since the draft moved onto the graph (Qwen3.8-27B / 5090:
4775        // k=3 51.2, k=4 51.8 with the per-op draft; the graph draft
4776        // halves the draft cost, so the extra draft is cheaper still).
4777        // 5 with the int8 verify (the default: measured 76.5 against
4778        // k=4's 72-74 and k=6's 74 on the 5090), 4 with the f32 one.
4779        #[cfg(target_os = "macos")]
4780        let metal_native = crate::gpu::q1_force();
4781        #[cfg(not(target_os = "macos"))]
4782        let metal_native = false;
4783        #[cfg(feature = "gpu")]
4784        let k_default = if metal_native {
4785            // the Metal verify's GEMM tile is 8 rows wide and flat in b:
4786            // seven drafts + the tip fill it for free
4787            7
4788        } else if crate::gpu_wgpu::verify_i8_on() {
4789            5
4790        } else {
4791            4
4792        };
4793        #[cfg(not(feature = "gpu"))]
4794        let k_default = 4;
4795        let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
4796            .ok()
4797            .and_then(|v| v.parse().ok())
4798            .filter(|&v| (1..=8).contains(&v))
4799            .unwrap_or(k_default);
4800        if next_pos == 0 {
4801            return None;
4802        }
4803        let t_round = std::time::Instant::now();
4804        // Submissions per phase — and they say where the round's money is.
4805        // Qwen3.6-27B on an RTX 5090, k=3:
4806        //
4807        //   draft   9.3 ms / 12 submissions   (four per MTP step)
4808        //   verify 52.8 ms /  1               (the batched graph)
4809        //   commit  5.4 ms /  6               (two per warm)
4810        //
4811        // The verify is already one submit. The draft's own work is 834 MB
4812        // a step — 0.8 ms at this card's measured 1056 GB/s — against 3.1
4813        // ms measured, so ~0.58 ms of every step is round trip, not
4814        // arithmetic, and the same holds for the warms. Eighteen round
4815        // trips a round at roughly half a millisecond each is ~11 ms of a
4816        // 68 ms round: fusing the MTP block into ONE submit the way the
4817        // trunk already is projects to ~64 tok/s against today's 50.9.
4818        // That is the largest measured item left on this path.
4819        let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
4820        let sub0 = subs();
4821        // Greedy without penalties verifies by argmax equality (bit-exact
4822        // against the plain path). Anything else is speculative SAMPLING:
4823        // each draft is a DRAW from the MTP head's post-chain distribution
4824        // q_j, kept for the accept test; the verify's rows give p_j.
4825        let cfg = self.sampler_config.clone();
4826        let penalized = !(cfg.repetition_penalty == 1.0
4827            && cfg.presence_penalty == 0.0
4828            && cfg.suppress_tokens.is_empty());
4829        // Three verify regimes: plain greedy (argmax of the raw rows),
4830        // greedy WITH penalties (argmax of the penalized rows — a single
4831        // pass each, no distributions), and sampling (draw / accept /
4832        // correct on post-chain distributions).
4833        let greedy_pen = cfg.temperature < 1e-6 && penalized;
4834        let sampling = cfg.temperature >= 1e-6;
4835        // Sampling with a top-k goes through the SPARSE chain: the dense
4836        // one builds nine 248k-float distributions a round (four drafts,
4837        // five verify rows) and measured 19-22 tok/s against a plain 40 —
4838        // the host, not the card. Sparse, the same nine cost tens of
4839        // microseconds each.
4840        let sparse = sampling && sampler::sparse_ok(&cfg);
4841        let base_len = all_ids.len();
4842        if sampling && !sparse && self.spec_q.len() < k_spec {
4843            self.spec_q.resize_with(k_spec, Vec::new);
4844        }
4845        if sparse && self.spec_qs.len() < k_spec {
4846            self.spec_qs.resize_with(k_spec, Vec::new);
4847        }
4848        // Draft the chain: first from the trunk's tip hidden, then the head
4849        // iterating on itself. Rows land in the MTP KV; the chain rows past
4850        // the first are speculation over speculative state and roll back
4851        // below, replaced by verified pairs.
4852        let mut drafts = Vec::with_capacity(k_spec);
4853        let mut hx = hidden.to_vec();
4854        // CMF_SPEC_DBG=1: draft 0 through BOTH MTP arms (graph and per-op)
4855        // from the same inputs — are the arms the difference, or the inputs?
4856        let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
4857        for j in 0..k_spec {
4858            let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
4859            let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
4860            if spec_dbg {
4861                let saved = self.mtp_graph_mode;
4862                self.mtp_graph_mode = Some(false);
4863                let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4864                self.mtp_graph_mode = saved;
4865                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4866                    return None;
4867                }
4868                m.kv.truncate_last(1);
4869                dbg_ref = Some(r);
4870            }
4871            let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4872            if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4873                return None;
4874            }
4875            if let Some((lg_cpu, h_cpu)) = dbg_ref {
4876                let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
4877                let dl = lg
4878                    .iter()
4879                    .zip(&lg_cpu)
4880                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4881                let dh = hj
4882                    .iter()
4883                    .zip(&h_cpu)
4884                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4885                eprintln!(
4886                    "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 {}",
4887                    next_pos - 1 + j,
4888                    sampler::argmax(&lg_cpu),
4889                    sampler::argmax(&lg),
4890                    n(&h_cpu),
4891                    n(&hj),
4892                    m.kv.seq_len
4893                );
4894            }
4895            let dj = if sparse {
4896                let mut q = std::mem::take(&mut self.spec_qs[j]);
4897                let ok = sampler::sparse_distribution_into(
4898                    &lg,
4899                    &cfg,
4900                    all_ids,
4901                    &mut self.sampler_scratch,
4902                    self.pool.as_deref(),
4903                    &mut q,
4904                );
4905                let d = if ok {
4906                    sampler::draw_sparse(&q, &mut self.rng)
4907                } else {
4908                    // everything filtered: the dense chain's greedy fallback
4909                    let t = sampler::argmax(&lg);
4910                    q.clear();
4911                    q.push((t, 1.0));
4912                    t
4913                };
4914                self.spec_qs[j] = q;
4915                all_ids.push(d);
4916                d
4917            } else if sampling {
4918                let mut q = std::mem::take(&mut self.spec_q[j]);
4919                sampler::distribution_into(
4920                    &lg,
4921                    &cfg,
4922                    all_ids,
4923                    &mut self.sampler_scratch,
4924                    self.pool.as_deref(),
4925                    &mut q,
4926                );
4927                let d = sampler::draw(&q, &mut self.rng);
4928                self.spec_q[j] = q;
4929                all_ids.push(d); // the next draft's penalties see this one
4930                d
4931            } else if greedy_pen {
4932                let d = sampler::argmax_penalized(
4933                    &lg,
4934                    &cfg,
4935                    all_ids,
4936                    &mut self.sampler_scratch,
4937                    self.pool.as_deref(),
4938                );
4939                all_ids.push(d);
4940                d
4941            } else {
4942                sampler::argmax(&lg)
4943            };
4944            attention::recycle_buf(&mut lg);
4945            drafts.push(dj);
4946            hx = hj;
4947        }
4948        all_ids.truncate(base_len);
4949        *drafted += k_spec;
4950        let t_draft = t_round.elapsed();
4951        let sub_draft = subs();
4952        // Verify batch: [t_next, d1 .. d_{k-1}] at next_pos.. — every row's
4953        // logits come back from the graph's own head.
4954        let b = k_spec + 1;
4955        let mut hiddens = vec![0.0f32; b * self.hidden_size];
4956        for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
4957            let e = self.embed_single(t);
4958            hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
4959        }
4960        let positions: Vec<usize> = (next_pos..next_pos + b).collect();
4961        let (lm_gw, lm_rows) = {
4962            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4963            (
4964                crate::gpu::GraphW {
4965                    idx: i,
4966                    kind,
4967                    row_scale: rs,
4968                    data: &[],
4969                    prism: crate::gpu::GraphPrismOp::None,
4970                    affine: false,
4971                },
4972                self.weights.lm_head.rows(),
4973            )
4974        };
4975        let mut logits = Vec::new();
4976        let final_norm = self.weights.final_norm.clone();
4977        #[cfg(target_os = "macos")]
4978        let verify_outcome = if metal_native {
4979            let lm = self.weights.lm_head.q1_parts()?;
4980            self.try_batch_graph_metal(
4981                &mut hiddens,
4982                &positions,
4983                b,
4984                Some((lm, &final_norm, &mut logits)),
4985            )
4986        } else {
4987            self.try_batch_graph_wgpu(
4988                &mut hiddens,
4989                &positions,
4990                b,
4991                Some(crate::gpu::SpecTail {
4992                    lm: lm_gw,
4993                    lm_rows,
4994                    final_norm: &final_norm,
4995                    logits_out: &mut logits,
4996                }),
4997            )
4998        };
4999        #[cfg(not(target_os = "macos"))]
5000        let verify_outcome = self.try_batch_graph_wgpu(
5001            &mut hiddens,
5002            &positions,
5003            b,
5004            Some(crate::gpu::SpecTail {
5005                lm: lm_gw,
5006                lm_rows,
5007                final_norm: &final_norm,
5008                logits_out: &mut logits,
5009            }),
5010        );
5011        match verify_outcome {
5012            crate::gpu::BatchGraphOutcome::Completed => {}
5013            crate::gpu::BatchGraphOutcome::Declined => {
5014                // The verifier refused before admission.  Its draft MTP
5015                // rows are still device-resident, so rewind the separate
5016                // mirror before the caller takes the exact one-token path.
5017                m.kv.truncate_last(k_spec);
5018                if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
5019                    self.clear_sequence_state();
5020                    self.graph_failed
5021                        .store(true, std::sync::atomic::Ordering::Relaxed);
5022                    self.cancel
5023                        .store(true, std::sync::atomic::Ordering::Relaxed);
5024                    tracing::error!("MTP graph mirror rewind failed after verify decline");
5025                }
5026                return None;
5027            }
5028            crate::gpu::BatchGraphOutcome::Failed => {
5029                // A failed batch may have advanced trunk/GDN state.  Clear
5030                // both mirrors and preserve the terminal outcome rather than
5031                // falling through to stale CPU state.
5032                self.clear_sequence_state();
5033                self.graph_failed
5034                    .store(true, std::sync::atomic::Ordering::Relaxed);
5035                self.cancel
5036                    .store(true, std::sync::atomic::Ordering::Relaxed);
5037                tracing::error!("MTP verify batch graph failed after admission");
5038                return None;
5039            }
5040        }
5041        // `CMF_METAL_VERIFY_CHECK=1`: run the same b tokens through the
5042        // plain per-token path and compare each row's argmax + logits with
5043        // the verify's — the bring-up oracle for the batched graph. The
5044        // plain forwards mutate the CPU state; it is snapshotted and put
5045        // back, and the K/V mirrors re-pointed, before the round goes on.
5046        #[cfg(target_os = "macos")]
5047        if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
5048            let snap: Vec<Vec<f32>> = self
5049                .kv_cache
5050                .layers
5051                .iter()
5052                .map(|l| l.linear_state.clone())
5053                .collect();
5054            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5055            let toks: Vec<u32> = std::iter::once(t_next)
5056                .chain(drafts.iter().copied())
5057                .collect();
5058            let want_save = self.graph_want_logits;
5059            self.graph_want_logits = false;
5060            for (i, &t) in toks.iter().enumerate() {
5061                let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5062                let _ = self.graph_logits.take();
5063                // CMF_SPEC_PLAIN_HIDDEN=1: the next round drafts from the
5064                // plain path's hidden instead of the verify's (an experiment
5065                // on the chain's sensitivity to the half-GEMM noise)
5066                if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
5067                    hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
5068                }
5069                let ref_lg = self.logits_from_hidden(&hi);
5070                let row = &logits[i * lm_rows..(i + 1) * lm_rows];
5071                let ra = sampler::argmax(&ref_lg);
5072                let va = sampler::argmax(row);
5073                let mut md = 0f32;
5074                let mut rms = 0f64;
5075                for j in 0..lm_rows.min(ref_lg.len()) {
5076                    let d = (ref_lg[j] - row[j]).abs();
5077                    md = md.max(d);
5078                    rms += (d as f64) * (d as f64);
5079                }
5080                let mut hd = 0f32;
5081                for j in 0..self.hidden_size {
5082                    hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
5083                }
5084                eprintln!(
5085                    "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
5086                    next_pos + i,
5087                    if ra == va { "OK" } else { "MISMATCH" },
5088                    (rms / lm_rows as f64).sqrt()
5089                );
5090            }
5091            self.graph_want_logits = want_save;
5092            // restore IN PLACE: the pending verify graph wraps these very
5093            // allocations (zero-copy) — replacing the Vec would strand it
5094            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5095                if l.linear_state.len() == st.len() {
5096                    l.linear_state.copy_from_slice(&st);
5097                } else {
5098                    l.linear_state = st;
5099                }
5100            }
5101            for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
5102                let extra = l.seq_len.saturating_sub(n0);
5103                if extra > 0 {
5104                    l.truncate_last(extra);
5105                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
5106                }
5107            }
5108        }
5109        let t_verify = t_round.elapsed();
5110        let sub_verify = subs();
5111        // Acceptance. Greedy: row i's argmax is the trunk's token after
5112        // input i. Sampling: accept draft i with min(1, p_i/q_i), and on
5113        // the first rejection draw the correction from max(0, p_i − q_i)
5114        // — that token is committed by the loop top as-is (spec_forced).
5115        let mut a = 0usize;
5116        let mut forced: Option<u32> = None;
5117        let ids: Vec<u32> = if sparse {
5118            let mut p = std::mem::take(&mut self.spec_ps);
5119            let mut res = std::mem::take(&mut self.spec_ress);
5120            while a < k_spec {
5121                let ok = sampler::sparse_distribution_into(
5122                    &logits[a * lm_rows..(a + 1) * lm_rows],
5123                    &cfg,
5124                    all_ids,
5125                    &mut self.sampler_scratch,
5126                    self.pool.as_deref(),
5127                    &mut p,
5128                );
5129                if !ok {
5130                    let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
5131                    p.clear();
5132                    p.push((t, 1.0));
5133                }
5134                match sampler::spec_accept_or_correct_sparse(
5135                    &p,
5136                    &self.spec_qs[a],
5137                    drafts[a],
5138                    &mut self.rng,
5139                    &mut res,
5140                ) {
5141                    None => {
5142                        all_ids.push(drafts[a]);
5143                        a += 1;
5144                    }
5145                    Some(c) => {
5146                        forced = Some(c);
5147                        break;
5148                    }
5149                }
5150            }
5151            all_ids.truncate(base_len);
5152            self.spec_ps = p;
5153            self.spec_ress = res;
5154            drafts.clone()
5155        } else if sampling {
5156            let mut p = std::mem::take(&mut self.spec_p);
5157            let mut res = std::mem::take(&mut self.spec_res);
5158            while a < k_spec {
5159                sampler::distribution_into(
5160                    &logits[a * lm_rows..(a + 1) * lm_rows],
5161                    &cfg,
5162                    all_ids,
5163                    &mut self.sampler_scratch,
5164                    self.pool.as_deref(),
5165                    &mut p,
5166                );
5167                match sampler::spec_accept_or_correct(
5168                    &p,
5169                    &self.spec_q[a],
5170                    drafts[a],
5171                    &mut self.rng,
5172                    &mut res,
5173                    self.pool.as_deref(),
5174                ) {
5175                    None => {
5176                        all_ids.push(drafts[a]);
5177                        a += 1;
5178                    }
5179                    Some(c) => {
5180                        forced = Some(c);
5181                        break;
5182                    }
5183                }
5184            }
5185            all_ids.truncate(base_len);
5186            self.spec_p = p;
5187            self.spec_res = res;
5188            // the accepted drafts ARE the verified tokens after inputs 0..a
5189            drafts.clone()
5190        } else if greedy_pen {
5191            // Row i's penalized argmax, penalties over the stream that
5192            // includes the accepted drafts before it — the plain loop's
5193            // exact arithmetic, one pass per row, no working copy.
5194            let mut ids: Vec<u32> = Vec::with_capacity(b);
5195            for i in 0..b {
5196                let t = sampler::argmax_penalized(
5197                    &logits[i * lm_rows..(i + 1) * lm_rows],
5198                    &cfg,
5199                    all_ids,
5200                    &mut self.sampler_scratch,
5201                    self.pool.as_deref(),
5202                );
5203                ids.push(t);
5204                if i < k_spec && t == drafts[i] {
5205                    all_ids.push(t);
5206                } else {
5207                    break;
5208                }
5209            }
5210            all_ids.truncate(base_len);
5211            while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
5212                a += 1;
5213            }
5214            // rows past the first mismatch were never scored; the loop
5215            // top re-samples the last verified row itself.
5216            ids
5217        } else {
5218            let ids: Vec<u32> = (0..b)
5219                .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
5220                .collect();
5221            while a < k_spec && ids[a] == drafts[a] {
5222                a += 1;
5223            }
5224            ids
5225        };
5226        if spec_dbg {
5227            eprintln!(
5228                "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
5229                drafts, ids
5230            );
5231        }
5232        // CMF_METAL_VERIFY_CHECK=2: the commit oracle — plain-forward the
5233        // a+1 accepted tokens from a snapshot, then diff the replayed GDN
5234        // states and the appended K/V rows against that.
5235        #[cfg(target_os = "macos")]
5236        let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
5237            && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
5238        {
5239            let snap: Vec<Vec<f32>> = self
5240                .kv_cache
5241                .layers
5242                .iter()
5243                .map(|l| l.linear_state.clone())
5244                .collect();
5245            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5246            let toks: Vec<u32> = std::iter::once(t_next)
5247                .chain(drafts.iter().copied())
5248                .collect();
5249            let want_save = self.graph_want_logits;
5250            self.graph_want_logits = false;
5251            for (i, &t) in toks.iter().take(a + 1).enumerate() {
5252                let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5253                let _ = self.graph_logits.take();
5254            }
5255            self.graph_want_logits = want_save;
5256            let plain_states: Vec<Vec<f32>> = self
5257                .kv_cache
5258                .layers
5259                .iter()
5260                .map(|l| l.linear_state.clone())
5261                .collect();
5262            let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5263            let mut rows = Vec::new();
5264            for (li, (l, n0)) in self
5265                .kv_cache
5266                .layers
5267                .iter_mut()
5268                .zip(attn_lens.iter())
5269                .enumerate()
5270            {
5271                let extra = l.seq_len.saturating_sub(*n0);
5272                if extra > 0 {
5273                    let mut kk = Vec::new();
5274                    let mut vv = Vec::new();
5275                    for g in 0..nkv {
5276                        kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5277                        vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5278                    }
5279                    rows.push((li, kk, vv));
5280                    l.truncate_last(extra);
5281                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
5282                }
5283            }
5284            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5285                if l.linear_state.len() == st.len() {
5286                    l.linear_state.copy_from_slice(&st);
5287                } else {
5288                    l.linear_state = st;
5289                }
5290            }
5291            Some((plain_states, rows))
5292        } else {
5293            None
5294        };
5295        // a fully-accepted round needs no restore: every input was real.
5296        #[cfg(target_os = "macos")]
5297        if metal_native {
5298            // the Metal verify never wrote its states: the commit replays the
5299            // accepted prefix into the CPU owners and appends the K/V rows
5300            if !self.metal_verify_commit(a) {
5301                self.clear_sequence_state();
5302                self.graph_failed
5303                    .store(true, std::sync::atomic::Ordering::Relaxed);
5304                self.cancel
5305                    .store(true, std::sync::atomic::Ordering::Relaxed);
5306                tracing::error!("Metal verify state/KV handoff failed after admission");
5307                return None;
5308            }
5309            if let Some((plain_states, rows)) = commit_ref {
5310                crate::gpu_metal::queue_fence();
5311                let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5312                let mut worst_s = 0f32;
5313                let mut worst_li = 0usize;
5314                for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
5315                    if l.linear_state.len() != ps.len() || ps.is_empty() {
5316                        continue;
5317                    }
5318                    let d = l
5319                        .linear_state
5320                        .iter()
5321                        .zip(ps)
5322                        .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5323                    let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
5324                    let rel = d / n.max(1e-6);
5325                    if rel > worst_s {
5326                        worst_s = rel;
5327                        worst_li = li;
5328                    }
5329                }
5330                let mut worst_k = 0f32;
5331                for (li, kk, vv) in &rows {
5332                    let l = &self.kv_cache.layers[*li];
5333                    let n0 = l.seq_len - (kk.len() / (nkv * hd));
5334                    let mut ck = Vec::new();
5335                    let mut cv = Vec::new();
5336                    for g in 0..nkv {
5337                        ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5338                        cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5339                    }
5340                    if ck.len() == kk.len() {
5341                        let dk = ck
5342                            .iter()
5343                            .zip(kk)
5344                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5345                        let dv = cv
5346                            .iter()
5347                            .zip(vv)
5348                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5349                        worst_k = worst_k.max(dk).max(dv);
5350                    } else {
5351                        eprintln!(
5352                            "commit-check L{li}: kv row count mismatch {} vs {}",
5353                            ck.len(),
5354                            kk.len()
5355                        );
5356                    }
5357                }
5358                eprintln!(
5359                    "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}"
5360                );
5361            }
5362        }
5363        if !metal_native && a + 1 < b {
5364            let expected_gdn_layers = self.graph_gdn_layer_count();
5365            if expected_gdn_layers > 0
5366                && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
5367            {
5368                self.clear_sequence_state();
5369                self.graph_failed
5370                    .store(true, std::sync::atomic::Ordering::Relaxed);
5371                self.cancel
5372                    .store(true, std::sync::atomic::Ordering::Relaxed);
5373                tracing::error!("GDN speculative restore failed after verify");
5374                return None;
5375            }
5376        }
5377        if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
5378            // The verify graph committed the full batch, but one of its
5379            // persistent Full-attention mirrors could not be re-pointed to
5380            // the accepted prefix.  Treat that as terminal state failure;
5381            // an exact CPU fallback would otherwise consume stale GDN/KV.
5382            self.clear_sequence_state();
5383            self.graph_failed
5384                .store(true, std::sync::atomic::Ordering::Relaxed);
5385            self.cancel
5386                .store(true, std::sync::atomic::Ordering::Relaxed);
5387            tracing::error!("trunk graph KV rewind failed after speculative verify");
5388            return None;
5389        }
5390        *accepted += a;
5391        // MTP cache: keep the first draft row (its inputs were real), drop
5392        // the chain's, then append the verified pairs the round produced.
5393        // Each of those is a whole MTP block on the per-op path and they
5394        // cost 5.8 ms of a 69 ms round at k=3 — a third of what the
5395        // round's own draft costs. PRICED, and they earn it: skipping
5396        // them (`CMF_SPEC_WARM=0`) drops acceptance from 89% to 81% at
5397        // k=3 and 85% to 74% at k=4, and the tok/s goes nowhere at k=3
5398        // (50.3 against 50.5) and backwards at k=4 (48.1 against 50.1).
5399        // The knob stays so the next person can re-price it after the
5400        // warms are batched instead of assuming either way.
5401        m.kv.truncate_last(k_spec.saturating_sub(1));
5402        #[cfg(target_os = "macos")]
5403        if metal_native && self.mtp_graph_mode == Some(true) {
5404            // the mirror rows below the cut are the CPU rows: re-point,
5405            // no re-upload
5406            crate::gpu_metal::kv_mirror_set_stored(
5407                self.mtp_kv_id(),
5408                Self::MTP_LAYER_BASE,
5409                m.kv.seq_len,
5410            );
5411        }
5412        if !metal_native
5413            && self.mtp_graph_mode == Some(true)
5414            && !self.rewind_mtp_graph_mirror(next_pos)
5415        {
5416            // The graph draft was admitted, so inability to move its cursor
5417            // back to the real anchor is a state failure, not a capability
5418            // refusal.  Do not warm or continue with a stale mirror.
5419            self.clear_sequence_state();
5420            self.graph_failed
5421                .store(true, std::sync::atomic::Ordering::Relaxed);
5422            self.cancel
5423                .store(true, std::sync::atomic::Ordering::Relaxed);
5424            tracing::error!("MTP graph mirror rewind failed after verify commit");
5425            return None;
5426        }
5427        let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
5428        if !warm_off && a > 0 {
5429            // Graph arm: all accepted pairs in ONE batched run over the
5430            // MTP block; the token graph one by one if the batch declines.
5431            let mut warmed = false;
5432            #[cfg(target_os = "macos")]
5433            if metal_native && self.mtp_graph_mode == Some(true) {
5434                // all accepted pairs in ONE b-row graph run over the MTP
5435                // block (its input projection folded in); one by one on
5436                // the token graph if that declines
5437                let pairs: Vec<(&[f32], u32)> = (0..a)
5438                    .map(|j| {
5439                        (
5440                            &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
5441                            ids[j],
5442                        )
5443                    })
5444                    .collect();
5445                warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
5446                if !warmed {
5447                    warmed = true;
5448                    for j in 0..a {
5449                        let row =
5450                            hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
5451                        if self
5452                            .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
5453                            .is_none()
5454                        {
5455                            warmed = false;
5456                            break;
5457                        }
5458                    }
5459                }
5460            }
5461            if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
5462                let rows: Vec<Vec<f32>> = (0..a)
5463                    .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
5464                    .collect();
5465                let pairs: Vec<(&[f32], u32)> = rows
5466                    .iter()
5467                    .zip(ids.iter())
5468                    .map(|(r, &t)| (r.as_slice(), t))
5469                    .collect();
5470                match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
5471                    Ok(()) => warmed = true,
5472                    Err(err) => {
5473                        // A warm-up failure after graph admission cannot
5474                        // fall back to `mtp_warm`: the detached CPU cache is
5475                        // not authoritative for the device mirror.  Mark it
5476                        // terminal so the generation caller clears state and
5477                        // returns instead of drafting from stale attention.
5478                        tracing::error!("{err}");
5479                        self.clear_sequence_state();
5480                        self.graph_failed
5481                            .store(true, std::sync::atomic::Ordering::Relaxed);
5482                        self.cancel
5483                            .store(true, std::sync::atomic::Ordering::Relaxed);
5484                        return None;
5485                    }
5486                }
5487            }
5488            if !warmed {
5489                for j in 0..a {
5490                    let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
5491                    let row = row.to_vec();
5492                    self.mtp_warm(m, &row, ids[j], next_pos + j);
5493                }
5494            }
5495        }
5496        // The sampler's contract: logits of the LAST verified position —
5497        // unless a rejected draft already drew the correction, in which
5498        // case the loop top commits that token and samples nothing.
5499        if let Some(c) = forced {
5500            self.spec_forced = Some(c);
5501            self.graph_logits = None;
5502        } else {
5503            let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
5504            row.resize(self.vocab_size, 0.0);
5505            if let Some(c) = self.final_softcap {
5506                for l in row.iter_mut() {
5507                    *l = c * (*l / c).tanh();
5508                }
5509            }
5510            self.graph_logits = Some(row);
5511        }
5512        let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
5513        // Three phases, not two. The round's wall clock was 4 ms longer
5514        // than draft+verify and the difference had nowhere to be seen:
5515        // the accepted prefix re-runs the MTP block once per token to
5516        // keep the draft head's attention cache warm, and the GDN state
5517        // rolls back on any rejection. Both live here, after the verify.
5518        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
5519            let end = subs();
5520            eprintln!(
5521                "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
5522                 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
5523                t_draft.as_secs_f64() * 1e3,
5524                sub_draft - sub0,
5525                (t_verify - t_draft).as_secs_f64() * 1e3,
5526                sub_verify - sub_draft,
5527                (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
5528                end - sub_verify,
5529            );
5530        }
5531        Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
5532    }
5533
5534    /// Micro-benchmark: two single-position forwards vs one fused pair
5535    /// from the current cache state (KV rewound after each probe).
5536    /// Returns (two_singles_ms, fused_pair_ms) per probe, or the (0, 0)
5537    /// sentinel when this model has no pair path to measure — the same
5538    /// answer the o1 arm gives, and the bench prints it the same way.
5539    /// (An architecture that loads its own layers leaves `weights.layers`
5540    /// empty; walking it here was an index panic, found by `bench` on
5541    /// deepseek_v4.)
5542    pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
5543        if !self.pair_supported() {
5544            return (0.0, 0.0);
5545        }
5546        // This is a host-side pair micro-benchmark. It truncates the host KV
5547        // after every probe, so letting the whole-token graph participate
5548        // would leave its device GDN/KV mirror ahead of the next probe and
5549        // poison the process-wide graph verdict before the real generation
5550        // benchmark starts. Keep the existing per-op/GPU arithmetic while
5551        // suppressing only the stateful token graph for this measurement.
5552        let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
5553        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5554        let emb1 = self.embed_single(1);
5555        let emb2 = self.embed_single(2);
5556        let pos = self.kv_cache.seq_len();
5557
5558        let t0 = std::time::Instant::now();
5559        for _ in 0..iters {
5560            let _ = self.forward_layers(&emb1, pos, None);
5561            let _ = self.forward_layers(&emb2, pos + 1, None);
5562            for l in &mut self.kv_cache.layers {
5563                l.truncate_last(2);
5564            }
5565        }
5566        let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5567
5568        let t1 = std::time::Instant::now();
5569        for _ in 0..iters {
5570            let _ = self.forward_pair(&emb1, &emb2, pos);
5571            for l in &mut self.kv_cache.layers {
5572                l.truncate_last(2);
5573            }
5574        }
5575        let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5576        match graph_env {
5577            Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
5578            None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
5579        }
5580        (singles_ms, pair_ms)
5581    }
5582
5583    /// Fused two-position forward: weight rows are streamed from memory
5584    /// once per layer for both positions. Full layers → fused GQA pair;
5585    /// linear layers → vmf_phase pair (lane 2 state is tentative in the
5586    /// per-layer scratch until the draft is accepted).
5587    /// Whether the fused two-position path covers every layer kind in
5588    /// this model. MLA and KDA run per position (their pair arms are
5589    /// unreachable); the seq prefill falls back to singles for them.
5590    fn pair_supported(&self) -> bool {
5591        // An EMPTY layer stack means the architecture loaded its own and
5592        // this path has nothing to walk. Checking that directly, rather
5593        // than naming each such architecture, is what makes the guard hold
5594        // for the next one: `any()` over no layers is false, so a
5595        // feature-by-feature test says "supported" for a model that has no
5596        // layers here at all.
5597        !self.weights.layers.is_empty()
5598            && self.g3n.is_none()
5599            && !self
5600                .weights
5601                .layers
5602                .iter()
5603                .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
5604    }
5605
5606    fn forward_pair(
5607        &mut self,
5608        emb1: &[f32],
5609        emb2: &[f32],
5610        position: usize,
5611    ) -> (Vec<f32>, Vec<f32>) {
5612        let mut h1 = emb1.to_vec();
5613        let mut h2 = emb2.to_vec();
5614        let (_nkv, _hd, hs, _rd, eps) = (
5615            self.num_kv_heads,
5616            self.head_dim,
5617            self.hidden_size,
5618            self.rotary_dim,
5619            self.rms_eps,
5620        );
5621        let pool = self.pool.clone();
5622
5623        for li in 0..self.num_layers {
5624            let lw = &self.weights.layers[self.phys_layer(li)];
5625            // Norms into pipeline scratch (4 allocs/layer on the MTP
5626            // decode hot path before this).
5627            inference::rms_norm_into(
5628                &h1,
5629                &lw.input_norm,
5630                self.rms_eps,
5631                self.norm_style,
5632                &mut self.ws.n1,
5633            );
5634            inference::rms_norm_into(
5635                &h2,
5636                &lw.input_norm,
5637                self.rms_eps,
5638                self.norm_style,
5639                &mut self.ws.n2,
5640            );
5641
5642            let (a1, a2) = match &lw.attn {
5643                AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5644                AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5645                AttnKind::Linear(w) => {
5646                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5647                    let layer = &mut self.kv_cache.layers[li];
5648                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5649                    vmf_phase_pair(
5650                        &self.ws.n1,
5651                        &self.ws.n2,
5652                        w,
5653                        &cfg,
5654                        state,
5655                        scratch,
5656                        self.pool.as_deref(),
5657                    )
5658                }
5659                AttnKind::LinearGdn(w) => {
5660                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5661                    let layer = &mut self.kv_cache.layers[li];
5662                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5663                    gdn_pair(
5664                        &self.ws.n1,
5665                        &self.ws.n2,
5666                        w,
5667                        &cfg,
5668                        state,
5669                        scratch,
5670                        self.pool.as_deref(),
5671                    )
5672                }
5673                AttnKind::ShortConv(w) => {
5674                    let cfg = self
5675                        .short_conv_cfg
5676                        .expect("short-conv layer without short_conv_cfg");
5677                    let layer = &mut self.kv_cache.layers[li];
5678                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5679                    short_conv_pair(
5680                        &self.ws.n1,
5681                        &self.ws.n2,
5682                        w,
5683                        &cfg,
5684                        state,
5685                        scratch,
5686                        self.pool.as_deref(),
5687                    )
5688                }
5689                AttnKind::Full {
5690                    wq,
5691                    wk,
5692                    wv,
5693                    wo,
5694                    q_norm,
5695                    k_norm,
5696                    output_gate,
5697                    softplus_gate,
5698                    bias,
5699                } => {
5700                    let inv_freq_l = self.layer_inv_freq(li);
5701                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5702                    let cfg = QwenAttnCfg {
5703                        num_heads: self.layer_num_heads(li),
5704                        num_kv_heads: nkv_l,
5705                        head_dim: hd_l,
5706                        hidden_size: hs,
5707                        position,
5708                        inv_freq: &inv_freq_l,
5709                        rotary_dim: rd_l,
5710                        scale: self.attn_scale,
5711                        softcap: self.attn_softcap,
5712                        window: self.layer_window(li),
5713                        v_norm: self.attn_v_norm,
5714                        qk_norm_after_rope: self.qk_norm_after_rope,
5715                        q_norm: q_norm.as_deref(),
5716                        k_norm: k_norm.as_deref(),
5717                        output_gate: *output_gate,
5718                        softplus_gate: softplus_gate
5719                            .as_ref()
5720                            .map(|(gate, per_head)| (gate, *per_head)),
5721                        rope_scale: self.layer_rope_scale(li),
5722                        bias: bias
5723                            .as_ref()
5724                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5725                        rms_eps: eps,
5726                        norm_style: self.norm_style,
5727                        pool: pool.as_deref(),
5728                    };
5729                    attention::qwen_attention_pair(
5730                        &self.ws.n1,
5731                        &self.ws.n2,
5732                        wq,
5733                        wk,
5734                        wv,
5735                        wo,
5736                        &mut self.kv_cache.layers[li],
5737                        &cfg,
5738                    )
5739                }
5740            };
5741            let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5742                Some(w) => (
5743                    inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
5744                    inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
5745                ),
5746                None => (a1, a2),
5747            };
5748            for i in 0..self.hidden_size {
5749                h1[i] += a1[i];
5750                h2[i] += a2[i];
5751            }
5752            let (mut a1, mut a2) = (a1, a2);
5753            attention::recycle_buf(&mut a1);
5754            attention::recycle_buf(&mut a2);
5755
5756            let lw = &self.weights.layers[self.phys_layer(li)];
5757            inference::rms_norm_into(
5758                &h1,
5759                &lw.post_norm,
5760                self.rms_eps,
5761                self.norm_style,
5762                &mut self.ws.p1,
5763            );
5764            inference::rms_norm_into(
5765                &h2,
5766                &lw.post_norm,
5767                self.rms_eps,
5768                self.norm_style,
5769                &mut self.ws.p2,
5770            );
5771            let (f1, f2) = match &lw.ffn {
5772                // Dual-branch layers need the raw residuals — run the
5773                // two positions through the same fn decode uses.
5774                FfnKind::DenseMoe(dm) => (
5775                    dense_moe_ffn(
5776                        dm,
5777                        &self.ws.p1,
5778                        &h1,
5779                        self.rms_eps,
5780                        self.norm_style,
5781                        self.pool.as_deref(),
5782                    ),
5783                    dense_moe_ffn(
5784                        dm,
5785                        &self.ws.p2,
5786                        &h2,
5787                        self.rms_eps,
5788                        self.norm_style,
5789                        self.pool.as_deref(),
5790                    ),
5791                ),
5792                _ => ffn_forward_pair(
5793                    &lw.ffn,
5794                    &self.ws.p1,
5795                    &self.ws.p2,
5796                    self.pool.as_deref(),
5797                    None,
5798                ),
5799            };
5800            let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5801                Some(w) => (
5802                    inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
5803                    inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
5804                ),
5805                None => (f1, f2),
5806            };
5807            for i in 0..self.hidden_size {
5808                h1[i] += f1[i];
5809                h2[i] += f2[i];
5810            }
5811            let (mut f1, mut f2) = (f1, f2);
5812            attention::recycle_buf(&mut f1);
5813            attention::recycle_buf(&mut f2);
5814            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5815                for i in 0..self.hidden_size {
5816                    h1[i] *= sc;
5817                    h2[i] *= sc;
5818                }
5819            }
5820            // Looped Transformer: apply final norm at the end of each loop iteration.
5821            if self.is_loop_end(li) && li + 1 < self.num_layers {
5822                h1 = inference::rms_norm(
5823                    &h1,
5824                    &self.weights.final_norm,
5825                    self.rms_eps,
5826                    self.norm_style,
5827                );
5828                h2 = inference::rms_norm(
5829                    &h2,
5830                    &self.weights.final_norm,
5831                    self.rms_eps,
5832                    self.norm_style,
5833                );
5834            }
5835        }
5836        // Real O(1) prefill pairs may also carry tentative lane-2 recurrent
5837        // state. Commit it before publishing the transition epoch so the
5838        // next serial/device row cannot observe a new attention epoch with an
5839        // old GDN state. Speculative pairs run only when O(1) is inactive and
5840        // retain their existing caller-controlled commit/rollback semantics.
5841        if self.o1_active() {
5842            self.commit_linear_scratch();
5843        }
5844        self.o1_progress();
5845        (h1, h2)
5846    }
5847
5848    /// Commit lane-2 linear states after an accepted draft.
5849    fn commit_linear_scratch(&mut self) {
5850        for layer in &mut self.kv_cache.layers {
5851            if !layer.linear_scratch.is_empty() {
5852                std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
5853                layer.linear_scratch.clear();
5854            }
5855        }
5856    }
5857
5858    /// Forward a full id sequence from a fresh cache and return the
5859    /// logits after the last position (golden-parity harness, bench).
5860    pub fn forward_ids(
5861        &mut self,
5862        ids: &[u32],
5863        task_mask: Option<&TaskMask>,
5864    ) -> Result<Vec<f32>, String> {
5865        if ids.is_empty() {
5866            return Err("empty id sequence".to_string());
5867        }
5868        self.clear_sequence_state();
5869        self.check_forward_graph("forward_ids setup", 0)?;
5870        if task_mask.is_none() {
5871            self.o1_begin();
5872        }
5873        let mut hidden = vec![0.0f32; self.hidden_size];
5874        let mut pos = 0usize;
5875        if let Some(b) = &mut self.dsv41 {
5876            let pool = self.pool.clone();
5877            let mut logits = Vec::new();
5878            crate::dsv41::forward_chunk(
5879                &b.0,
5880                &b.1,
5881                &b.2,
5882                &mut b.3,
5883                ids,
5884                0,
5885                pool.as_deref(),
5886                &mut logits,
5887            );
5888            if let Err(err) = self.o1_seal_checked() {
5889                self.clear_sequence_state();
5890                return Err(err);
5891            }
5892            return Ok(logits);
5893        }
5894        // Same routing predicate generation uses. Two reasons it must be
5895        // the same one: (1) a GDN hybrid's recurrent state is GPU-
5896        // resident, and a batched CPU prefill would build it on the host
5897        // only — decode then reads buffers the prefill never wrote;
5898        // (2) bench times THIS function and calls the result "prefill",
5899        // so a different path here reports a number production never
5900        // sees (W2 on 2×5090: 8.7 tok/s reported against 125 real).
5901        if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
5902            // prefill-GEMM in chunks; only the last position's hidden is
5903            // needed. (o1-compatible: the batch path attends per position
5904            // through qwen_attention, which carries the collection hook.)
5905            let chunk = prefill_chunk();
5906            let hs = self.hidden_size;
5907            while pos < ids.len() {
5908                let end = (pos + chunk).min(ids.len());
5909                let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
5910                self.check_forward_graph("forward_ids batched prefill", end - 1)?;
5911                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5912                pos = end;
5913            }
5914        }
5915        // Same guards as generation's prefill — INCLUDING the graph one.
5916        // The CPU pair walk was intercepting positions that the resident
5917        // token graph would have run itself: on a GDN hybrid over wgpu
5918        // that is 89 ms of host forward against 7 ms of device submit,
5919        // and it made prefill look 12× slower than it is (W2 on an RTX
5920        // 5090, ctx 512: 11.2 tok/s with the walk, 136.6 without).
5921        // CMF_PAIR=0 opts out; a model whose layers live outside
5922        // `weights.layers` has no pair walk to take.
5923        if task_mask.is_none()
5924            && !self.graph_prefill_preferred()
5925            && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
5926            && self.pair_supported()
5927        {
5928            while pos + 1 < ids.len() {
5929                let e1 = self.embed_single(ids[pos]);
5930                let e2 = self.embed_single(ids[pos + 1]);
5931                let (_, h2) = self.forward_pair(&e1, &e2, pos);
5932                self.check_forward_graph("forward_ids pair", pos + 1)?;
5933                self.commit_linear_scratch();
5934                hidden = h2;
5935                pos += 2;
5936            }
5937        }
5938        while pos < ids.len() {
5939            hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
5940            self.check_forward_graph("forward_ids", pos)?;
5941            pos += 1;
5942        }
5943        // Harness contract: after forward_ids the cache is decode-ready —
5944        // under o1 that means sealed (bench measures the seal as part of
5945        // prefill, honestly).
5946        if let Err(err) = self.o1_seal_checked() {
5947            self.clear_sequence_state();
5948            return Err(err);
5949        }
5950        let normed = inference::rms_norm(
5951            &hidden,
5952            &self.weights.final_norm,
5953            self.rms_eps,
5954            self.norm_style,
5955        );
5956        Ok(self.lm_head_forward(&normed))
5957    }
5958
5959    /// Run the V4.1 stack one token at a time and retain logits for every
5960    /// position. This is a diagnostic surface for comparing a converted
5961    /// checkpoint with a tokenwise reference implementation.
5962    #[doc(hidden)]
5963    pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
5964        #[cfg(target_os = "macos")]
5965        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
5966        if ids.is_empty() {
5967            return Err("empty id sequence".to_string());
5968        }
5969        self.clear_sequence_state();
5970        self.dsv41
5971            .as_ref()
5972            .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
5973        self.o1_begin();
5974        let rows = {
5975            let pool = self.pool.clone();
5976            let b = self
5977                .dsv41
5978                .as_mut()
5979                .expect("dsv41 checked above; state cannot change during forward");
5980            let mut rows = Vec::with_capacity(ids.len());
5981            for (position, &id) in ids.iter().enumerate() {
5982                let mut logits = Vec::new();
5983                crate::dsv41::forward_token(
5984                    &b.0,
5985                    &b.1,
5986                    &b.2,
5987                    &mut b.3,
5988                    id,
5989                    position,
5990                    pool.as_deref(),
5991                    &mut logits,
5992                );
5993                rows.push(logits);
5994            }
5995            rows
5996        };
5997        self.o1_seal();
5998        Ok(rows)
5999    }
6000
6001    /// Teacher-forced perplexity over a token sequence (phase-C gate:
6002    /// honest quant comparisons instead of prompt vibes).
6003    ///
6004    /// Attention is EXACT even on a model whose layers are flagged for
6005    /// the O(1) kernel — scoring the backbone is the default on purpose
6006    /// (it is the yardstick). `nll_ids_o1` scores the CONVERTED model.
6007    pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
6008        let (nll, cnt) = self.nll_ids_from(ids, 0)?;
6009        Ok((nll / cnt.max(1) as f64).exp())
6010    }
6011
6012    /// DTG-MA calibration pass (Patent 2): run `ids` through the model
6013    /// (CPU path, per position) and return each layer's per-neuron
6014    /// activation mass Σ|silu(gate)·up| — the statistic the task-guided
6015    /// FFN mask is derived from.
6016    pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
6017        self.clear_sequence_state();
6018        FFN_PROBE.with(|p| {
6019            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6020        });
6021        crate::gpu::cpu_scope(|| {
6022            for (pos, &id) in ids.iter().enumerate() {
6023                let emb = self.embed_single(id);
6024                let _ = self.forward_layers(&emb, pos, None);
6025            }
6026        });
6027        self.clear_sequence_state();
6028        FFN_PROBE
6029            .with(|p| p.borrow_mut().take())
6030            .unwrap_or_default()
6031    }
6032
6033    /// `probe_ffn_mass` over the BATCHED prefill: same accumulator, one
6034    /// sweep instead of one forward per token. What makes the statistic
6035    /// affordable on a 27B.
6036    pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
6037        if let Err(err) = self.nll_begin() {
6038            // A recorder can be left by a caller that was interrupted before
6039            // this request entered its scoring block.  Consume it even when
6040            // the preflight failure prevents initialization of a new one.
6041            let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
6042            self.nll_end();
6043            return Err(err);
6044        }
6045        FFN_PROBE.with(|p| {
6046            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6047        });
6048        let result: Result<(), String> = (|| {
6049            for chunk in ids.chunks(256) {
6050                if chunk.len() < 2 {
6051                    continue;
6052                }
6053                self.nll_ids_masked(chunk, 0, None)?;
6054            }
6055            Ok(())
6056        })();
6057        self.nll_end();
6058        let probe = FFN_PROBE
6059            .with(|p| p.borrow_mut().take())
6060            .unwrap_or_default();
6061        match result {
6062            Ok(()) => Ok(probe),
6063            Err(err) => {
6064                drop(probe);
6065                Err(err)
6066            }
6067        }
6068    }
6069
6070    /// Teacher-forced PPL with a task mask active (sparse execution) —
6071    /// the quality gate for a DTG-MA-masked skill. Sequential per
6072    /// position: the batched prefill path is dense-only.
6073    pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
6074        self.nll_begin()?;
6075        let result: Result<f64, String> = (|| {
6076            let mut nll = 0f64;
6077            let mut cnt = 0usize;
6078            let mut hidden = vec![0f32; self.hidden_size];
6079            for (pos, &id) in ids.iter().enumerate() {
6080                if pos > 0 {
6081                    inference::rms_norm_into(
6082                        &hidden,
6083                        &self.weights.final_norm,
6084                        self.rms_eps,
6085                        self.norm_style,
6086                        &mut self.ws.n1,
6087                    );
6088                    let mut logits = self.lm_head_forward(&self.ws.n1);
6089                    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
6090                    let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
6091                    let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
6092                    nll -= p.max(1e-300).ln();
6093                    cnt += 1;
6094                    attention::recycle_buf(&mut logits);
6095                }
6096                let emb = self.embed_single(id);
6097                hidden = self.forward_layers(&emb, pos, Some(mask));
6098                self.nll_check_graph("masked serial forward", pos)?;
6099                // Consume a possible graph logits side channel before the
6100                // next row.  Masked scoring normally disables that route,
6101                // but stale channel state must never survive a request.
6102                let _ = self.graph_logits.take();
6103            }
6104            Ok((nll / cnt.max(1) as f64).exp())
6105        })();
6106        self.nll_end();
6107        result
6108    }
6109
6110    /// Teacher-forced NLL sum + scored-token count over positions
6111    /// `start..len-1`, attention EXACT. Positions below `start` still
6112    /// run — they are the context — they are just not scored, so this
6113    /// pairs with `nll_ids_o1(ids, start)` over the very same tokens.
6114    ///
6115    /// Returning (nll, cnt) rather than a ppl is what lets a windowed
6116    /// caller combine windows before the exp, so every scored token
6117    /// weighs the same regardless of how the windows are cut.
6118    /// `nll_ids_from` with a task mask held active at every position.
6119    ///
6120    /// The batched prefill path does not thread masks, so this walks the
6121    /// per-position forward — slower, but it scores the file exactly the
6122    /// way `run --task` will serve it, which is the point of the gate
6123    /// that calls it. With `None` it defers to the fast path.
6124    /// Masked scoring rides the SAME batched sweep as unmasked scoring —
6125    /// the masked-inference fast path: `prefill_batch_masked` lands the
6126    /// per-visit FFN rows on the activations inside the fused arms. The
6127    /// per-position loop below remains only as the no-batch fallback.
6128    pub fn nll_ids_masked(
6129        &mut self,
6130        ids: &[u32],
6131        start: usize,
6132        task_mask: Option<&TaskMask>,
6133    ) -> Result<(f64, usize), String> {
6134        let task_mask = self.drop_open_mask(task_mask);
6135        self.nll_ids_inner(ids, start, task_mask)
6136    }
6137
6138    pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
6139        self.nll_ids_inner(ids, start, None)
6140    }
6141
6142    fn nll_ids_inner(
6143        &mut self,
6144        ids: &[u32],
6145        start: usize,
6146        task_mask: Option<&TaskMask>,
6147    ) -> Result<(f64, usize), String> {
6148        self.nll_begin()?;
6149        let result: Result<(f64, usize), String> = (|| {
6150            let mut nll = 0f64;
6151            let mut cnt = 0usize;
6152            // An unmasked quality run with the resident wgpu graph must score
6153            // the same stateful path used by generation.  The layer-major
6154            // GEMM prefill below is a valid CPU/GEMM oracle, but it seeds
6155            // neither the graph's device GDN state nor its device KV mirrors;
6156            // using it here would silently score a different execution.  Keep
6157            // masked scoring on the exact per-position path as before, and
6158            // let the serial arm below drive the graph-aware scorer.
6159            // Only native Metal has a fused graph lm_head contract.  Vulkan
6160            // and other graph backends may expose hidden state without the
6161            // optional logits side channel; preserve their established CPU
6162            // norm/head fallback instead of turning that valid route into a
6163            // hard missing-logits error.
6164            let (graph_quality, fused_head_quality) = nll_graph_policy(
6165                task_mask.is_none(),
6166                self.graph_prefill_preferred(),
6167                crate::gpu::q1_force(),
6168            );
6169            self.graph_head_required = fused_head_quality;
6170            self.graph_want_logits = fused_head_quality;
6171            #[cfg(target_os = "macos")]
6172            if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
6173                match self.nll_batch_metal(ids, start) {
6174                    MetalBatchNllOutcome::Completed(nll, count) => {
6175                        return Ok((nll, count));
6176                    }
6177                    MetalBatchNllOutcome::Declined => {}
6178                    MetalBatchNllOutcome::Failed(err) => return Err(err),
6179                }
6180            }
6181            if self.can_prefill_batched() && !graph_quality {
6182                // prefill-GEMM: layer-major position chunks, lm_head batched
6183                // (254MB lm_head read once per chunk, not per position).
6184                // The layer chunk is large (grouping positions by MoE experts
6185                // wins with size), lm_head in sub-blocks (logit buffer
6186                // 32×vocab ≈ 32MB instead of 128×).
6187                const CHUNK: usize = 128;
6188                const LM_SUB: usize = 32;
6189                let n = ids.len().saturating_sub(1);
6190                let hs = self.hidden_size;
6191                let rows = self.weights.lm_head.rows();
6192                let mut pos = 0usize;
6193                while pos < n {
6194                    let end = (pos + CHUNK).min(n);
6195                    let bsz = end - pos;
6196                    let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6197                    self.nll_check_graph("batched prefill", pos)?;
6198                    let mut k0 = 0usize;
6199                    while k0 < bsz {
6200                        let k1 = (k0 + LM_SUB).min(bsz);
6201                        let sb = k1 - k0;
6202                        // Sub-block entirely below the scored range: the KV
6203                        // it just built is all this pass needed from it.
6204                        if pos + k1 <= start {
6205                            k0 = k1;
6206                            continue;
6207                        }
6208                        let mut normed = vec![0.0f32; sb * hs];
6209                        for k in 0..sb {
6210                            let r = inference::rms_norm(
6211                                &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
6212                                &self.weights.final_norm,
6213                                self.rms_eps,
6214                                self.norm_style,
6215                            );
6216                            normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
6217                        }
6218                        let mut logits = vec![0.0f32; sb * rows];
6219                        self.weights
6220                            .lm_head
6221                            .matmat(&normed, sb, &mut logits, self.pool.as_deref());
6222                        for k in 0..sb {
6223                            if pos + k0 + k < start {
6224                                continue;
6225                            }
6226                            self.nll_check_graph("batched score row", pos + k0 + k)?;
6227                            let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
6228                            if let Some(mu) = self.logit_multiplier {
6229                                for v in lg.iter_mut() {
6230                                    *v *= mu;
6231                                }
6232                            }
6233                            // Gemma-class final-logit soft-capping: the
6234                            // decode paths apply it; scoring must too, or
6235                            // the uncapped softmax misprices every token.
6236                            if let Some(c) = self.final_softcap {
6237                                for v in lg.iter_mut() {
6238                                    *v = c * (*v / c).tanh();
6239                                }
6240                            }
6241                            // Cortiq Embryo hierarchical head: same correction
6242                            // the decode path applies (lm_head_forward).
6243                            if let Some(cm) = self.head_clusters.clone() {
6244                                self.hierarchical_head_logprobs(
6245                                    &normed[k * hs..(k + 1) * hs],
6246                                    &cm,
6247                                    lg,
6248                                );
6249                            }
6250                            let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
6251                            let target = ids[pos + k0 + k + 1] as usize;
6252                            let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6253                            let lse: f64 = lg
6254                                .iter()
6255                                .map(|&v| ((v - max) as f64).exp())
6256                                .sum::<f64>()
6257                                .ln()
6258                                + max as f64;
6259                            nll += lse - lg[target] as f64;
6260                            cnt += 1;
6261                            if std::env::var("CMF_PPL_TRACE").is_ok() {
6262                                let top = lg
6263                                    .iter()
6264                                    .enumerate()
6265                                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6266                                    .map(|(i, _)| i)
6267                                    .unwrap_or(0);
6268                                eprintln!(
6269                                    "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
6270                                    pos + k0 + k,
6271                                    target,
6272                                    lse - lg[target] as f64,
6273                                    top,
6274                                    lg[target],
6275                                    lg[top]
6276                                );
6277                            }
6278                        }
6279                        k0 = k1;
6280                    }
6281                    pos = end;
6282                }
6283                return Ok((nll, cnt));
6284            }
6285            for pos in 0..ids.len().saturating_sub(1) {
6286                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6287                self.nll_check_graph("serial forward", pos)?;
6288                // Architectures whose head lives inside their own stack return
6289                // the logits out of band and a zero hidden — DeepSeek-V4 folds
6290                // its hyper-connection copies between the last layer and the
6291                // norm, so it cannot hand back a vector this loop could use.
6292                // Scoring the zeros gave a perplexity of exactly the vocabulary
6293                // size, which is a uniform distribution reported as a
6294                // measurement. `generate` already reads this channel.
6295                let out_of_band = self.graph_logits.take();
6296                if self.graph_head_required && out_of_band.is_none() {
6297                    METAL_GRAPH_HEAD_MISS.fetch_add(
6298                        1,
6299                        std::sync::atomic::Ordering::Relaxed,
6300                    );
6301                    return Err(format!(
6302                        "fused Metal graph head did not complete at NLL position {pos}"
6303                    ));
6304                }
6305                if pos < start {
6306                    continue;
6307                }
6308                let logits = match out_of_band {
6309                    Some(lg) => lg,
6310                    None => {
6311                        let normed = inference::rms_norm(
6312                            &hidden,
6313                            &self.weights.final_norm,
6314                            self.rms_eps,
6315                            self.norm_style,
6316                        );
6317                        // lm_head_forward applies the final-logit softcap itself
6318                        // — capping again here double-squashed gemma-class
6319                        // logits (tanh∘tanh) and reported a flattered ppl.
6320                        self.lm_head_forward(&normed)
6321                    }
6322                };
6323                let target = ids[pos + 1] as usize;
6324                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6325                let lse: f64 = logits
6326                    .iter()
6327                    .map(|&v| ((v - max) as f64).exp())
6328                    .sum::<f64>()
6329                    .ln()
6330                    + max as f64;
6331                let tok_nll = lse - logits[target] as f64;
6332                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6333                    let top = logits
6334                        .iter()
6335                        .enumerate()
6336                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6337                        .map(|(i, _)| i)
6338                        .unwrap_or(0);
6339                    eprintln!(
6340                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6341                        logits[target], logits[top]
6342                    );
6343                }
6344                nll += tok_nll;
6345                cnt += 1;
6346            }
6347            Ok((nll, cnt))
6348        })();
6349        self.nll_end();
6350        result
6351    }
6352
6353    /// Score one post-layer hidden with the same final norm/head path used by
6354    /// decode. Keeping this in one helper is important for the production
6355    /// batch scorer: its rows stop before the final norm, just like the
6356    /// per-position O(1) path below.
6357    fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
6358        let normed = inference::rms_norm(
6359            hidden,
6360            &self.weights.final_norm,
6361            self.rms_eps,
6362            self.norm_style,
6363        );
6364        // lm_head_forward applies the final-logit softcap itself — capping
6365        // again here double-squashed gemma-class logits in earlier scorers.
6366        let mut logits = self.lm_head_forward(&normed);
6367        let target = target as usize;
6368        let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6369        let lse: f64 = logits
6370            .iter()
6371            .map(|&v| ((v - max) as f64).exp())
6372            .sum::<f64>()
6373            .ln()
6374            + max as f64;
6375        let tok_nll = lse - logits[target] as f64;
6376        if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6377            let top = logits
6378                .iter()
6379                .enumerate()
6380                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6381                .map(|(i, _)| i)
6382                .unwrap_or(0);
6383            eprintln!(
6384                "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6385                logits[target], logits[top]
6386            );
6387        }
6388        attention::recycle_buf(&mut logits);
6389        tok_nll
6390    }
6391
6392    /// Teacher-forced NLL of the CONVERTED model: the O(1) Nyström path
6393    /// is ACTIVE over the scored positions. Returns `Ok((nll sum, scored
6394    /// count))` over `prefill..len-1` and surfaces a post-mutation batch
6395    /// failure instead of returning a partial score.
6396    ///
6397    /// Runtime discipline, deliberately NOT the matrix probe's: the
6398    /// requested prefix plus any required deferred lead-in run the exact
6399    /// prompt pass — that pass is what freezes the landmarks and M — and
6400    /// every post-seal scored position goes through `NystromState::step()`,
6401    /// the same code decode runs.
6402    /// So the landmarks are PREFILL-frozen (what ships), not
6403    /// full-sequence oracles (what the published probe measured). When the
6404    /// requested prefix is shorter than the bounded transition, rows in the
6405    /// exact lead-in are still scored so the shifted target range is stable.
6406    ///
6407    /// Pair with `nll_ids_from(ids, prefill)` for the exact baseline
6408    /// over the identical token set — that ratio is the honest one.
6409    pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
6410        // This scorer consumes host hiddens, so never request the optional
6411        // token-graph lm_head side channel. `nll_begin` also consumes a
6412        // prior graph failure and clears only the cancel bit that failure
6413        // raised, leaving a caller-owned cancellation observable.
6414        self.nll_begin()?;
6415        let requested_prefix = (prefill > 0).then_some(prefill);
6416        self.o1_begin_with_prefix(requested_prefix);
6417        let n = ids.len().saturating_sub(1);
6418        let requested_start = prefill.min(n);
6419        // The exact prefix must reach the deferred boundary before a
6420        // collecting layer can convert. Rows between the requested start and
6421        // that boundary remain part of the public NLL range and are scored
6422        // from the same hidden pass below.
6423        let exact_end = if self.o1_active() {
6424            match requested_prefix {
6425                Some(requested) => self.o1_effective_boundary(requested),
6426                None => self
6427                    .o1_cfg
6428                    .as_ref()
6429                    .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
6430            }
6431            .unwrap_or(requested_start)
6432            .min(n)
6433        } else {
6434            requested_start
6435        };
6436        let mut nll = 0f64;
6437        let mut cnt = 0usize;
6438
6439        // Exact prompt pass over ids[..exact_end]: the seal consumes its
6440        // q/k/v. Rows at or after requested_start are scored here when the
6441        // bounded lead-in is longer than the caller's requested prefix.
6442        let mut pos = 0usize;
6443        if self.can_prefill_batched() {
6444            const CHUNK: usize = 128;
6445            while pos < exact_end {
6446                let end = (pos + CHUNK).min(exact_end);
6447                let hiddens = self.prefill_batch(&ids[pos..end], pos);
6448                if self
6449                    .graph_failed
6450                    .swap(false, std::sync::atomic::Ordering::Relaxed)
6451                {
6452                    self.cancel
6453                        .store(false, std::sync::atomic::Ordering::Relaxed);
6454                    self.nll_end();
6455                    return Err("GPU graph failed during O(1) NLL prefix".into());
6456                }
6457                for row in 0..end - pos {
6458                    let score_pos = pos + row;
6459                    if score_pos >= requested_start && score_pos < n {
6460                        nll += self.nll_from_hidden(
6461                            &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
6462                            ids[score_pos + 1],
6463                            score_pos,
6464                        );
6465                        cnt += 1;
6466                    }
6467                }
6468                pos = end;
6469            }
6470        } else {
6471            while pos < exact_end {
6472                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6473                if self
6474                    .graph_failed
6475                    .swap(false, std::sync::atomic::Ordering::Relaxed)
6476                {
6477                    self.cancel
6478                        .store(false, std::sync::atomic::Ordering::Relaxed);
6479                    self.nll_end();
6480                    return Err("GPU graph failed during O(1) NLL prefix".into());
6481                }
6482                if pos >= requested_start && pos < n {
6483                    nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6484                    cnt += 1;
6485                }
6486                pos += 1;
6487            }
6488        }
6489        self.o1_seal_checked().map_err(|err| {
6490            self.nll_end();
6491            err
6492        })?;
6493
6494        // Reuse the production whole-token batch graph for the post-seal
6495        // suffix when the caller explicitly enabled both routes. This is a
6496        // teacher-forced scorer, so every row is ids[pos] and its target is
6497        // ids[pos + 1]; no speculative tail or rollback state is involved.
6498        // A first Declined is safe to handle with the established serial O(1)
6499        // path. Once a chunk completes, however, the device recurrent state
6500        // owns the sequence and a later decline must be terminal rather than
6501        // falling back to stale CPU state.
6502        let batch_k = std::env::var("CMF_BATCH_K")
6503            .ok()
6504            .and_then(|v| v.parse::<usize>().ok())
6505            .unwrap_or(0);
6506        let batch_admitted = batch_k > 0
6507            && self.can_prefill_batched()
6508            && self.o1_active()
6509            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6510            && (0..self.num_layers).all(|li| {
6511                let cache = &self.kv_cache.layers[self.phys_layer(li)];
6512                cache.o1.is_none() || cache.o1_views().is_some()
6513            });
6514        if std::env::var("CMF_GRAPH_PROF").is_ok() {
6515            eprintln!(
6516                "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
6517                batch_admitted,
6518                batch_k,
6519                n.saturating_sub(exact_end),
6520            );
6521        }
6522        let mut batch_completed = false;
6523        if batch_admitted && exact_end < n {
6524            let hs = self.hidden_size;
6525            let mut batch_pos = exact_end;
6526            while batch_pos < n {
6527                let end = (batch_pos + batch_k).min(n);
6528                let bk = end - batch_pos;
6529                let mut hiddens = vec![0.0f32; bk * hs];
6530                for (row, &id) in ids[batch_pos..end].iter().enumerate() {
6531                    hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
6532                }
6533                let positions: Vec<usize> = (batch_pos..end).collect();
6534                let t_batch = std::time::Instant::now();
6535                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6536                if std::env::var("CMF_GRAPH_PROF").is_ok() {
6537                    let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
6538                    eprintln!(
6539                        "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6540                        batch_pos,
6541                        end.saturating_sub(1),
6542                        bk as f64 / (ms / 1000.0),
6543                    );
6544                }
6545                if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
6546                    self.nll_end();
6547                    return Err(err);
6548                }
6549                match outcome {
6550                    crate::gpu::BatchGraphOutcome::Completed => {
6551                        batch_completed = true;
6552                        for row in 0..bk {
6553                            nll += self.nll_from_hidden(
6554                                &hiddens[row * hs..(row + 1) * hs],
6555                                ids[batch_pos + row + 1],
6556                                batch_pos + row,
6557                            );
6558                            cnt += 1;
6559                        }
6560                        batch_pos = end;
6561                    }
6562                    crate::gpu::BatchGraphOutcome::Declined => {
6563                        if batch_completed {
6564                            self.nll_end();
6565                            return Err(format!(
6566                                "O(1) NLL batch declined after completed chunk at position {batch_pos}"
6567                            ));
6568                        }
6569                        break;
6570                    }
6571                    crate::gpu::BatchGraphOutcome::Failed => {
6572                        self.nll_end();
6573                        return Err(format!(
6574                            "O(1) NLL batch graph failed after admission at position {batch_pos}"
6575                        ));
6576                    }
6577                }
6578            }
6579            if batch_completed && cnt == n.saturating_sub(requested_start) {
6580                self.nll_end();
6581                return Ok((nll, cnt));
6582            }
6583        }
6584
6585        // Serial O(1) fallback/reference. It is intentionally retained when
6586        // batch admission declines before mutation; callers must label this
6587        // CMF_BATCH_K=0/per-position path separately from the production
6588        // whole-token batch route.
6589        for pos in exact_end..n {
6590            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6591            if self
6592                .graph_failed
6593                .swap(false, std::sync::atomic::Ordering::Relaxed)
6594            {
6595                self.cancel
6596                    .store(false, std::sync::atomic::Ordering::Relaxed);
6597                self.nll_end();
6598                return Err(format!(
6599                    "GPU graph failed during O(1) NLL serial scoring at position {pos}"
6600                ));
6601            }
6602            nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6603            cnt += 1;
6604        }
6605        self.nll_end();
6606        Ok((nll, cnt))
6607    }
6608
6609    /// Teacher-forced calibration data (B1): for each position, whether the
6610    /// argmax equals the actual next token, and the top-1 softmax prob
6611    /// (top-1 probability) under EACH temperature in `temps` — all from ONE forward
6612    /// pass (argmax/correctness are temperature-invariant; only p_max
6613    /// reshapes). Feeds `cortiq calibrate` (reliability/ECE + temperature
6614    /// fit): is the model's confidence a true property, or does it need a
6615    /// measured scaling?
6616    pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
6617        self.clear_sequence_state();
6618        let n = ids.len().saturating_sub(1);
6619        let mut correct = Vec::with_capacity(n);
6620        let mut pmax = Vec::with_capacity(n);
6621        for pos in 0..n {
6622            let emb = self.embed_single(ids[pos]);
6623            let hidden = self.forward_layers(&emb, pos, None);
6624            let normed = inference::rms_norm(
6625                &hidden,
6626                &self.weights.final_norm,
6627                self.rms_eps,
6628                self.norm_style,
6629            );
6630            // lm_head_forward applies the final-logit softcap itself —
6631            // capping again here double-squashed gemma-class logits
6632            // (tanh∘tanh) and reported a flattered ppl.
6633            let logits = self.lm_head_forward(&normed);
6634            let target = ids[pos + 1] as usize;
6635            let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
6636            for (i, &v) in logits.iter().enumerate() {
6637                if v > mval {
6638                    mval = v;
6639                    amax = i;
6640                }
6641            }
6642            correct.push(amax == target);
6643            let row: Vec<f32> = temps
6644                .iter()
6645                .map(|&t| {
6646                    let tt = t.max(1e-3);
6647                    let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
6648                    1.0 / s.max(1e-12) // numerator at the max is exp(0)=1
6649                })
6650                .collect();
6651            pmax.push(row);
6652        }
6653        self.clear_sequence_state();
6654        (correct, pmax)
6655    }
6656
6657    /// Teacher-forced PPL with the dynamic router driving per-window
6658    /// skill switches (VMF experiment №2 measurement). Sequential (φ
6659    /// must update per token), returns (ppl, switch_count). The router
6660    /// must be enabled (`enable_dynamic_routing`); else this equals
6661    /// plain `ppl_ids`. The active skill when scoring token t shapes the
6662    /// logits for t+1 — on-policy over the held-out text itself.
6663    pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
6664        if self.dyn_router.is_none() {
6665            return Ok((self.ppl_ids(ids)?, 0));
6666        }
6667        self.nll_begin()?;
6668        let saved_active = self.dyn_active;
6669        let mut router = self
6670            .dyn_router
6671            .take()
6672            .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
6673        router.reset();
6674        self.dyn_phi_seen = 0;
6675        let _ = self.set_active_skill(None);
6676
6677        let result: Result<(f64, usize), String> = (|| {
6678            let mut nll = 0f64;
6679            let mut cnt = 0usize;
6680            for pos in 0..ids.len().saturating_sub(1) {
6681                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6682                self.nll_check_graph("dynamic serial forward", pos)?;
6683                let out_of_band = self.graph_logits.take();
6684                let mut logits = match out_of_band {
6685                    Some(lg) => lg,
6686                    None => {
6687                        let normed = inference::rms_norm(
6688                            &hidden,
6689                            &self.weights.final_norm,
6690                            self.rms_eps,
6691                            self.norm_style,
6692                        );
6693                        // lm_head_forward applies the final-logit softcap itself —
6694                        // capping again here double-squashed gemma-class logits
6695                        // and reported a flattered ppl.
6696                        self.lm_head_forward(&normed)
6697                    }
6698                };
6699                let target = ids[pos + 1] as usize;
6700                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6701                let lse: f64 = logits
6702                    .iter()
6703                    .map(|&v| ((v - max) as f64).exp())
6704                    .sum::<f64>()
6705                    .ln()
6706                    + max as f64;
6707                let tok_nll = lse - logits[target] as f64;
6708                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6709                    let top = logits
6710                        .iter()
6711                        .enumerate()
6712                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6713                        .map(|(i, _)| i)
6714                        .unwrap_or(0);
6715                    eprintln!(
6716                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6717                        logits[target], logits[top]
6718                    );
6719                }
6720                nll += tok_nll;
6721                cnt += 1;
6722                attention::recycle_buf(&mut logits);
6723                // Route on the evolving phi (drives the NEXT token's skill).
6724                let phi = self.dyn_phi_ema.clone();
6725                if let Some(new_active) = router.step(&phi, pos) {
6726                    let _ = self.set_active_skill(new_active);
6727                }
6728            }
6729            Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
6730        })();
6731
6732        // Restore the detached router and the active overlay on both success
6733        // and failure. The scoring state is cleared independently below.
6734        let _ = self.set_active_skill(saved_active);
6735        self.dyn_router = Some(router);
6736        self.nll_end();
6737        result
6738    }
6739
6740    /// Routing probe φ (spec §9): mean-pooled hidden after `layer`.
6741    pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
6742        self.clear_sequence_state();
6743        let mut acc = vec![0f32; self.hidden_size];
6744        for (pos, &id) in ids.iter().enumerate() {
6745            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
6746            for (a, v) in acc.iter_mut().zip(&h) {
6747                *a += v;
6748            }
6749        }
6750        let n = ids.len().max(1) as f32;
6751        for a in acc.iter_mut() {
6752            *a /= n;
6753        }
6754        self.clear_sequence_state();
6755        acc
6756    }
6757
6758    /// Layer-major batched prefill (prefill-GEMM): full-attention —
6759    /// per-position with the existing operators (KV grows naturally,
6760    /// causality preserved), GDN projections / FFN / MoE — batched
6761    /// (a weight row is read from DRAM once per chunk, not per
6762    /// position). Returns the hidden of all positions [b × hidden].
6763    fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
6764        self.prefill_batch_masked(ids, start_pos, None)
6765    }
6766
6767    /// `prefill_batch` with a task mask honored on the dense-FFN panels
6768    /// (the masked-inference fast path: full fused compute, mask lands on
6769    /// the activations). The whole-chunk GPU graph is skipped for masked
6770    /// layers by the callers' arms; the per-GEMM device paths stay in
6771    /// play because the zeroing happens on the host between them.
6772    fn prefill_batch_masked(
6773        &mut self,
6774        ids: &[u32],
6775        start_pos: usize,
6776        task_mask: Option<&TaskMask>,
6777    ) -> Vec<f32> {
6778        self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
6779    }
6780
6781    /// The layer-major batched walk over a layer span [from..upto_excl):
6782    /// the whole prefill machinery (chunk graph, batched attends, GEMM
6783    /// panels) for a PARTIAL stack — the network split's prefill rides
6784    /// the same canon as the local one. Input is token ids (embeds
6785    /// itself, coordinator side) or ready boundary hiddens (worker side).
6786    fn prefill_batch_span(
6787        &mut self,
6788        input: PrefillIn<'_>,
6789        start_pos: usize,
6790        task_mask: Option<&TaskMask>,
6791        from: usize,
6792        upto_excl: usize,
6793    ) -> Vec<f32> {
6794        let hs = self.hidden_size;
6795        let b = match input {
6796            PrefillIn::Ids(ids) => ids.len(),
6797            PrefillIn::Hidden(hb) => hb.len() / hs,
6798        };
6799        let upto_excl = upto_excl.min(self.num_layers);
6800        // The CPU embed is deferred: when the chunk graph takes the run
6801        // from layer 0 it gathers the embeddings on the device instead.
6802        // A hidden input is ready by definition.
6803        let mut h: Vec<f32>;
6804        let mut h_ready;
6805        match input {
6806            PrefillIn::Ids(_) => {
6807                h = vec![0.0; b * hs];
6808                h_ready = false;
6809            }
6810            PrefillIn::Hidden(hb) => {
6811                h = hb.to_vec();
6812                h_ready = true;
6813            }
6814        }
6815        let fill_h = |h: &mut Vec<f32>, me: &Self| {
6816            if let PrefillIn::Ids(ids) = input {
6817                for (bi, &id) in ids.iter().enumerate() {
6818                    let e = me.embed_single(id);
6819                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
6820                }
6821                if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6822                    if let Ok(t) = tp.parse::<usize>() {
6823                        if t >= start_pos && t < start_pos + ids.len() {
6824                            let bi = t - start_pos;
6825                            let row = &h[bi * hs..(bi + 1) * hs];
6826                            let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6827                            eprintln!(
6828                                "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
6829                                ids[bi],
6830                                row[0],
6831                                row[1],
6832                                ids.len(),
6833                                &ids[..ids.len().min(8)]
6834                            );
6835                        }
6836                    }
6837                }
6838            }
6839        };
6840        let (_nkv, _hd, _rd, eps) = (
6841            self.num_kv_heads,
6842            self.head_dim,
6843            self.rotary_dim,
6844            self.rms_eps,
6845        );
6846        let pool = self.pool.clone();
6847        let norm_style = self.norm_style;
6848        let automatic_gpu_prefix = self.automatic_gpu_prefix();
6849
6850        #[cfg(target_os = "macos")]
6851        let mut chunk_skip_until = 0usize;
6852        for li in from..upto_excl {
6853            let _capacity_tail = automatic_gpu_prefix
6854                .filter(|&prefix| li >= prefix)
6855                .map(|_| crate::gpu::enter_cpu_scope());
6856            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU
6857            // GPU chunk graph (default-on under CMF_GPU=1): a run of
6858            // consecutive eligible layers for the whole chunk in ONE
6859            // Metal submission — norm, QKV, RoPE with fused mirror
6860            // append, causal attend, O, FFN, hidden device-resident
6861            // across the run. Any refusal falls through to the CPU path.
6862            #[cfg(target_os = "macos")]
6863            if task_mask.is_none() {
6864                if li < chunk_skip_until {
6865                    continue;
6866                }
6867                // Device-side embedding needs a q8_row embedding matrix;
6868                // with any other layout the CPU fills `h` first and the
6869                // graph starts from a ready hidden (refusing the whole
6870                // run over the embedding alone kept q4t models — the
6871                // whole Nanbeige/Bonsai class — on the CPU prefill).
6872                if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
6873                    fill_h(&mut h, self);
6874                    h_ready = true;
6875                }
6876                let ids_for_embed = match input {
6877                    PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
6878                    PrefillIn::Hidden(_) => None,
6879                };
6880                let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
6881                if end > li {
6882                    h_ready = true;
6883                    chunk_skip_until = end;
6884                    // Looped Transformer: the graph stopped at a loop
6885                    // boundary — apply final norm before the next iteration.
6886                    if self.is_loop_end(end - 1) && end < self.num_layers {
6887                        for bi in 0..b {
6888                            let normed = inference::rms_norm(
6889                                &h[bi * hs..(bi + 1) * hs],
6890                                &self.weights.final_norm,
6891                                eps,
6892                                norm_style,
6893                            );
6894                            h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6895                        }
6896                    }
6897                    continue;
6898                }
6899            }
6900            if !h_ready {
6901                fill_h(&mut h, self);
6902                h_ready = true;
6903            }
6904            let lw = &self.weights.layers[self.phys_layer(li)];
6905            // ── attention ──
6906            match &lw.attn {
6907                AttnKind::Kda(w) => {
6908                    // Projections batched, recurrence sequential.
6909                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
6910                    let mut normed = vec![0.0f32; b * hs];
6911                    for bi in 0..b {
6912                        inference::rms_norm_into(
6913                            &h[bi * hs..(bi + 1) * hs],
6914                            &lw.input_norm,
6915                            eps,
6916                            norm_style,
6917                            &mut normed[bi * hs..(bi + 1) * hs],
6918                        );
6919                    }
6920                    let attn = crate::linear_core::kda_forward_batch(
6921                        &normed,
6922                        b,
6923                        w,
6924                        &cfg,
6925                        &mut self.kv_cache.layers[li].linear_state,
6926                        pool.as_deref(),
6927                    );
6928                    for (dst, &a) in h.iter_mut().zip(&attn) {
6929                        *dst += a;
6930                    }
6931                }
6932                AttnKind::LinearGdn(w) => {
6933                    // Projections batched, recurrence sequential.
6934                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6935                    let mut normed = vec![0.0f32; b * hs];
6936                    for bi in 0..b {
6937                        let r = inference::rms_norm(
6938                            &h[bi * hs..(bi + 1) * hs],
6939                            &lw.input_norm,
6940                            eps,
6941                            norm_style,
6942                        );
6943                        normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6944                    }
6945                    let attn = crate::linear_core::gdn_forward_batch(
6946                        &normed,
6947                        b,
6948                        w,
6949                        &cfg,
6950                        &mut self.kv_cache.layers[li].linear_state,
6951                        pool.as_deref(),
6952                    );
6953                    for (dst, &a) in h.iter_mut().zip(&attn) {
6954                        *dst += a;
6955                    }
6956                }
6957                AttnKind::ShortConv(w) => {
6958                    // Projections batched over the chunk; the conv walks the
6959                    // contiguous positions in order (same ring as decode).
6960                    let cfg = self
6961                        .short_conv_cfg
6962                        .expect("short-conv layer without short_conv_cfg");
6963                    let mut normed = vec![0.0f32; b * hs];
6964                    for bi in 0..b {
6965                        inference::rms_norm_into(
6966                            &h[bi * hs..(bi + 1) * hs],
6967                            &lw.input_norm,
6968                            eps,
6969                            norm_style,
6970                            &mut normed[bi * hs..(bi + 1) * hs],
6971                        );
6972                    }
6973                    let attn = short_conv_forward_batch(
6974                        &normed,
6975                        b,
6976                        w,
6977                        &cfg,
6978                        &mut self.kv_cache.layers[li].linear_state,
6979                        pool.as_deref(),
6980                    );
6981                    for (dst, &a) in h.iter_mut().zip(&attn) {
6982                        *dst += a;
6983                    }
6984                }
6985                AttnKind::Mla(w) => {
6986                    // Per-position prefill (correctness first; latent
6987                    // batching is a later optimization).
6988                    let inv_freq_l = self.layer_inv_freq(li);
6989                    let rs = self.layer_rope_scale(li);
6990                    let mut normed = vec![0.0f32; hs];
6991                    for bi in 0..b {
6992                        inference::rms_norm_into(
6993                            &h[bi * hs..(bi + 1) * hs],
6994                            &lw.input_norm,
6995                            eps,
6996                            norm_style,
6997                            &mut normed,
6998                        );
6999                        let ao = mla_attention(
7000                            w,
7001                            &normed,
7002                            &mut self.kv_cache.layers[li],
7003                            start_pos + bi,
7004                            &inv_freq_l,
7005                            rs,
7006                            eps,
7007                            pool.as_deref(),
7008                        );
7009                        for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
7010                            *dst += a;
7011                        }
7012                    }
7013                }
7014                AttnKind::Full {
7015                    wq,
7016                    wk,
7017                    wv,
7018                    wo,
7019                    q_norm,
7020                    k_norm,
7021                    output_gate,
7022                    softplus_gate,
7023                    bias,
7024                } => {
7025                    // Chunk-GEMM QKV/O; per-position causal attention
7026                    // inside (roadmap §3 P0 — full-attention prefill no
7027                    // longer re-reads the projection weights b times).
7028                    let mut normed = vec![0.0f32; b * hs];
7029                    for bi in 0..b {
7030                        inference::rms_norm_into(
7031                            &h[bi * hs..(bi + 1) * hs],
7032                            &lw.input_norm,
7033                            eps,
7034                            norm_style,
7035                            &mut normed[bi * hs..(bi + 1) * hs],
7036                        );
7037                    }
7038                    let inv_freq_l = self.layer_inv_freq(li);
7039                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7040                    let cfg = QwenAttnCfg {
7041                        num_heads: self.layer_num_heads(li),
7042                        num_kv_heads: nkv_l,
7043                        head_dim: hd_l,
7044                        hidden_size: hs,
7045                        position: start_pos,
7046                        inv_freq: &inv_freq_l,
7047                        rotary_dim: rd_l,
7048                        scale: self.attn_scale,
7049                        softcap: self.attn_softcap,
7050                        window: self.layer_window(li),
7051                        v_norm: self.attn_v_norm,
7052                        qk_norm_after_rope: self.qk_norm_after_rope,
7053                        q_norm: q_norm.as_deref(),
7054                        k_norm: k_norm.as_deref(),
7055                        output_gate: *output_gate,
7056                        softplus_gate: softplus_gate
7057                            .as_ref()
7058                            .map(|(gate, per_head)| (gate, *per_head)),
7059                        rope_scale: self.layer_rope_scale(li),
7060                        bias: bias
7061                            .as_ref()
7062                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7063                        rms_eps: eps,
7064                        norm_style,
7065                        pool: pool.as_deref(),
7066                    };
7067                    let mut attn = attention::qwen_attention_batch(
7068                        &normed,
7069                        b,
7070                        wq,
7071                        wk,
7072                        wv,
7073                        wo,
7074                        &mut self.kv_cache.layers[li],
7075                        &cfg,
7076                    );
7077                    if let Some(w) = &lw.attn_out_norm {
7078                        for bi in 0..b {
7079                            inference::rms_norm_into(
7080                                &attn[bi * hs..(bi + 1) * hs],
7081                                w,
7082                                eps,
7083                                norm_style,
7084                                &mut normed[bi * hs..(bi + 1) * hs],
7085                            );
7086                        }
7087                        attn.copy_from_slice(&normed);
7088                    }
7089                    for (dst, &a) in h.iter_mut().zip(&attn) {
7090                        *dst += a;
7091                    }
7092                }
7093                AttnKind::Linear(w) => {
7094                    for bi in 0..b {
7095                        let normed = inference::rms_norm(
7096                            &h[bi * hs..(bi + 1) * hs],
7097                            &lw.input_norm,
7098                            eps,
7099                            norm_style,
7100                        );
7101                        vmf_phase_forward(
7102                            &normed,
7103                            w,
7104                            &self.vmf_cfg.expect("linear layer without vmf_cfg"),
7105                            &mut self.kv_cache.layers[li].linear_state,
7106                            pool.as_deref(),
7107                        )
7108                        .iter()
7109                        .enumerate()
7110                        .for_each(|(i, &a)| h[bi * hs + i] += a);
7111                    }
7112                }
7113            }
7114
7115            // ── FFN batched ──
7116            let lw = &self.weights.layers[self.phys_layer(li)];
7117            let mut post = vec![0.0f32; b * hs];
7118            for bi in 0..b {
7119                let r =
7120                    inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
7121                post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7122            }
7123            // A restrictive per-visit FFN row lands on the activations
7124            // inside the dense arm; an all-open row costs nothing.
7125            let mask_row = task_mask
7126                .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
7127                .and_then(|m| m.ffn_masks.get(li))
7128                .map(|v| v.as_slice());
7129            let mut ffn = match &lw.ffn {
7130                FfnKind::Dense(d) if !d.segs.is_empty() => {
7131                    tube_ffn(d, &post, b, pool.as_deref(), mask_row)
7132                }
7133                FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
7134                FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
7135                // Dual-branch layers run per position (the expert branch
7136                // reads the raw residual — nothing to batch yet).
7137                FfnKind::DenseMoe(dm) => {
7138                    let mut out = vec![0.0f32; b * hs];
7139                    for bi in 0..b {
7140                        let r = dense_moe_ffn(
7141                            dm,
7142                            &post[bi * hs..(bi + 1) * hs],
7143                            &h[bi * hs..(bi + 1) * hs],
7144                            eps,
7145                            norm_style,
7146                            pool.as_deref(),
7147                        );
7148                        out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7149                    }
7150                    out
7151                }
7152            };
7153            if let Some(w) = &lw.ffn_out_norm {
7154                for bi in 0..b {
7155                    inference::rms_norm_into(
7156                        &ffn[bi * hs..(bi + 1) * hs],
7157                        w,
7158                        eps,
7159                        norm_style,
7160                        &mut post[bi * hs..(bi + 1) * hs],
7161                    );
7162                }
7163                ffn.copy_from_slice(&post);
7164            }
7165            for (dst, &f) in h.iter_mut().zip(&ffn) {
7166                *dst += f;
7167            }
7168            if let Some(sc) = lw.layer_scale {
7169                for v in h.iter_mut() {
7170                    *v *= sc;
7171                }
7172            }
7173            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
7174                if let Ok(t) = tp.parse::<usize>() {
7175                    if t >= start_pos && t < start_pos + b {
7176                        let bi = t - start_pos;
7177                        let row = &h[bi * hs..(bi + 1) * hs];
7178                        let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
7179                        eprintln!(
7180                            "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
7181                            row[0], row[1]
7182                        );
7183                    }
7184                }
7185            }
7186            // CMF_DEBUG_LAYERS=1: per-layer hidden-state health of the
7187            // LAST prompt position — the knife for "which layer type
7188            // breaks first" on a new architecture.
7189            if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
7190                let row = &h[(b - 1) * hs..b * hs];
7191                let rms =
7192                    (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
7193                let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
7194                eprintln!(
7195                    "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
7196                    match &self.weights.layers[self.phys_layer(li)].attn {
7197                        AttnKind::LinearGdn(_) => "gdn",
7198                        AttnKind::Linear(_) => "vmf",
7199                        AttnKind::ShortConv(_) => "conv",
7200                        _ => "attn",
7201                    },
7202                    match &lw.ffn {
7203                        FfnKind::Moe(_) => "moe",
7204                        FfnKind::Dense(_) => "dense",
7205                        FfnKind::DenseMoe(_) => "dense+moe",
7206                    },
7207                );
7208            }
7209            // Looped Transformer: apply final norm at the end of each loop iteration.
7210            if self.is_loop_end(li) && li + 1 < self.num_layers {
7211                for bi in 0..b {
7212                    let normed = inference::rms_norm(
7213                        &h[bi * hs..(bi + 1) * hs],
7214                        &self.weights.final_norm,
7215                        eps,
7216                        norm_style,
7217                    );
7218                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
7219                }
7220            }
7221            if std::env::var("CMF_TRACE_H").is_ok() {
7222                let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
7223                let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
7224                eprintln!(
7225                    "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
7226                    lw.layer_scale
7227                );
7228            }
7229        }
7230        crate::gpu::set_layer(-1); // lm_head/final ops outside layer-split
7231        // A batched span owns a complete set of positions. Publish any
7232        // collecting→sealed transition only after every layer has finished;
7233        // callers that cross into serial/device work must see the new epoch
7234        // before this function returns.
7235        self.o1_progress();
7236        h
7237    }
7238
7239    /// Embed a single token.
7240    fn embed_single(&self, id: u32) -> Vec<f32> {
7241        let mut out = vec![0.0f32; self.hidden_size];
7242        if (id as usize) < self.weights.embed_tokens.rows() {
7243            self.weights.embed_tokens.row_f32(id as usize, &mut out);
7244        }
7245        if self.embed_multiplier != 1.0 {
7246            for v in out.iter_mut() {
7247                *v *= self.embed_multiplier;
7248            }
7249        }
7250        // DeepSeek-V4's hash layers route by TOKEN ID, so the id has to
7251        // reach the forward. It rides in slot 0 (the forward re-reads the
7252        // real embedding itself from the table).
7253        if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
7254            let mut v = vec![0.0f32; self.hidden_size.max(1)];
7255            v[0] = id as f32;
7256            return v;
7257        }
7258        // Gemma-3n: the per-layer-embedding half needs the token ID, so
7259        // it rides appended to the embedding; the g3n forward splits it.
7260        if let Some(b) = &self.g3n {
7261            return b.0.extend_embedding(id, &out, self.pool.as_deref());
7262        }
7263        out
7264    }
7265
7266    /// A run of consecutive prefill layers on the GPU for the whole
7267    /// chunk (default-on under CMF_GPU=1; CMF_GPU_CHUNK=0 disables).
7268    /// Eligibility per layer: q8_row weights, plain full attention
7269    /// (no output gate), F32 KV, no o1/masks/gemma extras. Returns the
7270    /// first layer index NOT processed (== `li0` when the run is empty).
7271    #[cfg(target_os = "macos")]
7272    fn chunk_run_gpu(
7273        &mut self,
7274        li0: usize,
7275        h: &mut [f32],
7276        b: usize,
7277        pos0: usize,
7278        embed_ids: Option<&[u32]>,
7279        cap: usize,
7280    ) -> usize {
7281        // (The old streaming attend needed a depth bound at ~1k; the
7282        // GEMM attention scales like the CPU path and lifted it.)
7283        // CMF_GPU_CHUNK=0 disables the graph.
7284        if !crate::gpu::enabled_here()
7285            || std::env::var("CMF_GPU_CHUNK")
7286                .map(|v| v == "0")
7287                .unwrap_or(false)
7288            || b < 32
7289            || self.swa.is_some()
7290            || self.global_attn.is_some()
7291            // Collection owns the exact Q trace and boundary conversion;
7292            // this chunk graph appends dense KV without feeding that trace.
7293            || self.o1_active()
7294            || self.attn_v_norm
7295            || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
7296        {
7297            return li0;
7298        }
7299        let Some(model) = self.model.clone() else {
7300            return li0;
7301        };
7302        let inv_freq = self.inv_freq.clone();
7303        let (nh, nkv, hd, hs) = (
7304            self.num_heads,
7305            self.num_kv_heads,
7306            self.head_dim,
7307            self.hidden_size,
7308        );
7309        // Collect the longest run of consecutive eligible layers.
7310        // Looped Transformer: stop at the loop boundary so the CPU can
7311        // apply loop_final_norm between iterations.
7312        let loop_end = if self.loop_final_norm {
7313            ((li0 / self.physical_layers) + 1) * self.physical_layers
7314        } else {
7315            self.num_layers
7316        };
7317        let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
7318        let mut stored_at: Vec<usize> = Vec::new();
7319        for li in li0..self.num_layers.min(loop_end).min(cap) {
7320            let lw = &self.weights.layers[self.phys_layer(li)];
7321            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
7322                break;
7323            }
7324            let AttnKind::Full {
7325                wq,
7326                wk,
7327                wv,
7328                wo,
7329                q_norm,
7330                k_norm,
7331                output_gate: false,
7332                softplus_gate: None,
7333                bias,
7334            } = &lw.attn
7335            else {
7336                break;
7337            };
7338            let FfnKind::Dense(d) = &lw.ffn else { break };
7339            if d.act != Act::Silu || !d.segs.is_empty() {
7340                break;
7341            }
7342            // q8_row (row_scale populated), or q4_tiled / q4tp (row_scale
7343            // empty — their scales are in the payload). Mixing across the
7344            // seven projections of one layer is fine; the encoder branches
7345            // per weight on the tensor's dtype. Anything else refuses.
7346            fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
7347                t.q8_row_parts()
7348                    .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7349                    .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7350            }
7351            let parts = (
7352                cw(wq),
7353                cw(wk),
7354                cw(wv),
7355                cw(wo),
7356                cw(&d.gate_proj),
7357                cw(&d.up_proj),
7358                cw(&d.down_proj),
7359            );
7360            let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
7361            else {
7362                break;
7363            };
7364            let layer = &self.kv_cache.layers[li];
7365            if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
7366                break;
7367            }
7368            stored_at.push(layer.head_len(0));
7369            layers.push(crate::gpu_metal::ChunkLayer {
7370                model: &model,
7371                kv_id: self.graph_kv_id,
7372                layer: li,
7373                wq: pq,
7374                wk: pk,
7375                wv: pv,
7376                wo: po,
7377                gate: pg,
7378                up: pu,
7379                down: pd,
7380                input_norm: &lw.input_norm,
7381                post_norm: &lw.post_norm,
7382                bias: bias
7383                    .as_ref()
7384                    .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
7385                q_norm: q_norm.as_deref(),
7386                k_norm: k_norm.as_deref(),
7387                inv_freq: &inv_freq,
7388                rd: self.rotary_dim,
7389                nh,
7390                nkv,
7391                hd,
7392                hs,
7393                inter: d.gate_proj.rows(),
7394                gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
7395                late_qk_norm: self.qk_norm_after_rope,
7396                eps: self.rms_eps as f32,
7397            });
7398        }
7399        if layers.is_empty() {
7400            return li0;
7401        }
7402        let row = nkv * hd;
7403        let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
7404            .iter()
7405            .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
7406            .collect();
7407        let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
7408        for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
7409            let li = layers[i].layer;
7410            let layer = &self.kv_cache.layers[li];
7411            io.push(crate::gpu_metal::ChunkIo {
7412                cpu_stored: stored_at[i],
7413                cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
7414                cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
7415                out_k: ok,
7416                out_v: ov,
7417                imp: oi,
7418            });
7419        }
7420        let n_run = layers.len();
7421        let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
7422        // Device-side embedding when the run starts the model and the
7423        // embedding matrix is q8_row-mapped.
7424        let ep = embed_ids.and_then(|ids| {
7425            self.weights
7426                .embed_tokens
7427                .q8_row_parts()
7428                .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
7429                    idx,
7430                    rows,
7431                    row_scale: rs,
7432                    ids,
7433                    mult: self.embed_multiplier,
7434                })
7435        });
7436        if embed_ids.is_some() && ep.is_none() {
7437            return li0;
7438        }
7439        if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
7440            return li0;
7441        }
7442        drop(io);
7443        drop(layers);
7444        // CPU caches stay the owners of record: append the chunk rows
7445        // and bank the importance masses per layer.
7446        for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
7447            let li = li0 + i;
7448            let layer = &mut self.kv_cache.layers[li];
7449            for bi in 0..b {
7450                layer.append(
7451                    &ok[bi * row..(bi + 1) * row],
7452                    &ov[bi * row..(bi + 1) * row],
7453                    &[],
7454                );
7455            }
7456            layer.accumulate_imp(oi);
7457        }
7458        last
7459    }
7460
7461    /// Is layer `li` a sliding-window (local-RoPE) layer? Gemma-3:
7462    /// every `pattern`-th layer is global, the rest are local.
7463    fn layer_is_local(&self, li: usize) -> bool {
7464        if let Some(layers) = &self.sliding_layers {
7465            return layers.get(li).copied().unwrap_or(false);
7466        }
7467        match self.swa {
7468            Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
7469            None => false,
7470        }
7471    }
7472
7473    /// The RoPE table for layer `li` (local layers may have their own;
7474    /// Gemma-4 global layers use the proportional padded table).
7475    fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
7476        if self.layer_is_local(li) {
7477            if let Some(f) = &self.inv_freq_local {
7478                return f.clone();
7479            }
7480        } else if let Some(f) = &self.inv_freq_global {
7481            return f.clone();
7482        }
7483        self.inv_freq.clone()
7484    }
7485
7486    /// The attend window for layer `li` (None = full context).
7487    fn layer_window(&self, li: usize) -> Option<usize> {
7488        self.swa
7489            .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
7490    }
7491
7492    fn layer_num_heads(&self, li: usize) -> usize {
7493        self.attention_heads_per_layer
7494            .as_ref()
7495            .and_then(|v| v.get(li).copied())
7496            .unwrap_or(self.num_heads)
7497    }
7498
7499    fn layer_rope_scale(&self, li: usize) -> f32 {
7500        if self.layer_is_local(li) {
7501            self.rope_scale_local
7502        } else {
7503            self.rope_scale
7504        }
7505    }
7506
7507    /// Attention geometry of layer `li`: (num_kv_heads, head_dim,
7508    /// rotary_dim). Gemma-4 global layers override all three.
7509    fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
7510        if !self.layer_is_local(li) {
7511            if let Some((ghd, gkv)) = self.global_attn {
7512                return (gkv, ghd, ghd);
7513            }
7514        }
7515        (
7516            self.num_kv_heads,
7517            self.head_dim,
7518            if self.layer_is_local(li) {
7519                self.rotary_dim_local.unwrap_or(self.rotary_dim)
7520            } else {
7521                self.rotary_dim
7522            },
7523        )
7524    }
7525
7526    /// Forward one position through all layers (hybrid dispatch).
7527    fn forward_layers(
7528        &mut self,
7529        hidden: &[f32],
7530        position: usize,
7531        task_mask: Option<&TaskMask>,
7532    ) -> Vec<f32> {
7533        let out = self.forward_layers_upto(hidden, position, task_mask, None);
7534        self.o1_progress();
7535        out
7536    }
7537
7538    // ── Network pipeline-split building blocks (coordinator/worker) ──
7539    // A remote worker owns layers [from ..= upto] and their KV; the
7540    // coordinator owns the rest plus embed / final norm / head. Attention
7541    // causality is per-layer, so a whole prompt's boundary hiddens ship
7542    // as one batch and decode ships one vector per token.
7543
7544    /// Embed one token id (embed multiplier applied).
7545    pub fn embed_id(&self, id: u32) -> Vec<f32> {
7546        self.embed_single(id)
7547    }
7548
7549    /// Refuse the archs/modes whose forward cannot be cut at a layer
7550    /// boundary. Loud by design: a split that silently changed the math
7551    /// would be a chimera.
7552    pub fn split_supported(&self) -> Result<(), String> {
7553        if self.dsv4.is_some() {
7554            return Err(
7555                "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
7556            );
7557        }
7558        if self.dsv41.is_some() {
7559            return Err(
7560                "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
7561                    .into(),
7562            );
7563        }
7564        if self.qwen4_exp.is_some() {
7565            return Err(
7566                "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
7567            );
7568        }
7569        if self.g3n.is_some() {
7570            return Err(
7571                "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
7572            );
7573        }
7574        Ok(())
7575    }
7576
7577    /// Forward `hidden` through layers [from ..= upto] at `position`,
7578    /// appending those layers' KV/state. Both split sides call this
7579    /// over their own range; a task mask applies to the span's own
7580    /// layers (each side masks what it runs).
7581    pub fn forward_span(
7582        &mut self,
7583        hidden: &[f32],
7584        position: usize,
7585        from: usize,
7586        upto: usize,
7587        task_mask: Option<&TaskMask>,
7588    ) -> Result<Vec<f32>, String> {
7589        self.split_supported()?;
7590        if from > upto || upto >= self.num_layers {
7591            return Err(format!(
7592                "forward_span: layer range {from}..={upto} outside 0..{}",
7593                self.num_layers
7594            ));
7595        }
7596        if hidden.len() != self.hidden_size {
7597            return Err(format!(
7598                "forward_span: hidden len {} ≠ hidden_size {}",
7599                hidden.len(),
7600                self.hidden_size
7601            ));
7602        }
7603        let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
7604        self.o1_progress();
7605        if self
7606            .graph_failed
7607            .swap(false, std::sync::atomic::Ordering::Relaxed)
7608        {
7609            self.cancel
7610                .store(false, std::sync::atomic::Ordering::Relaxed);
7611            self.clear_sequence_state();
7612            return Err("forward_span: deferred O(1) transition failed".into());
7613        }
7614        Ok(out)
7615    }
7616
7617    /// Final norm + lm_head over a boundary hidden (the final-logit
7618    /// softcap is applied by lm_head_forward itself).
7619    pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
7620        let normed = inference::rms_norm(
7621            hidden,
7622            &self.weights.final_norm,
7623            self.rms_eps,
7624            self.norm_style,
7625        );
7626        self.lm_head_forward(&normed)
7627    }
7628
7629    /// Sample the next token with this pipeline's sampler state.
7630    pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
7631        sampler::sample_with_scratch(
7632            logits,
7633            &self.sampler_config,
7634            past_tokens,
7635            &mut self.rng,
7636            &mut self.sampler_scratch,
7637        )
7638    }
7639
7640    /// Fresh sequence: clear KV, reuse history and device mirrors.
7641    pub fn reset_session(&mut self) {
7642        self.clear_sequence_state();
7643    }
7644
7645    /// Batched span prefill from token ids (coordinator side): embed +
7646    /// layers [0 ..= upto]; returns the boundary hiddens of ALL positions
7647    /// (ids.len() × hidden). Rides the same layer-major machinery as the
7648    /// local prefill; falls back to the per-position walk under
7649    /// CMF_PREFILL=seq.
7650    pub fn prefill_span_ids(
7651        &mut self,
7652        ids: &[u32],
7653        start_pos: usize,
7654        upto: usize,
7655        task_mask: Option<&TaskMask>,
7656    ) -> Result<Vec<f32>, String> {
7657        self.split_supported()?;
7658        if upto >= self.num_layers {
7659            return Err(format!(
7660                "prefill_span_ids: upto {upto} outside 0..{}",
7661                self.num_layers
7662            ));
7663        }
7664        // Same predicate as the whole-stack prefill: a span whose GDN
7665        // state lives on the device must walk positions through the
7666        // graph, not through the batched CPU span.
7667        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7668            let out =
7669                self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
7670            self.check_o1_progress_failure("prefill_span_ids")?;
7671            Ok(out)
7672        } else {
7673            let hs = self.hidden_size;
7674            let mut out = Vec::with_capacity(ids.len() * hs);
7675            for (i, &id) in ids.iter().enumerate() {
7676                let emb = self.embed_id(id);
7677                out.extend_from_slice(&self.forward_span(
7678                    &emb,
7679                    start_pos + i,
7680                    0,
7681                    upto,
7682                    task_mask,
7683                )?);
7684            }
7685            Ok(out)
7686        }
7687    }
7688
7689    /// Batched span prefill from boundary hiddens (worker side): layers
7690    /// [from ..= upto] for every position in the batch; returns the batch.
7691    pub fn prefill_span_hidden(
7692        &mut self,
7693        hidden: &[f32],
7694        start_pos: usize,
7695        from: usize,
7696        upto: usize,
7697        task_mask: Option<&TaskMask>,
7698    ) -> Result<Vec<f32>, String> {
7699        self.split_supported()?;
7700        let hs = self.hidden_size;
7701        if hidden.is_empty() || hidden.len() % hs != 0 {
7702            return Err(format!(
7703                "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
7704                hidden.len()
7705            ));
7706        }
7707        if from > upto || upto >= self.num_layers {
7708            return Err(format!(
7709                "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
7710                self.num_layers
7711            ));
7712        }
7713        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7714            let out = self.prefill_batch_span(
7715                PrefillIn::Hidden(hidden),
7716                start_pos,
7717                task_mask,
7718                from,
7719                upto + 1,
7720            );
7721            self.check_o1_progress_failure("prefill_span_hidden")?;
7722            Ok(out)
7723        } else {
7724            let b = hidden.len() / hs;
7725            let mut out = Vec::with_capacity(hidden.len());
7726            for i in 0..b {
7727                let h = self.forward_span(
7728                    &hidden[i * hs..(i + 1) * hs],
7729                    start_pos + i,
7730                    from,
7731                    upto,
7732                    task_mask,
7733                )?;
7734                out.extend_from_slice(&h);
7735            }
7736            Ok(out)
7737        }
7738    }
7739
7740    /// Build the whole-token wgpu graph for a pure-attention q1 model (every
7741    /// layer Full q1 + dense q1 FFN, no gate/bias). Returns the post-stack
7742    /// hidden (caller does final norm + lm_head), or None to fall back.
7743    fn try_token_graph_wgpu(
7744        &self,
7745        hidden: &[f32],
7746        position: usize,
7747        logits_out: &mut Vec<f32>,
7748        layers_run: &mut usize,
7749    ) -> Option<Result<Vec<f32>, ()>> {
7750        self.try_token_graph_wgpu_steps(
7751            hidden,
7752            position,
7753            logits_out,
7754            1,
7755            None,
7756            Some(layers_run),
7757            0,
7758            self.num_layers,
7759        )
7760    }
7761
7762    /// The span twin (network split): the graph covers [from..upto_excl)
7763    /// — one submit per SEGMENT per token. lm_head folds in only when
7764    /// the span reaches the last layer.
7765    fn try_token_graph_wgpu_span(
7766        &self,
7767        hidden: &[f32],
7768        position: usize,
7769        logits_out: &mut Vec<f32>,
7770        from: usize,
7771        upto_excl: usize,
7772        layers_run: &mut usize,
7773    ) -> Option<Result<Vec<f32>, ()>> {
7774        self.try_token_graph_wgpu_steps(
7775            hidden,
7776            position,
7777            logits_out,
7778            1,
7779            None,
7780            Some(layers_run),
7781            from,
7782            upto_excl,
7783        )
7784    }
7785
7786    /// Greedy burst: forward `t_next` and let the device pick + re-embed
7787    /// the next k−1 tokens — k frames, ONE submit, k ids back. The ZML
7788    /// trade, on wgpu. None ⇒ caller keeps the per-token path.
7789    fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
7790        if self.o1_active() || self.attn_softcap > 0.0 {
7791            return None;
7792        }
7793        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
7794        if !graph_on || crate::gpu::graph_unsupported() {
7795            // Same memo as the decode site: this path builds the very
7796            // same graph, so a model it cannot build for must not be
7797            // walked again here either. Missing this guard was worth
7798            // 2.5x on an Adreno — 0.361 tok/s against 0.905 — because
7799            // the burst retried per token what decode had already given
7800            // up on.
7801            return None;
7802        }
7803        let emb = self.embed_single(t_next);
7804        let mut lg = Vec::new();
7805        let mut ids = Vec::new();
7806        match self.try_token_graph_wgpu_steps(
7807            &emb,
7808            position,
7809            &mut lg,
7810            k,
7811            Some(&mut ids),
7812            None,
7813            0,
7814            self.num_layers,
7815        ) {
7816            Some(Ok(_)) => {}
7817            Some(Err(())) => {
7818                // Preserve the backend's post-admission failure through the
7819                // Option-based burst API.  The decode caller consumes this
7820                // flag and clears the sequence instead of falling through
7821                // to a stale CPU recurrent state.
7822                self.graph_failed
7823                    .store(true, std::sync::atomic::Ordering::Relaxed);
7824                return None;
7825            }
7826            None => return None,
7827        }
7828        (ids.len() == k).then_some(ids)
7829    }
7830
7831    /// Multi-step greedy: k whole frames in ONE submit, argmax and re-embed
7832    /// on the device. `ids_out` receives the k winner ids; the hidden/logits
7833    /// outputs are NOT produced in that mode.
7834    fn try_token_graph_wgpu_steps(
7835        &self,
7836        hidden: &[f32],
7837        position: usize,
7838        logits_out: &mut Vec<f32>,
7839        steps: usize,
7840        ids_out: Option<&mut Vec<u32>>,
7841        layers_run: Option<&mut usize>,
7842        from: usize,
7843        upto_excl: usize,
7844    ) -> Option<Result<Vec<f32>, ()>> {
7845        // O(1) Nyström decode runs off the sealed state, not the KV cache the
7846        // graph mirrors — never take the graph while o1 is active.
7847        let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
7848        if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
7849            // Softcapped scores have no graph kernel yet — CPU owns them.
7850            // o1 rides the graph only behind CMF_O1_GPU=1 while the port
7851            // proves itself; without it the CPU path owns o1 as before.
7852            return None;
7853        }
7854        // Per-layer sealed o1 state for the graph. During prefill the
7855        // state is still Collecting -> views are None -> the graph
7856        // refuses below and the CPU prefill records the q trace and
7857        // seals, exactly as the o1 design requires.
7858        let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
7859            .map(|li| {
7860                if !o1_gpu {
7861                    return None;
7862                }
7863                self.kv_cache.layers[self.phys_layer(li)].o1_views()
7864            })
7865            .collect();
7866        if self.o1_active() && o1_gpu {
7867            // Any o1 layer not sealed (or degenerate exact-only) keeps the
7868            // whole token on the CPU: half-graph forwards would desync.
7869            let want: usize = (from..upto_excl)
7870                .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
7871                .count();
7872            let have = o1_views.iter().filter(|v| v.is_some()).count();
7873            if want == 0 || have != want {
7874                // The silent twin of the gpu-side o1 gates, found the
7875                // same way: a 15x decode drop with an empty log. Views
7876                // stay None until the layer's state SEALS, so `have`
7877                // lagging `want` early in a run is the o1 design working
7878                // — but it must say so, or the next reader spends a
7879                // night proving the kernels innocent.
7880                // On CHANGE, not once: the first decline is the legal
7881                // unsealed prefill, and a once-print buries the state
7882                // that matters — what the count reads AFTER the seal.
7883                use std::sync::atomic::{AtomicUsize, Ordering};
7884                static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
7885                let code = have * 1000 + want;
7886                if LAST.swap(code, Ordering::Relaxed) != code {
7887                    tracing::warn!(
7888                        "o1 graph: {have} of {want} layers sealed — per-op until all seal"
7889                    );
7890                }
7891                return None;
7892            }
7893        }
7894        let nh = self.num_heads;
7895        let (nkv, hd, rd) = self.layer_geom(0);
7896        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7897        let mut layers = Vec::with_capacity(upto_excl - from);
7898        let mut model = None;
7899        let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
7900        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7901            if let Some((m, i, kind, rs)) = t
7902                .graph_weight()
7903                .or_else(|| t.graph_weight_descriptor())
7904            {
7905                let name = &m.tensors[i].name;
7906                let prism = if crate::prism::is_inverse_embedding(m, name) {
7907                    crate::gpu::GraphPrismOp::InverseEmbedding
7908                } else if crate::prism::is_forward_weight(m, name) {
7909                    crate::gpu::GraphPrismOp::Forward
7910                } else {
7911                    crate::gpu::GraphPrismOp::None
7912                };
7913                return Some(crate::gpu::GraphW {
7914                    idx: i,
7915                    kind,
7916                    row_scale: rs,
7917                    data: &[],
7918                    prism,
7919                    affine: crate::prism::is_affine_target(m, name),
7920                });
7921            }
7922            // Small unquantized projections (GDN in_proj_a/b) stay f32.
7923            match t.as_f32() {
7924                Some(d) => Some(crate::gpu::GraphW {
7925                    idx: 0,
7926                    kind: 4,
7927                    row_scale: &[],
7928                    data: d,
7929                    prism: crate::gpu::GraphPrismOp::None,
7930                    affine: false,
7931                }),
7932                None => {
7933                    if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
7934                        eprintln!("batch graph: weight has no graph/f32 representation");
7935                    }
7936                    None
7937                }
7938            }
7939        }
7940        for li in from..upto_excl {
7941            let lw = &self.weights.layers[self.phys_layer(li)];
7942            if dbg {
7943                let ak = match &lw.attn {
7944                    AttnKind::Mla(_) => "Mla".into(),
7945                    AttnKind::Full {
7946                        output_gate, bias, ..
7947                    } => format!("Full gate={output_gate} bias={}", bias.is_some()),
7948                    AttnKind::LinearGdn(_) => "LinearGdn".into(),
7949                    AttnKind::Kda(_) => "Kda".into(),
7950                    AttnKind::Linear(_) => "Linear".into(),
7951                    AttnKind::ShortConv(_) => "ShortConv".into(),
7952                };
7953                let fk = match &lw.ffn {
7954                    FfnKind::Dense(_) => "Dense",
7955                    FfnKind::Moe(_) => "Moe",
7956                    FfnKind::DenseMoe(_) => "DenseMoe",
7957                };
7958                eprintln!("graph L{li}: attn={ak} ffn={fk}");
7959            }
7960            let gffn = match &lw.ffn {
7961                FfnKind::DenseMoe(_) => return None, // dual branch: CPU path
7962                // A tube layer is several matrices, not one — the
7963                // whole-layer graph has no shape for it yet.
7964                FfnKind::Dense(d) if !d.segs.is_empty() => return None,
7965                FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7966                    gate: gw(&d.gate_proj)?,
7967                    up: gw(&d.up_proj)?,
7968                    down: gw(&d.down_proj)?,
7969                },
7970                FfnKind::Moe(m) => {
7971                    // Adaptive τ and expert masks keep the CPU path, where
7972                    // they are implemented. Sigmoid routing with a selection
7973                    // bias (LFM2-MoE / DeepSeek noaux_tc), a routed scale ≠ 1
7974                    // and an UNGATED shared expert (HunYuan hy_v3: ×2.826 on
7975                    // the routed mix, the shared expert at weight 1) are all
7976                    // graphed — before, every such token fell to the per-op
7977                    // path whole (145 submits/token on Hy-MT2-30B-A3B).
7978                    if m.route_tau.is_some() || m.mask.is_some() {
7979                        return None;
7980                    }
7981                    let shared = m.shared.as_ref();
7982                    let has_shared = shared.is_some();
7983                    let shared_gated = matches!(shared, Some((_, Some(_))));
7984                    let sgate = match shared {
7985                        Some((_, Some(sg))) => gw(sg)?,
7986                        // No gate (hy_v3) or no shared expert at all: the
7987                        // router weight stands in so the plumbing stays
7988                        // total; the select kernels pin weight 1 or skip.
7989                        _ => gw(&m.router)?,
7990                    };
7991                    let router = gw(&m.router)?;
7992                    // The resident MoE kernels do not yet carry the
7993                    // descriptor-aware transform through router/shared-gate
7994                    // selection.  Refuse the complete layer instead of
7995                    // scoring with an untransformed Prism plane (the dense
7996                    // path has an explicit FWHT boundary below).
7997                    if router.prism != crate::gpu::GraphPrismOp::None
7998                        || sgate.prism != crate::gpu::GraphPrismOp::None
7999                        || router.affine
8000                        || sgate.affine
8001                    {
8002                        tracing::warn!(
8003                            "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
8004                        );
8005                        return None;
8006                    }
8007                    let inter = m.experts.first()?.gate_proj.rows();
8008                    let mut experts = Vec::with_capacity(m.experts.len() + 1);
8009                    // q4t or q4tp, but not both in one layer — the kernels
8010                    // are picked per layer, not per expert.
8011                    let mut q4tp: Option<bool> = None;
8012                    // The mixed 2-bit profile: q2tp gate/up over a q4tp
8013                    // down. Uniform across the layer, like `q4tp` itself.
8014                    let mut gu_q2: Option<bool> = None;
8015                    for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
8016                        if !matches!(e.act, Act::Silu)
8017                            || e.gate_proj.rows() != inter
8018                            || e.up_proj.rows() != inter
8019                        {
8020                            return None;
8021                        }
8022                        // Expert tensors are packed into one resident buffer
8023                        // and the MoE kernels have no transform slot per
8024                        // expert.  Keep the CPU/per-op owner for Prism or
8025                        // affine experts rather than silently using raw bytes.
8026                        for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
8027                            let Some((em, ei, _, _)) = expert_weight
8028                                .graph_weight()
8029                                .or_else(|| expert_weight.graph_weight_descriptor())
8030                            else {
8031                                return None;
8032                            };
8033                            let name = &em.tensors[ei].name;
8034                            if crate::prism::is_forward_weight(em, name)
8035                                || crate::prism::is_inverse_embedding(em, name)
8036                                || crate::prism::is_affine_target(em, name)
8037                            {
8038                                tracing::warn!(
8039                                    "resident MoE declined: expert Prism/affine transform is not implemented"
8040                                );
8041                                return None;
8042                            }
8043                        }
8044                        let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8045                            Some((mm, gi)) => (
8046                                mm,
8047                                gi,
8048                                e.up_proj.mapped_q4t()?.1,
8049                                e.down_proj.mapped_q4t()?.1,
8050                                false,
8051                                false,
8052                            ),
8053                            None => match e.gate_proj.mapped_q2tp() {
8054                                Some((mm, gi)) => (
8055                                    mm,
8056                                    gi,
8057                                    e.up_proj.mapped_q2tp()?.1,
8058                                    e.down_proj.mapped_q4tp()?.1,
8059                                    true,
8060                                    true,
8061                                ),
8062                                None => {
8063                                    let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8064                                    (
8065                                        mm,
8066                                        gi,
8067                                        e.up_proj.mapped_q4tp()?.1,
8068                                        e.down_proj.mapped_q4tp()?.1,
8069                                        true,
8070                                        false,
8071                                    )
8072                                }
8073                            },
8074                        };
8075                        if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
8076                        {
8077                            // The shared expert rides in the same packed
8078                            // buffer as the routed ones, so a layer that
8079                            // mixes layouts cannot be indexed by one stride.
8080                            // Say so: the symptom is a whole model quietly
8081                            // running its MoE on the CPU.
8082                            tracing::warn!(
8083                                "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."
8084                            );
8085                            return None;
8086                        }
8087                        model.get_or_insert_with(|| mm.clone());
8088                        experts.push((gi, ui, di));
8089                    }
8090                    crate::gpu::GraphFfn::Moe {
8091                        router,
8092                        shared_gate: sgate,
8093                        experts,
8094                        n_exp: m.experts.len(),
8095                        // CMF_TOPK_PROBE: timing probe only — output is WRONG.
8096                        // Fewer experts shrink the MoE arithmetic while the
8097                        // dispatch count stays identical, which is the only
8098                        // clean way to tell a launch-bound decode from a
8099                        // compute-bound one.
8100                        top_k: std::env::var("CMF_TOPK_PROBE")
8101                            .ok()
8102                            .and_then(|v| v.parse::<usize>().ok())
8103                            .filter(|k| *k > 0 && *k <= m.top_k)
8104                            .unwrap_or(m.top_k),
8105                        inter,
8106                        norm_topk: m.norm_topk_prob,
8107                        q4tp: q4tp?,
8108                        gu_q2: gu_q2.unwrap_or(false),
8109                        sigmoid: m.router_sigmoid,
8110                        bias: m.expert_bias.as_deref(),
8111                        has_shared,
8112                        shared_gated,
8113                        route_scale: m.routed_scaling,
8114                    }
8115                }
8116            };
8117            let attn = match &lw.attn {
8118                AttnKind::Full {
8119                    wq,
8120                    wk,
8121                    wv,
8122                    wo,
8123                    q_norm,
8124                    k_norm,
8125                    output_gate,
8126                    softplus_gate,
8127                    bias,
8128                } => {
8129                    if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
8130                        return None;
8131                    }
8132                    let (m, _, _, _) = wq
8133                        .graph_weight()
8134                        .or_else(|| wq.graph_weight_descriptor())?;
8135                    model = Some(m.clone());
8136                    crate::gpu::GraphAttn::Full {
8137                        wq: gw(wq)?,
8138                        wk: gw(wk)?,
8139                        wv: gw(wv)?,
8140                        wo: gw(wo)?,
8141                        q_norm: q_norm.as_deref(),
8142                        k_norm: k_norm.as_deref(),
8143                        late_qk_norm: self.qk_norm_after_rope,
8144                        bias: bias
8145                            .as_ref()
8146                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8147                        output_gate: *output_gate,
8148                        cpu_k: self.kv_cache.layers[li].k_heads(),
8149                        cpu_v: self.kv_cache.layers[li].v_heads(),
8150                    }
8151                }
8152                AttnKind::LinearGdn(w) => {
8153                    let cfg = self.gdn_cfg?;
8154                    let (m, _, _, _) = w
8155                        .in_proj_qkv
8156                        .graph_weight()
8157                        .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
8158                    model = Some(m.clone());
8159                    crate::gpu::GraphAttn::Gdn {
8160                        qkv: gw(&w.in_proj_qkv)?,
8161                        z: gw(&w.in_proj_z)?,
8162                        a: gw(&w.in_proj_a)?,
8163                        b: gw(&w.in_proj_b)?,
8164                        out: gw(&w.out_proj)?,
8165                        conv1d: &w.conv1d,
8166                        a_log: &w.a_log,
8167                        dt_bias: &w.dt_bias,
8168                        norm: &w.norm,
8169                        nv: cfg.num_v_heads,
8170                        nk: cfg.num_k_heads,
8171                        dk: cfg.key_head_dim,
8172                        dv: cfg.value_head_dim,
8173                        kk: cfg.conv_kernel,
8174                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8175                    }
8176                }
8177                AttnKind::ShortConv(w) => {
8178                    let cfg = self.short_conv_cfg?;
8179                    let (m, _, _, _) = w
8180                        .in_proj
8181                        .graph_weight()
8182                        .or_else(|| w.in_proj.graph_weight_descriptor())?;
8183                    model = Some(m.clone());
8184                    crate::gpu::GraphAttn::ShortConv {
8185                        inp: gw(&w.in_proj)?,
8186                        out: gw(&w.out_proj)?,
8187                        taps: &w.conv,
8188                        kernel: cfg.kernel,
8189                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8190                    }
8191                }
8192                _ => return None,
8193            };
8194            layers.push(crate::gpu::GraphLayer {
8195                input_norm: &lw.input_norm,
8196                attn,
8197                post_norm: &lw.post_norm,
8198                ffn: gffn,
8199            });
8200        }
8201        let model = model?;
8202        // Fold final-norm + lm_head into the graph when this call wants logits
8203        // and the lm_head is a graphable (quantized) weight — the graph then
8204        // reads back logits (into logits_out) instead of the hidden, dropping
8205        // the separate CPU/GPU lm_head op + its sync. Never the f32 fallback:
8206        // an unquantized lm_head is vocab·hidden and must not be uploaded.
8207        let lm_gw = if upto_excl == self.num_layers
8208            && self.graph_want_logits
8209            && std::env::var("CMF_GPU_LMHEAD")
8210                .map(|v| v != "0")
8211                .unwrap_or(true)
8212        {
8213            self.weights
8214                .lm_head
8215                .graph_weight()
8216                .or_else(|| self.weights.lm_head.graph_weight_descriptor())
8217                .map(|(m, i, kind, rs)| {
8218                let name = &m.tensors[i].name;
8219                let prism = if crate::prism::is_inverse_embedding(m, name) {
8220                    crate::gpu::GraphPrismOp::InverseEmbedding
8221                } else if crate::prism::is_forward_weight(m, name) {
8222                    crate::gpu::GraphPrismOp::Forward
8223                } else {
8224                    crate::gpu::GraphPrismOp::None
8225                };
8226                (
8227                    crate::gpu::GraphW {
8228                        idx: i,
8229                        kind,
8230                        row_scale: rs,
8231                        data: &[],
8232                        prism,
8233                        affine: crate::prism::is_affine_target(m, name),
8234                    },
8235                    self.weights.lm_head.rows(),
8236                )
8237            })
8238        } else {
8239            None
8240        };
8241        let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
8242        // Multi-step re-embeds the winner on the device.
8243        let emb_gw = if steps > 1 {
8244            self.weights
8245                .embed_tokens
8246                .graph_weight()
8247                .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
8248                .map(|(m, i, kind, rs)| {
8249                    let name = &m.tensors[i].name;
8250                    let prism = if crate::prism::is_inverse_embedding(m, name) {
8251                        crate::gpu::GraphPrismOp::InverseEmbedding
8252                    } else if crate::prism::is_forward_weight(m, name) {
8253                        crate::gpu::GraphPrismOp::Forward
8254                    } else {
8255                        crate::gpu::GraphPrismOp::None
8256                    };
8257                    (
8258                        crate::gpu::GraphW {
8259                            idx: i,
8260                            kind,
8261                            row_scale: rs,
8262                            data: &[],
8263                            prism,
8264                            affine: crate::prism::is_affine_target(m, name),
8265                        },
8266                        self.weights.embed_tokens.rows(),
8267                        self.embed_multiplier,
8268                    )
8269                })
8270        } else {
8271            None
8272        };
8273
8274        // Loop boundaries: virtual layer indices after which final_norm is
8275        // applied (mid-stack only; the GLOBAL last layer's norm folds into
8276        // lm_head). Span-relative — the executor compares its enumerate
8277        // index. A span ending mid-stack keeps its boundary norm even when
8278        // it is the span's own last layer.
8279        let loop_norm_at: Vec<usize> = if self.loop_final_norm {
8280            (from..upto_excl.min(self.num_layers - 1))
8281                .filter(|&li| (li + 1) % self.physical_layers == 0)
8282                .map(|li| li - from)
8283                .collect()
8284        } else {
8285            Vec::new()
8286        };
8287        let mut h = hidden.to_vec();
8288        // The normal decode path only needs the fused lm-head logits.  A
8289        // CMF_LOGIT_DUMP diagnostic, however, promises a prompt-boundary
8290        // post-stack hidden alongside those logits; request the existing
8291        // second readback only for that explicit probe instead of dumping
8292        // the input copy left in `h` by a folded-head graph.
8293        let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
8294        let outcome = crate::gpu::forward_token_graph(
8295            &model,
8296            self.graph_kv_id,
8297            &layers,
8298            &o1_views,
8299            self.o1_epoch,
8300            &self.inv_freq,
8301            &mut h,
8302            nh,
8303            nkv,
8304            hd,
8305            self.attn_scale,
8306            rd,
8307            self.hidden_size,
8308            self.intermediate_size,
8309            position,
8310            self.kv_cache.max_seq_len,
8311            gemma,
8312            self.rms_eps as f32,
8313            lm,
8314            &self.weights.final_norm,
8315            logits_out,
8316            &loop_norm_at,
8317            steps,
8318            emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
8319            ids_out,
8320            layers_run,
8321            from,
8322            dump_hidden,
8323        );
8324        match outcome {
8325            crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
8326            crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
8327            crate::gpu::TokenGraphOutcome::Declined => None,
8328        }
8329    }
8330
8331    /// Batched prefill: k contiguous prompt positions through the whole wgpu
8332    /// graph in ONE submit (projections/FFN as GEMMs). `hiddens` is [k·hidden]
8333    /// in/out (embeddings in, layer output out); KV mirror / GDN state advance.
8334    /// false ⇒ unsupported → caller keeps the per-position graph.
8335    /// The b-row Metal graph plan for the whole model: every layer as a
8336    /// GDN run or a full-attention item, all-or-nothing (a layer outside the
8337    /// graph's contract → None, the caller runs plain). Shared by the
8338    /// speculative verify and the batched prefill.
8339    #[cfg(target_os = "macos")]
8340    #[allow(clippy::type_complexity)]
8341    fn metal_rows_plan(
8342        &self,
8343    ) -> Option<(
8344        Vec<MetalRowsItem<'_>>,
8345        std::sync::Arc<cortiq_core::CmfModel>,
8346        Option<crate::gpu_metal::GdnGpuCfg>,
8347    )> {
8348        use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
8349        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
8350        if !graph_force
8351            || !crate::gpu::enabled_here()
8352            || std::env::var("CMF_GPU_BLOCK")
8353                .map(|v| v == "0")
8354                .unwrap_or(false)
8355            || self.attn_softcap > 0.0
8356            || self.o1_active()
8357            || self.swa.is_some()
8358            || self.global_attn.is_some()
8359            || self.attention_heads_per_layer.is_some()
8360            || self.attn_v_norm
8361            || self.loop_final_norm
8362        {
8363            return None;
8364        }
8365        let attend_contract = self.head_dim % 4 == 0
8366            && self.head_dim <= 256
8367            && self.rotary_dim >= 2
8368            && self.rotary_dim <= self.head_dim
8369            && (self.rotary_dim / 2) % 32 == 0
8370            && self.num_kv_heads > 0
8371            && self.num_heads % self.num_kv_heads == 0;
8372        if !attend_contract {
8373            return None;
8374        }
8375        let mut plan: Vec<MetalRowsItem> = Vec::new();
8376        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
8377        for li in 0..self.num_layers {
8378            let lw = &self.weights.layers[self.phys_layer(li)];
8379            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
8380                return None;
8381            }
8382            let ffn = match &lw.ffn {
8383                FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
8384                    let (Some(g), Some(u), Some(dn)) = (
8385                        d.gate_proj.metal_graph_parts(),
8386                        d.up_proj.metal_graph_parts(),
8387                        d.down_proj.metal_graph_parts(),
8388                    ) else {
8389                        return None;
8390                    };
8391                    MetalFfn::Dense {
8392                        gate: g,
8393                        up: u,
8394                        down: dn,
8395                    }
8396                }
8397                _ => return None,
8398            };
8399            match &lw.attn {
8400                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
8401                    let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
8402                        w.in_proj_qkv.metal_graph_parts(),
8403                        w.in_proj_z.metal_graph_parts(),
8404                        w.in_proj_a.f32_parts(),
8405                        w.in_proj_b.f32_parts(),
8406                        w.out_proj.metal_graph_parts(),
8407                    ) else {
8408                        return None;
8409                    };
8410                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
8411                        model_ref.get_or_insert_with(|| model.clone());
8412                    }
8413                    let gl = GdnGpuLayer {
8414                        attn_norm: &lw.input_norm,
8415                        post_norm: &lw.post_norm,
8416                        qkv,
8417                        z,
8418                        a,
8419                        b: bb,
8420                        out,
8421                        ffn,
8422                        conv1d: &w.conv1d,
8423                        a_log: &w.a_log,
8424                        dt_bias: &w.dt_bias,
8425                        gnorm: &w.norm,
8426                    };
8427                    match plan.last_mut() {
8428                        Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
8429                        _ => plan.push(MetalRowsItem::Gdn {
8430                            run: vec![gl],
8431                            first: li,
8432                        }),
8433                    }
8434                }
8435                AttnKind::Full {
8436                    wq,
8437                    wk,
8438                    wv,
8439                    wo,
8440                    q_norm,
8441                    k_norm,
8442                    output_gate,
8443                    softplus_gate: None,
8444                    bias: None,
8445                } => {
8446                    let (Some(pq), Some(pk), Some(pv), Some(po)) =
8447                        (
8448                            wq.metal_graph_parts(),
8449                            wk.metal_graph_parts(),
8450                            wv.metal_graph_parts(),
8451                            wo.metal_graph_parts(),
8452                        )
8453                    else {
8454                        return None;
8455                    };
8456                    if let QTensor::Mapped { model, .. } = wq {
8457                        model_ref.get_or_insert_with(|| model.clone());
8458                    }
8459                    let cache = &self.kv_cache.layers[li];
8460                    if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
8461                        return None;
8462                    }
8463                    plan.push(MetalRowsItem::Attn {
8464                        l: AttnGpuLayer {
8465                            attn_norm: &lw.input_norm,
8466                            post_norm: &lw.post_norm,
8467                            wq: pq,
8468                            wk: pk,
8469                            wv: pv,
8470                            wo: po,
8471                            ffn,
8472                        },
8473                        li,
8474                        q_norm: q_norm.as_deref(),
8475                        k_norm: k_norm.as_deref(),
8476                        output_gate: *output_gate,
8477                    });
8478                }
8479                _ => return None,
8480            }
8481        }
8482        let model = model_ref?;
8483        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
8484            nv: cfg.num_v_heads,
8485            nk: cfg.num_k_heads,
8486            dk: cfg.key_head_dim,
8487            dv: cfg.value_head_dim,
8488            kk: cfg.conv_kernel,
8489            hidden: self.hidden_size,
8490            inter: self.intermediate_size,
8491            c_dim: cfg.conv_dim(),
8492            eps: cfg.rms_eps as f32,
8493            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8494        });
8495        Some((plan, model, gcfg))
8496    }
8497
8498    /// `AttnDeviceParams` for a plan item over the CPU cache as it stands.
8499    #[cfg(target_os = "macos")]
8500    #[allow(clippy::too_many_arguments)]
8501    fn metal_attn_params<'a>(
8502        li: usize,
8503        cache: &'a crate::kv_cache::LayerKvCache,
8504        q_norm: Option<&'a [f32]>,
8505        k_norm: Option<&'a [f32]>,
8506        output_gate: bool,
8507        inv_freq: &'a [f32],
8508        geom: (usize, usize, usize, usize),
8509        pos0: usize,
8510        kv_id: u64,
8511        scale: f32,
8512        eps: f32,
8513        gemma: bool,
8514        late_qk_norm: bool,
8515    ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
8516        let (nh, nkv, hd, rd) = geom;
8517        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8518        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8519        let cpu_stored = cpu_k[0].len() / hd;
8520        (
8521            crate::gpu_metal::AttnDeviceParams {
8522                kv_id,
8523                layer: li,
8524                nh,
8525                nkv,
8526                hd,
8527                rd,
8528                position: pos0,
8529                scale,
8530                eps,
8531                gemma,
8532                late_qk_norm,
8533                output_gate,
8534                q_norm,
8535                k_norm,
8536                inv_freq,
8537                cpu_k,
8538                cpu_v,
8539                cpu_stored,
8540                o1: None,
8541            },
8542            cpu_stored,
8543        )
8544    }
8545
8546    /// Run the rows plan over `hiddens` (b rows at `pos0..`): validate,
8547    /// encode every item, optionally the head, sync. Returns the graph
8548    /// (for the commit / state finish) plus the GDN layer indices and the
8549    /// attention layers with the row count they were encoded against.
8550    #[cfg(target_os = "macos")]
8551    #[allow(clippy::type_complexity)]
8552    fn metal_rows_run(
8553        &mut self,
8554        hiddens: &mut [f32],
8555        pos0: usize,
8556        b: usize,
8557        prefill: bool,
8558        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8559    ) -> MetalRowsRun {
8560        use crate::gpu_metal::{GraphDims, VerifyGraph};
8561        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
8562        for l in &mut self.kv_cache.layers {
8563            if l.linear_state.len() != want && want > 0 {
8564                l.linear_state = vec![0f32; want];
8565            }
8566        }
8567        let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
8568            return MetalRowsRun::Declined;
8569        };
8570        let dims = GraphDims {
8571            hidden: self.hidden_size,
8572            eps: self.rms_eps as f32,
8573            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8574        };
8575        let Some(mut graph) = (if prefill {
8576            VerifyGraph::new_prefill(&model, dims, hiddens, b)
8577        } else {
8578            VerifyGraph::new(&model, dims, hiddens, b)
8579        }) else {
8580            return MetalRowsRun::Declined;
8581        };
8582        let geom = (
8583            self.num_heads,
8584            self.num_kv_heads,
8585            self.head_dim,
8586            self.rotary_dim,
8587        );
8588        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8589        let eps = self.rms_eps as f32;
8590        let kv_id = self.graph_kv_id;
8591        let inv_freq = self.inv_freq.clone();
8592        for item in &plan {
8593            let ok = match item {
8594                MetalRowsItem::Gdn { run, .. } => gcfg
8595                    .as_ref()
8596                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
8597                    .unwrap_or(false),
8598                MetalRowsItem::Attn {
8599                    l,
8600                    li,
8601                    q_norm,
8602                    k_norm,
8603                    output_gate,
8604                } => {
8605                    let (p, _) = Self::metal_attn_params(
8606                        *li,
8607                        &self.kv_cache.layers[*li],
8608                        *q_norm,
8609                        *k_norm,
8610                        *output_gate,
8611                        &inv_freq,
8612                        geom,
8613                        pos0,
8614                        kv_id,
8615                        self.attn_scale,
8616                        eps,
8617                        gemma,
8618                        self.qk_norm_after_rope,
8619                    );
8620                    graph.attn_ok(l, &p)
8621                }
8622            };
8623            if !ok {
8624                use std::sync::atomic::{AtomicBool, Ordering};
8625                static SAID: AtomicBool = AtomicBool::new(false);
8626                if !SAID.swap(true, Ordering::Relaxed) {
8627                    tracing::warn!("metal rows graph: a layer failed preflight — declining");
8628                }
8629                return MetalRowsRun::Declined;
8630            }
8631        }
8632        let lm = match &spec {
8633            Some((lm, _, _)) => {
8634                if !graph.lm_head_ok(*lm) {
8635                    return MetalRowsRun::Declined;
8636                }
8637                Some(*lm)
8638            }
8639            None => None,
8640        };
8641        let mut gdn_layers = Vec::new();
8642        let mut attn_layers = Vec::new();
8643        for item in &plan {
8644            match item {
8645                MetalRowsItem::Gdn { run, first } => {
8646                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
8647                        .iter()
8648                        .map(|l| l.linear_state.as_slice())
8649                        .collect();
8650                    if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
8651                        return MetalRowsRun::Declined;
8652                    }
8653                    gdn_layers.extend(*first..*first + run.len());
8654                }
8655                MetalRowsItem::Attn {
8656                    l,
8657                    li,
8658                    q_norm,
8659                    k_norm,
8660                    output_gate,
8661                } => {
8662                    let (p, cpu_stored) = Self::metal_attn_params(
8663                        *li,
8664                        &self.kv_cache.layers[*li],
8665                        *q_norm,
8666                        *k_norm,
8667                        *output_gate,
8668                        &inv_freq,
8669                        geom,
8670                        pos0,
8671                        kv_id,
8672                        self.attn_scale,
8673                        eps,
8674                        gemma,
8675                        self.qk_norm_after_rope,
8676                    );
8677                    if !graph.encode_attn_b(l, &p) {
8678                        return MetalRowsRun::Declined;
8679                    }
8680                    attn_layers.push((*li, cpu_stored));
8681                }
8682            }
8683        }
8684        if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
8685            if !graph.encode_lm_head_b(final_norm, lm) {
8686                return MetalRowsRun::Declined;
8687            }
8688        }
8689        if !graph.sync() {
8690            return MetalRowsRun::Failed;
8691        }
8692        if let Some((lm, _, logits)) = spec {
8693            logits.resize(b * lm.1, 0.0);
8694            if !graph.read_logits(logits) {
8695                return MetalRowsRun::Failed;
8696            }
8697        }
8698        if !graph.read_hidden(hiddens) {
8699            return MetalRowsRun::Failed;
8700        }
8701        MetalRowsRun::Completed(MetalVerifyPending {
8702            graph,
8703            gdn_layers,
8704            attn_layers,
8705        })
8706    }
8707
8708    /// Native-Metal twin of `try_batch_graph_wgpu`: the b rows through the
8709    /// whole model on the `VerifyGraph` (one submit), the head folded in
8710    /// when `spec` asks; `hiddens` come back as the last layer's output
8711    /// rows, `spec.2` as `[b][lm_rows]` logits. The graph is parked in
8712    /// `metal_verify` for `metal_verify_commit`.
8713    #[cfg(target_os = "macos")]
8714    fn try_batch_graph_metal(
8715        &mut self,
8716        hiddens: &mut [f32],
8717        positions: &[usize],
8718        b: usize,
8719        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8720    ) -> crate::gpu::BatchGraphOutcome {
8721        let _t0 = std::time::Instant::now();
8722        if positions.len() != b
8723            || positions.windows(2).any(|w| w[1] != w[0] + 1)
8724            || hiddens.len() != b * self.hidden_size
8725        {
8726            return crate::gpu::BatchGraphOutcome::Declined;
8727        }
8728        let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec) {
8729            MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
8730            MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
8731            MetalRowsRun::Completed(pending) => pending,
8732        };
8733        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8734            eprintln!(
8735                "metal-verify: {:.1} ms | b={b}",
8736                _t0.elapsed().as_secs_f64() * 1e3
8737            );
8738        }
8739        self.metal_verify = Some(pending);
8740        crate::gpu::BatchGraphOutcome::Completed
8741    }
8742
8743    /// Batched prefill on the Metal rows graph: `ids` (≤ 512) at
8744    /// `start_pos..`, states written in place, K/V rows appended to the
8745    /// CPU caches; optional final norm/head logits are returned in `spec`.
8746    /// Declined means no command buffer was admitted; Failed is terminal.
8747    #[cfg(target_os = "macos")]
8748    fn prefill_rows_metal(
8749        &mut self,
8750        ids: &[u32],
8751        start_pos: usize,
8752        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8753    ) -> MetalPrefillOutcome {
8754        let b = ids.len();
8755        if b == 0 || b > 512 {
8756            return MetalPrefillOutcome::Declined;
8757        }
8758        METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8759        let with_head = spec.is_some();
8760        let hs = self.hidden_size;
8761        let mut hiddens = vec![0f32; b * hs];
8762        for (j, &id) in ids.iter().enumerate() {
8763            let e = self.embed_single(id);
8764            hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
8765        }
8766        let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec) {
8767            MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
8768            MetalRowsRun::Failed => {
8769                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8770                return MetalPrefillOutcome::Failed;
8771            }
8772            MetalRowsRun::Completed(pending) => pending,
8773        };
8774        // states are final: copy them to the owners
8775        let idxs = pending.gdn_layers.clone();
8776        let mut outs: Vec<&mut [f32]> = self
8777            .kv_cache
8778            .layers
8779            .iter_mut()
8780            .enumerate()
8781            .filter(|(i, _)| idxs.binary_search(i).is_ok())
8782            .map(|(_, l)| l.linear_state.as_mut_slice())
8783            .collect();
8784        if !pending.graph.finish_states(&mut outs) {
8785            METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8786            return MetalPrefillOutcome::Failed;
8787        }
8788        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8789        // Read every layer before mutating any CPU cache.  A missing mirror
8790        // row is a terminal graph failure, not a reason to append a partial
8791        // prefix and replay the remainder serially.
8792        let mut rows = Vec::with_capacity(pending.attn_layers.len());
8793        for (li, cpu_stored) in &pending.attn_layers {
8794            let mut kbuf = vec![0f32; b * nkv * hd];
8795            let mut vbuf = vec![0f32; b * nkv * hd];
8796            if !crate::gpu_metal::kv_mirror_read_rows(
8797                self.graph_kv_id,
8798                *li,
8799                nkv,
8800                hd,
8801                *cpu_stored,
8802                b,
8803                &mut kbuf,
8804                &mut vbuf,
8805            ) {
8806                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8807                return MetalPrefillOutcome::Failed;
8808            }
8809            rows.push((*li, *cpu_stored, kbuf, vbuf));
8810        }
8811        for (li, cpu_stored, kbuf, vbuf) in rows {
8812            let cache = &mut self.kv_cache.layers[li];
8813            for r in 0..b {
8814                cache.append(
8815                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8816                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8817                    &[],
8818                );
8819            }
8820            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
8821        }
8822        METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8823        if with_head {
8824            METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8825        }
8826        MetalPrefillOutcome::Completed(hiddens)
8827    }
8828
8829    #[cfg(target_os = "macos")]
8830    fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
8831        self.prefill_rows_metal(ids, start_pos, None)
8832    }
8833
8834    /// Exact teacher-forced NLL through the ordinary Metal rows graph.  This
8835    /// is intentionally separate from the serial TokenGraph scorer: every
8836    /// chunk owns a real b-row graph/head completion and the recurrent/KV
8837    /// handoff is committed before the next chunk begins.
8838    #[cfg(target_os = "macos")]
8839    fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
8840        if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
8841            return MetalBatchNllOutcome::Declined;
8842        }
8843        let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
8844            return MetalBatchNllOutcome::Declined;
8845        };
8846        let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
8847            .ok()
8848            .and_then(|v| v.parse::<usize>().ok())
8849            .filter(|&v| (1..=512).contains(&v))
8850            .unwrap_or(32);
8851        let final_norm = self.weights.final_norm.clone();
8852        let mut nll = 0.0f64;
8853        let mut count = 0usize;
8854        let mut pos = 0usize;
8855        let mut completed = 0usize;
8856        while pos < ids.len() {
8857            let end = (pos + chunk).min(ids.len());
8858            let mut logits = Vec::new();
8859            let outcome = self.prefill_rows_metal(
8860                &ids[pos..end],
8861                pos,
8862                Some((lm, &final_norm, &mut logits)),
8863            );
8864            match outcome {
8865                MetalPrefillOutcome::Declined => {
8866                    return if completed == 0 {
8867                        MetalBatchNllOutcome::Declined
8868                    } else {
8869                        MetalBatchNllOutcome::Failed(format!(
8870                            "ordinary Metal NLL batch declined after {completed} chunks"
8871                        ))
8872                    };
8873                }
8874                MetalPrefillOutcome::Failed => {
8875                    return MetalBatchNllOutcome::Failed(
8876                        "ordinary Metal NLL batch failed after admission".to_string(),
8877                    );
8878                }
8879                MetalPrefillOutcome::Completed(_) => {}
8880            }
8881            completed += 1;
8882            let vocab = self.vocab_size.min(lm.1);
8883            if logits.len() != (end - pos) * lm.1 || vocab == 0 {
8884                return MetalBatchNllOutcome::Failed(
8885                    "ordinary Metal NLL head returned an invalid shape".to_string(),
8886                );
8887            }
8888            for row in 0..(end - pos) {
8889                let absolute = pos + row;
8890                if absolute < start || absolute + 1 >= ids.len() {
8891                    continue;
8892                }
8893                let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
8894                if let Some(mu) = self.logit_multiplier {
8895                    for v in lg.iter_mut() {
8896                        *v *= mu;
8897                    }
8898                }
8899                if let Some(c) = self.final_softcap {
8900                    for v in lg.iter_mut() {
8901                        *v = c * (*v / c).tanh();
8902                    }
8903                }
8904                let target = ids[absolute + 1] as usize;
8905                if target >= vocab {
8906                    return MetalBatchNllOutcome::Failed(format!(
8907                        "target token {target} exceeds Metal head rows {vocab}"
8908                    ));
8909                }
8910                let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8911                let lse: f64 = lg
8912                    .iter()
8913                    .map(|&v| ((v - max) as f64).exp())
8914                    .sum::<f64>()
8915                    .ln()
8916                    + max as f64;
8917                nll += lse - lg[target] as f64;
8918                count += 1;
8919            }
8920            pos = end;
8921        }
8922        MetalBatchNllOutcome::Completed(nll, count)
8923    }
8924
8925    /// Commit a Metal verify round: replay the GDN recurrences over the
8926    /// `a + 1` accepted positions into the CPU states, append the accepted
8927    /// K/V rows from the mirrors to the CPU caches, re-point the mirrors.
8928    #[cfg(target_os = "macos")]
8929    fn metal_verify_commit(&mut self, a: usize) -> bool {
8930        let Some(mut pending) = self.metal_verify.take() else {
8931            return false;
8932        };
8933        let n = a + 1;
8934        // encode order == ascending layer order (the plan walks 0..layers)
8935        let idxs = pending.gdn_layers.clone();
8936        let mut outs: Vec<&mut [f32]> = self
8937            .kv_cache
8938            .layers
8939            .iter_mut()
8940            .enumerate()
8941            .filter(|(i, _)| idxs.binary_search(i).is_ok())
8942            .map(|(_, l)| l.linear_state.as_mut_slice())
8943            .collect();
8944        if !pending.graph.commit(n, &mut outs) {
8945            return false;
8946        }
8947        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8948        // Read every layer before mutating any CPU cache.  Missing rows are
8949        // terminal after the replay has executed; never append a partial KV
8950        // prefix and continue on a serial path.
8951        let mut rows = Vec::with_capacity(pending.attn_layers.len());
8952        for (li, cpu_stored) in &pending.attn_layers {
8953            let mut kbuf = vec![0f32; n * nkv * hd];
8954            let mut vbuf = vec![0f32; n * nkv * hd];
8955            if !crate::gpu_metal::kv_mirror_read_rows(
8956                self.graph_kv_id,
8957                *li,
8958                nkv,
8959                hd,
8960                *cpu_stored,
8961                n,
8962                &mut kbuf,
8963                &mut vbuf,
8964            ) {
8965                return false;
8966            }
8967            rows.push((*li, *cpu_stored, kbuf, vbuf));
8968        }
8969        for (li, cpu_stored, kbuf, vbuf) in rows {
8970            let cache = &mut self.kv_cache.layers[li];
8971            for r in 0..n {
8972                cache.append(
8973                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8974                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8975                    &[],
8976                );
8977            }
8978            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
8979        }
8980        true
8981    }
8982
8983    /// The round's warm-ups as ONE b-row graph run over the MTP block on
8984    /// Metal: `pairs` = (trunk hidden, next token) at consecutive positions
8985    /// from `first_pos`; the block's input projection is folded in, the
8986    /// appended K/V rows are pulled into the CPU MTP cache. False = the
8987    /// graph declined (nothing appended).
8988    #[cfg(target_os = "macos")]
8989    fn mtp_warm_batch_metal(
8990        &mut self,
8991        m: &mut MtpModule,
8992        pairs: &[(&[f32], u32)],
8993        first_pos: usize,
8994    ) -> bool {
8995        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
8996        let b = pairs.len();
8997        if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
8998            return false;
8999        }
9000        let AttnKind::Full {
9001            wq,
9002            wk,
9003            wv,
9004            wo,
9005            q_norm,
9006            k_norm,
9007            output_gate,
9008            softplus_gate: None,
9009            bias: None,
9010        } = &m.layer.attn
9011        else {
9012            return false;
9013        };
9014        let FfnKind::Dense(d) = &m.layer.ffn else {
9015            return false;
9016        };
9017        if !d.segs.is_empty() {
9018            return false;
9019        }
9020        let (Some(pq), Some(pk), Some(pv), Some(po)) =
9021            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
9022        else {
9023            return false;
9024        };
9025        let (Some(g), Some(u), Some(dn)) = (
9026            d.gate_proj.q1_parts(),
9027            d.up_proj.q1_parts(),
9028            d.down_proj.q1_parts(),
9029        ) else {
9030            return false;
9031        };
9032        let Some(eh) = m.eh_proj.q1_parts() else {
9033            return false;
9034        };
9035        let QTensor::Mapped { model, .. } = wq else {
9036            return false;
9037        };
9038        let model = model.clone();
9039        let hs = self.hidden_size;
9040        // [enorm(embed(tok)); hnorm(hidden)] rows
9041        let mut cat = vec![0f32; b * 2 * hs];
9042        for (j, (h, tok)) in pairs.iter().enumerate() {
9043            let e = self.embed_single(*tok);
9044            let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
9045            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
9046            inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
9047        }
9048        let dims = GraphDims {
9049            hidden: hs,
9050            eps: self.rms_eps as f32,
9051            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9052        };
9053        let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
9054            return false;
9055        };
9056        let l = AttnGpuLayer {
9057            attn_norm: &m.layer.input_norm,
9058            post_norm: &m.layer.post_norm,
9059            wq: pq,
9060            wk: pk,
9061            wv: pv,
9062            wo: po,
9063            ffn: MetalFfn::Dense {
9064                gate: g,
9065                up: u,
9066                down: dn,
9067            },
9068        };
9069        let (nh, nkv, hd, rd) = (
9070            self.num_heads,
9071            self.num_kv_heads,
9072            self.head_dim,
9073            self.rotary_dim,
9074        );
9075        let inv_freq = self.inv_freq.clone();
9076        let cpu_stored;
9077        {
9078            let cache = &m.kv;
9079            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9080            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9081            cpu_stored = cpu_k[0].len() / hd;
9082            if cpu_stored != first_pos {
9083                return false;
9084            }
9085            let p = AttnDeviceParams {
9086                kv_id: self.mtp_kv_id(),
9087                layer: Self::MTP_LAYER_BASE,
9088                nh,
9089                nkv,
9090                hd,
9091                rd,
9092                position: first_pos,
9093                scale: self.attn_scale,
9094                eps: self.rms_eps as f32,
9095                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9096                late_qk_norm: self.qk_norm_after_rope,
9097                output_gate: *output_gate,
9098                q_norm: q_norm.as_deref(),
9099                k_norm: k_norm.as_deref(),
9100                inv_freq: &inv_freq,
9101                cpu_k,
9102                cpu_v,
9103                cpu_stored,
9104                o1: None,
9105            };
9106            if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
9107                return false;
9108            }
9109        }
9110        if !graph.sync() {
9111            return false;
9112        }
9113        let mut kbuf = vec![0f32; b * nkv * hd];
9114        let mut vbuf = vec![0f32; b * nkv * hd];
9115        if !crate::gpu_metal::kv_mirror_read_rows(
9116            self.mtp_kv_id(),
9117            Self::MTP_LAYER_BASE,
9118            nkv,
9119            hd,
9120            cpu_stored,
9121            b,
9122            &mut kbuf,
9123            &mut vbuf,
9124        ) {
9125            return false;
9126        }
9127        for r in 0..b {
9128            m.kv.append(
9129                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9130                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9131                &[],
9132            );
9133        }
9134        crate::gpu_metal::kv_mirror_set_stored(
9135            self.mtp_kv_id(),
9136            Self::MTP_LAYER_BASE,
9137            cpu_stored + b,
9138        );
9139        true
9140    }
9141
9142    /// Draft-head shortlist size: `CMF_DRAFT_VOCAB` rows (default 65536,
9143    /// capped at the head; 0 = full head).
9144    fn draft_vocab_rows(head_rows: usize) -> usize {
9145        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9146        let n = *N.get_or_init(|| {
9147            std::env::var("CMF_DRAFT_VOCAB")
9148                .ok()
9149                .and_then(|v| v.parse().ok())
9150                .unwrap_or(65536)
9151        });
9152        if n == 0 { head_rows } else { n.min(head_rows) }
9153    }
9154
9155    /// One MTP block step on the native Metal token graph: block input on
9156    /// the host, the attention layer + FFN device-resident over the MTP
9157    /// mirror, the head folded in when `want_logits`. The appended K/V row
9158    /// is pulled into the CPU MTP cache (owner of record) after the sync.
9159    #[cfg(target_os = "macos")]
9160    fn mtp_step_metal(
9161        &mut self,
9162        m: &mut MtpModule,
9163        hidden: &[f32],
9164        next_token: u32,
9165        position: usize,
9166        want_logits: bool,
9167    ) -> Option<(Vec<f32>, Vec<f32>)> {
9168        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
9169        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9170            || !crate::gpu::q1_force()
9171            || !crate::gpu::enabled_here()
9172            || self.attn_softcap > 0.0
9173            || self.attention_heads_per_layer.is_some()
9174            || m.kv.mode != crate::kv_cache::KvMode::F32
9175            || m.kv.o1.is_some()
9176        {
9177            return None;
9178        }
9179        let AttnKind::Full {
9180            wq,
9181            wk,
9182            wv,
9183            wo,
9184            q_norm,
9185            k_norm,
9186            output_gate,
9187            softplus_gate: None,
9188            bias: None,
9189        } = &m.layer.attn
9190        else {
9191            return None;
9192        };
9193        let FfnKind::Dense(d) = &m.layer.ffn else {
9194            return None;
9195        };
9196        if d.act != Act::Silu || !d.segs.is_empty() {
9197            return None;
9198        }
9199        let (pq, pk, pv, po) = (
9200            wq.q1_parts()?,
9201            wk.q1_parts()?,
9202            wv.q1_parts()?,
9203            wo.q1_parts()?,
9204        );
9205        let (g, u, dn) = (
9206            d.gate_proj.q1_parts()?,
9207            d.up_proj.q1_parts()?,
9208            d.down_proj.q1_parts()?,
9209        );
9210        let QTensor::Mapped { model, .. } = wq else {
9211            return None;
9212        };
9213        let model = model.clone();
9214        let lm = if want_logits {
9215            Some(self.weights.lm_head.q1_parts()?)
9216        } else {
9217            None
9218        };
9219        let dims = GraphDims {
9220            hidden: self.hidden_size,
9221            eps: self.rms_eps as f32,
9222            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9223        };
9224        // The block input `eh_proj · [enorm(e); hnorm(h)]` rides in the
9225        // graph (one submit a step); the host per-op matvec if it cannot.
9226        let hs = self.hidden_size;
9227        let mut x = vec![0f32; hs];
9228        let mut graph = TokenGraph::new(&model, dims, &x)?;
9229        let mut folded = false;
9230        if let Some(eh) = m.eh_proj.q1_parts() {
9231            let e = self.embed_single(next_token);
9232            let mut cat = vec![0.0f32; 2 * hs];
9233            let (cat_e, cat_h) = cat.split_at_mut(hs);
9234            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9235            inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9236            folded = graph.encode_input_proj(eh, &cat);
9237        }
9238        if !folded {
9239            x = self.mtp_block_input(m, hidden, next_token);
9240            graph = TokenGraph::new(&model, dims, &x)?;
9241        }
9242        let l = AttnGpuLayer {
9243            attn_norm: &m.layer.input_norm,
9244            post_norm: &m.layer.post_norm,
9245            wq: pq,
9246            wk: pk,
9247            wv: pv,
9248            wo: po,
9249            ffn: MetalFfn::Dense {
9250                gate: g,
9251                up: u,
9252                down: dn,
9253            },
9254        };
9255        let (nh, nkv, hd, rd) = (
9256            self.num_heads,
9257            self.num_kv_heads,
9258            self.head_dim,
9259            self.rotary_dim,
9260        );
9261        let inv_freq = self.inv_freq.clone();
9262        {
9263            let cache = &m.kv;
9264            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9265            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9266            let cpu_stored = cpu_k[0].len() / hd;
9267            let p = AttnDeviceParams {
9268                kv_id: self.mtp_kv_id(),
9269                layer: Self::MTP_LAYER_BASE,
9270                nh,
9271                nkv,
9272                hd,
9273                rd,
9274                position,
9275                scale: self.attn_scale,
9276                eps: self.rms_eps as f32,
9277                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9278                late_qk_norm: self.qk_norm_after_rope,
9279                output_gate: *output_gate,
9280                q_norm: q_norm.as_deref(),
9281                k_norm: k_norm.as_deref(),
9282                inv_freq: &inv_freq,
9283                cpu_k,
9284                cpu_v,
9285                cpu_stored,
9286                o1: None,
9287            };
9288            if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
9289                return None;
9290            }
9291        }
9292        // The draft's head over a vocabulary SHORTLIST (the first
9293        // CMF_DRAFT_VOCAB rows — BPE ids run roughly by merge rank, so the
9294        // low ids carry the mass): the verify keeps the full head, so a true
9295        // token past the cut is only a rejected draft, never a wrong token.
9296        // 662 MB a step on Qwen3.8 becomes 170 MB at 65536.
9297        let draft_rows = if let Some(lm) = lm {
9298            Self::draft_vocab_rows(lm.1)
9299        } else {
9300            0
9301        };
9302        if let Some(lm) = lm {
9303            if !graph.lm_head_ok(lm) {
9304                return None;
9305            }
9306            if draft_rows < lm.1 {
9307                if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
9308                    return None;
9309                }
9310            } else {
9311                graph.encode_lm_head(&m.final_norm, lm);
9312            }
9313        }
9314        if graph.sync_checked().is_err() {
9315            return None;
9316        }
9317        let mut logits = Vec::new();
9318        if let Some(lm) = lm {
9319            let n_read = draft_rows.min(lm.1).min(self.vocab_size);
9320            logits = attention::take_buf(n_read);
9321            graph.read_logits(&mut logits);
9322            // ids past the shortlist: never drafted (−∞ in every chain)
9323            logits.resize(self.vocab_size, f32::NEG_INFINITY);
9324        }
9325        graph.finish(&mut x);
9326        let mut krow = attention::take_buf(nkv * hd);
9327        let mut vrow = attention::take_buf(nkv * hd);
9328        if crate::gpu_metal::kv_mirror_read_last(
9329            self.mtp_kv_id(),
9330            Self::MTP_LAYER_BASE,
9331            nkv,
9332            hd,
9333            &mut krow,
9334            &mut vrow,
9335        ) {
9336            m.kv.append(&krow, &vrow, &[]);
9337        }
9338        attention::recycle_buf(&mut krow);
9339        attention::recycle_buf(&mut vrow);
9340        Some((logits, x))
9341    }
9342
9343    fn try_batch_graph_wgpu(
9344        &self,
9345        hiddens: &mut [f32],
9346        positions: &[usize],
9347        k: usize,
9348        spec: Option<crate::gpu::SpecTail<'_>>,
9349    ) -> crate::gpu::BatchGraphOutcome {
9350        let _tb = std::time::Instant::now();
9351        let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
9352        if self.attn_softcap > 0.0 {
9353            return crate::gpu::BatchGraphOutcome::Declined; // capped scores: no graph kernel — CPU path
9354        }
9355        let nh = self.num_heads;
9356        let (nkv, hd, rd) = self.layer_geom(0);
9357        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9358        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9359            if let Some((m, i, kind, rs)) = t
9360                .graph_weight()
9361                .or_else(|| t.graph_weight_descriptor())
9362            {
9363                let name = &m.tensors[i].name;
9364                let prism = if crate::prism::is_inverse_embedding(m, name) {
9365                    crate::gpu::GraphPrismOp::InverseEmbedding
9366                } else if crate::prism::is_forward_weight(m, name) {
9367                    crate::gpu::GraphPrismOp::Forward
9368                } else {
9369                    crate::gpu::GraphPrismOp::None
9370                };
9371                return Some(crate::gpu::GraphW {
9372                    idx: i,
9373                    kind,
9374                    row_scale: rs,
9375                    data: &[],
9376                    prism,
9377                    affine: crate::prism::is_affine_target(m, name),
9378                });
9379            }
9380            if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
9381                eprintln!(
9382                    "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
9383                    t.rows(),
9384                    t.cols()
9385                );
9386            }
9387            t.as_f32().map(|d| crate::gpu::GraphW {
9388                idx: 0,
9389                kind: 4,
9390                row_scale: &[],
9391                data: d,
9392                prism: crate::gpu::GraphPrismOp::None,
9393                affine: false,
9394            })
9395        }
9396        let built: Option<(
9397            Vec<crate::gpu::GraphLayer<'_>>,
9398            std::sync::Arc<cortiq_core::CmfModel>,
9399        )> = (|| {
9400            let mut layers = Vec::with_capacity(self.num_layers);
9401            let mut model = None;
9402            for li in 0..self.num_layers {
9403                let lw = &self.weights.layers[self.phys_layer(li)];
9404                // MoE routes per token, so its experts are encoded token by
9405                // token inside the batched submit while attention and the
9406                // projections stay GEMMs. Refusing MoE here is what left
9407                // prefill running one position at a time: 33 tok/s against
9408                // 54 on decode, i.e. reading the prompt was slower than
9409                // writing the answer.
9410                let gffn = match &lw.ffn {
9411                    FfnKind::Dense(d) if !d.segs.is_empty() => {
9412                        if batch_debug {
9413                            eprintln!("batch graph: dense segmented FFN at layer {li}");
9414                        }
9415                        return None;
9416                    }
9417                    FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
9418                        gate: gw(&d.gate_proj)?,
9419                        up: gw(&d.up_proj)?,
9420                        down: gw(&d.down_proj)?,
9421                    },
9422                    FfnKind::Moe(m) => {
9423                        if m.router_sigmoid
9424                            || m.expert_bias.is_some()
9425                            || m.route_tau.is_some()
9426                            || m.mask.is_some()
9427                        {
9428                            return None;
9429                        }
9430                        let (se, sg) = m.shared.as_ref()?;
9431                        let sgate = gw(sg.as_ref()?)?;
9432                        let router = gw(&m.router)?;
9433                        // The batch MoE kernels still consume raw per-token
9434                        // rows and do not carry the descriptor-aware Prism
9435                        // transform/affine bit for router or shared-gate
9436                        // planes.  Refuse rather than route an untransformed
9437                        // source activation.
9438                        if router.prism != crate::gpu::GraphPrismOp::None
9439                            || router.affine
9440                            || sgate.prism != crate::gpu::GraphPrismOp::None
9441                            || sgate.affine
9442                        {
9443                            return None;
9444                        }
9445                        let inter = m.experts.first()?.gate_proj.rows();
9446                        let mut experts = Vec::with_capacity(m.experts.len() + 1);
9447                        let mut q4tp: Option<bool> = None;
9448                        let mut gu_q2: Option<bool> = None;
9449                        for e in m.experts.iter().chain(std::iter::once(se)) {
9450                            if !matches!(e.act, Act::Silu)
9451                                || e.gate_proj.rows() != inter
9452                                || e.up_proj.rows() != inter
9453                            {
9454                                return None;
9455                            }
9456                            // Same ladder as the token graph: q4t → q2tp
9457                            // (mixed profile: 2-bit gate/up over a q4tp
9458                            // down) → q4tp. Uniform across the layer.
9459                            let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
9460                                Some((mm, gi)) => (
9461                                    mm,
9462                                    gi,
9463                                    e.up_proj.mapped_q4t()?.1,
9464                                    e.down_proj.mapped_q4t()?.1,
9465                                    false,
9466                                    false,
9467                                ),
9468                                None => match e.gate_proj.mapped_q2tp() {
9469                                    Some((mm, gi)) => (
9470                                        mm,
9471                                        gi,
9472                                        e.up_proj.mapped_q2tp()?.1,
9473                                        e.down_proj.mapped_q4tp()?.1,
9474                                        true,
9475                                        true,
9476                                    ),
9477                                    None => {
9478                                        let (mm, gi) = e.gate_proj.mapped_q4tp()?;
9479                                        (
9480                                            mm,
9481                                            gi,
9482                                            e.up_proj.mapped_q4tp()?.1,
9483                                            e.down_proj.mapped_q4tp()?.1,
9484                                            true,
9485                                            false,
9486                                        )
9487                                    }
9488                                },
9489                            };
9490                            if *q4tp.get_or_insert(is_p) != is_p
9491                                || *gu_q2.get_or_insert(is_q2) != is_q2
9492                            {
9493                                return None;
9494                            }
9495                            if [gi, ui, di].into_iter().any(|idx| {
9496                                mm.tensors
9497                                    .get(idx)
9498                                    .is_some_and(|t| {
9499                                        crate::prism::is_forward_weight(mm, &t.name)
9500                                            || crate::prism::is_affine_target(mm, &t.name)
9501                                    })
9502                            }) {
9503                                return None;
9504                            }
9505                            model.get_or_insert_with(|| mm.clone());
9506                            experts.push((gi, ui, di));
9507                        }
9508                        crate::gpu::GraphFfn::Moe {
9509                            router,
9510                            shared_gate: sgate,
9511                            experts,
9512                            n_exp: m.experts.len(),
9513                            top_k: m.top_k,
9514                            inter,
9515                            norm_topk: m.norm_topk_prob,
9516                            q4tp: q4tp?,
9517                            gu_q2: gu_q2.unwrap_or(false),
9518                            sigmoid: false,
9519                            bias: None,
9520                            has_shared: true,
9521                            shared_gated: true,
9522                            route_scale: 1.0,
9523                        }
9524                    }
9525                    _ => return None,
9526                };
9527                let attn = match &lw.attn {
9528                    AttnKind::Full {
9529                        wq,
9530                        wk,
9531                        wv,
9532                        wo,
9533                        q_norm,
9534                        k_norm,
9535                        output_gate,
9536                        softplus_gate,
9537                        bias,
9538                    } => {
9539                        if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
9540                            if batch_debug {
9541                                eprintln!(
9542                                    "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
9543                                    softplus_gate.is_some(),
9544                                    self.attention_heads_per_layer.is_some()
9545                                );
9546                            }
9547                            return None;
9548                        }
9549                        let (m, _, _, _) = wq
9550                            .graph_weight()
9551                            .or_else(|| wq.graph_weight_descriptor())?;
9552                        model = Some(m.clone());
9553                        crate::gpu::GraphAttn::Full {
9554                            wq: gw(wq)?,
9555                            wk: gw(wk)?,
9556                            wv: gw(wv)?,
9557                            wo: gw(wo)?,
9558                            q_norm: q_norm.as_deref(),
9559                            k_norm: k_norm.as_deref(),
9560                            late_qk_norm: self.qk_norm_after_rope,
9561                            bias: bias
9562                                .as_ref()
9563                                .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9564                            output_gate: *output_gate,
9565                            cpu_k: self.kv_cache.layers[li].k_heads(),
9566                            cpu_v: self.kv_cache.layers[li].v_heads(),
9567                        }
9568                    }
9569                    AttnKind::LinearGdn(w) => {
9570                        let Some(cfg) = self.gdn_cfg else {
9571                            if batch_debug {
9572                                eprintln!("batch graph: no GDN config at layer {li}");
9573                            }
9574                            return None;
9575                        };
9576                        let (m, _, _, _) = w
9577                            .in_proj_qkv
9578                            .graph_weight()
9579                            .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
9580                        model = Some(m.clone());
9581                        crate::gpu::GraphAttn::Gdn {
9582                            qkv: gw(&w.in_proj_qkv)?,
9583                            z: gw(&w.in_proj_z)?,
9584                            a: gw(&w.in_proj_a)?,
9585                            b: gw(&w.in_proj_b)?,
9586                            out: gw(&w.out_proj)?,
9587                            conv1d: &w.conv1d,
9588                            a_log: &w.a_log,
9589                            dt_bias: &w.dt_bias,
9590                            norm: &w.norm,
9591                            nv: cfg.num_v_heads,
9592                            nk: cfg.num_k_heads,
9593                            dk: cfg.key_head_dim,
9594                            dv: cfg.value_head_dim,
9595                            kk: cfg.conv_kernel,
9596                            cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
9597                        }
9598                    }
9599                    _ => return None,
9600                };
9601                layers.push(crate::gpu::GraphLayer {
9602                    input_norm: &lw.input_norm,
9603                    attn,
9604                    post_norm: &lw.post_norm,
9605                    ffn: gffn,
9606                });
9607            }
9608            Some((layers, model?))
9609        })();
9610        let Some((layers, model)) = built else {
9611            {
9612                use std::sync::atomic::{AtomicBool, Ordering};
9613                static SAID: AtomicBool = AtomicBool::new(false);
9614                if !SAID.swap(true, Ordering::Relaxed) {
9615                    tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
9616                }
9617            }
9618            return crate::gpu::BatchGraphOutcome::Declined;
9619        };
9620        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
9621            eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
9622        }
9623        crate::gpu::forward_batch_graph(
9624            &model,
9625            self.graph_kv_id,
9626            &layers,
9627            &self.inv_freq,
9628            hiddens,
9629            nh,
9630            nkv,
9631            hd,
9632            rd,
9633            self.hidden_size,
9634            self.intermediate_size,
9635            positions,
9636            self.kv_cache.max_seq_len,
9637            gemma,
9638            self.rms_eps as f32,
9639            self.attn_scale,
9640            k,
9641            &(0..self.num_layers)
9642                .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
9643                .collect::<Vec<_>>(),
9644            self.o1_epoch,
9645            spec,
9646        )
9647    }
9648
9649    /// Same, stopping after layer `upto` inclusive (routing probe φ).
9650    /// `CMF_DSV4_DRAFT_PROBE=1` — grade the draft against what the trunk goes on
9651    /// to produce. Off by default; it runs a whole draft per decoded token.
9652    fn draft_probe() -> bool {
9653        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9654        *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
9655    }
9656
9657    /// `CMF_DSV4_DRAFT_PROBE=1`: measure how much of the draft the trunk
9658    /// would have agreed with, WITHOUT verifying or rolling anything back.
9659    ///
9660    /// The number this produces decides the whole speculation design — at
9661    /// acceptance a, a block of B positions yields 1 + a + a² + ... tokens
9662    /// per trunk pass — so it is worth measuring before any of the machinery
9663    /// that would exploit it exists. Each draft is parked with the position
9664    /// it was made at, and graded as the real tokens arrive.
9665    /// `CMF_DSV4_SPEC=1` — the DeepSeek-V4 speculative decode: draft five
9666    /// on the card, verify them in one batched trunk pass, commit the
9667    /// accepted prefix, roll the rest back.
9668    #[cfg(feature = "gpu")]
9669    fn dsv4_spec_on() -> bool {
9670        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9671        *ON.get_or_init(|| {
9672            // Test-only runtime gate: model loading still performs the same
9673            // reservation and trunk packing, which gives rollback parity a
9674            // topology-identical non-speculative control arm.
9675            if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
9676                return v != "0";
9677            }
9678            // An explicit value is a diagnostic force/escape hatch.  With no
9679            // knob, speculation is eligible only when model loading reserved
9680            // its bounded pack.  On small q4tp cards the geometric reserve
9681            // gate deliberately leaves this at zero: trying to build DSpark
9682            // after the exact trunk filled VRAM is both slower and a device
9683            // OOM (measured on A40).
9684            std::env::var("CMF_DSV4_SPEC")
9685                .map(|v| v != "0")
9686                .unwrap_or_else(|_| {
9687                    crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
9688                })
9689        })
9690    }
9691
9692    /// One speculative round at the decode tip. `t_next` is the token the
9693    /// sampler just committed for `next_pos`. Returns the EXTRA accepted
9694    /// tokens (possibly none) and the new position, with `graph_logits`
9695    /// left holding the last accepted position's logits — exactly what the
9696    /// loop top expects. `None` means "speculate not this round": nothing
9697    /// was committed, the caller forwards normally.
9698    #[cfg(feature = "gpu")]
9699    fn dsv4_spec_step(
9700        &mut self,
9701        tip_token: u32,
9702        t_next: u32,
9703        next_pos: usize,
9704        max_extra: usize,
9705        drafted: &mut usize,
9706        accepted_ctr: &mut usize,
9707    ) -> Option<(Vec<u32>, usize)> {
9708        let t_all = std::time::Instant::now();
9709        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9710            thread_local! {
9711                static LAST: std::cell::Cell<Option<std::time::Instant>> =
9712                    const { std::cell::Cell::new(None) };
9713            }
9714            LAST.with(|l| {
9715                if let Some(prev) = l.get() {
9716                    eprintln!(
9717                        "между раундами {:.1} мс",
9718                        prev.elapsed().as_secs_f64() * 1e3
9719                    );
9720                }
9721                l.set(Some(std::time::Instant::now()));
9722            });
9723        }
9724        if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9725            eprintln!("spec_step: вход pos={next_pos}");
9726        }
9727        let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
9728        let cfg = self.dsv4.as_ref().map(|b| b.2)?;
9729        // The draft state and its capture, armed exactly as the probe does.
9730        if self.dspark.is_none() {
9731            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9732            if t.is_empty() {
9733                return None;
9734            }
9735            crate::dsv4::dspark_arm(&t, cfg.dim);
9736            self.dspark = Some(crate::dsv4::DsparkState::new(
9737                self.dsv4_mtp.len(),
9738                &cfg,
9739                t.len(),
9740            ));
9741        }
9742        let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9743        let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
9744        if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9745            eprintln!("spec_step: пак не построился (targets {targets:?})");
9746        }
9747        let pack = pack?;
9748        let block = crate::dsv4::dspark_block();
9749        let b_box = self.dsv4.as_mut()?;
9750        let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
9751        let ds = self.dspark.as_mut()?;
9752        // The tip's captures: either this token ran on a normal path that
9753        // filled the thread-local, or the previous spec round left them.
9754        let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
9755        if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
9756            if dbg {
9757                eprintln!("spec_step: нет захвата");
9758            }
9759            return None;
9760        }
9761        ds.have_hidden = true;
9762        let tip_pos = next_pos.checked_sub(1)?;
9763        let draft_started = std::time::Instant::now();
9764        let mut conf = Vec::new();
9765        let props = crate::dsv4::dspark_draft_gpu(
9766            g,
9767            &self.dsv4_mtp,
9768            &cfg,
9769            ds,
9770            pack,
9771            st.kv_id,
9772            tip_token,
9773            tip_pos,
9774            self.pool.as_deref(),
9775            &mut conf,
9776        );
9777        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9778        *drafted += block;
9779        if props.is_empty() || props[0] != t_next {
9780            if dbg {
9781                eprintln!(
9782                    "spec_step: черновик {} (props0={:?} t_next={t_next})",
9783                    if props.is_empty() {
9784                        "пуст"
9785                    } else {
9786                        "мимо"
9787                    },
9788                    props.first()
9789                );
9790            }
9791            return None;
9792        }
9793        // `fed[0]` is `t_next`, which the outer loop has already committed;
9794        // only `fed[1..]` become additional output tokens. Cap the verify
9795        // transaction itself to the caller's remaining output budget instead
9796        // of merely truncating the returned vector: otherwise the KV/state
9797        // would advance past `max_tokens` and a 64-token request could return
9798        // 66 tokens (and poison a reused session with two invisible steps).
9799        let mut k_verify = crate::dsv4::dspark_verify_k()
9800            .min(props.len())
9801            .min(max_extra.saturating_add(1));
9802        // Adaptive depth: positions the draft itself doubts are paid for on
9803        // every verify and delivered almost never (natural-text survival
9804        // [.67 .50 .29 .08 .04]). `CMF_DSPARK_CONF_MIN=p` trims the fed
9805        // prefix at the first proposal whose confidence drops below p; on
9806        // predictable text the confidences stay high and nothing changes.
9807        let conf_min = {
9808            static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
9809            *M.get_or_init(|| {
9810                std::env::var("CMF_DSPARK_CONF_MIN")
9811                    .ok()
9812                    .and_then(|v| v.parse().ok())
9813                    .unwrap_or(0.0)
9814            })
9815        };
9816        if conf_min > 0.0 && conf.len() >= props.len() {
9817            let mut keep = 1usize;
9818            while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
9819                keep += 1;
9820            }
9821            k_verify = k_verify.min(keep.max(2));
9822        }
9823        if k_verify < 2 {
9824            return None;
9825        }
9826        let mut fed = Vec::with_capacity(k_verify);
9827        fed.push(t_next);
9828        fed.extend_from_slice(&props[1..k_verify]);
9829        let mut argmax = Vec::new();
9830        let mut logits_all = Vec::new();
9831        let mut walked = Vec::new();
9832        let txn = crate::dsv4::dsv4_verify_chunk(
9833            g,
9834            layers,
9835            &cfg,
9836            st,
9837            &fed,
9838            next_pos,
9839            &self.inv_freq,
9840            self.pool.as_deref(),
9841            &targets,
9842            &mut argmax,
9843            &mut logits_all,
9844            &mut walked,
9845        );
9846        if txn.is_none() && dbg {
9847            eprintln!("spec_step: verify отказал");
9848        }
9849        let txn = txn?;
9850        let spec_gpu_end = txn.gpu_end;
9851        let b = fed.len();
9852        let mut accepted = 1usize;
9853        while accepted < b && fed[accepted] == argmax[accepted - 1] {
9854            accepted += 1;
9855        }
9856        // `CMF_DSV4_SPEC_FORCE_REJECT=1` — accept nothing beyond the known
9857        // token, every round: the pure rollback exerciser. The output must
9858        // stay byte-identical to the plain walk; anything else is a
9859        // transaction bug, isolated from the acceptance logic.
9860        if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
9861            accepted = 1;
9862        }
9863        if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
9864            eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
9865        }
9866        let t_fin = std::time::Instant::now();
9867        if !crate::dsv4::dsv4_spec_finish(
9868            g,
9869            layers,
9870            &cfg,
9871            st,
9872            txn,
9873            accepted,
9874            &fed,
9875            &self.inv_freq,
9876            self.pool.as_deref(),
9877        ) {
9878            tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
9879            return None;
9880        }
9881        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9882            eprintln!(
9883                "finish(k={accepted}): {:.1} мс",
9884                t_fin.elapsed().as_secs_f64() * 1e3
9885            );
9886        }
9887        *accepted_ctr += accepted - 1;
9888        // Captures per accepted token: device targets photographed by the
9889        // batch, host targets from the verify's own walk. The last one
9890        // becomes the new tip's draft input; every one owes the ring an
9891        // entry for its position.
9892        let (hc, dim) = (cfg.hc_mult, cfg.dim);
9893        // Complete-chain layers are photographed by the fused submission;
9894        // partial device layers overwrite that slot after exact host cold-
9895        // expert correction.  Thus every target in the contiguous device
9896        // prefix has a valid per-token capture.
9897        let dev_caps: Vec<usize> = targets
9898            .iter()
9899            .copied()
9900            .filter(|&t| t < spec_gpu_end)
9901            .collect();
9902        let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
9903        if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
9904            return None;
9905        }
9906        for t in 0..accepted {
9907            let tip = t + 1 == accepted;
9908            for (slot, &tl) in targets.iter().enumerate() {
9909                if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
9910                    let lo = (di * b + t) * hc * dim;
9911                    crate::dsv4::dspark_capture(
9912                        &caps_all[lo..lo + hc * dim],
9913                        &cfg,
9914                        slot,
9915                        &mut ds.main_hidden,
9916                    );
9917                } else if tip
9918                    && crate::dsv4::dspark_peek_slot(slot, dim, {
9919                        let lo = slot * dim;
9920                        &mut ds.main_hidden[lo..lo + dim]
9921                    })
9922                {
9923                    // The tip's host-layer captures are the walk's own
9924                    // per-layer notes — exact. (The walk that ran last ended
9925                    // on exactly this token, on both the accept-all and the
9926                    // rollback path.)
9927                } else {
9928                    // Intermediate tokens: the post-tail state stands in for
9929                    // the per-layer capture on host targets below the last
9930                    // layer. Ring-entry quality only; the tip is exact.
9931                    crate::dsv4::dspark_capture(
9932                        &walked[t * hc * dim..(t + 1) * hc * dim],
9933                        &cfg,
9934                        slot,
9935                        &mut ds.main_hidden,
9936                    );
9937                }
9938            }
9939            crate::dsv4::dspark_ring_append(
9940                g,
9941                &self.dsv4_mtp,
9942                &cfg,
9943                ds,
9944                next_pos + t,
9945                self.pool.as_deref(),
9946            );
9947        }
9948        let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
9949        self.graph_logits = Some(row);
9950        // The speculative loop never runs the probe, so the trunk tally has
9951        // no other place to cycle. Armed only when someone asked for the
9952        // dump; the host tail is the only tallying path here, which is
9953        // precisely the population a partial pack would serve.
9954        if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
9955            crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
9956            crate::dsv4::pick_tally_arm();
9957        }
9958        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9959            eprintln!(
9960                "spec_step total {:.1} мс (k={accepted})",
9961                t_all.elapsed().as_secs_f64() * 1e3
9962            );
9963        }
9964        Some((fed[1..accepted].to_vec(), next_pos + accepted))
9965    }
9966
9967    fn dspark_probe(&mut self, position: usize, token_id: u32) {
9968        if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
9969            return;
9970        }
9971        // What the trunk just routed to, for this token.
9972        let trunk_now = crate::dsv4::pick_tally_take();
9973        crate::dsv4::trunk_freq_note(&trunk_now);
9974        if !trunk_now.is_empty() {
9975            self.dspark_trunk_picks.push(trunk_now);
9976            let keep = crate::dsv4::dspark_block();
9977            if self.dspark_trunk_picks.len() > keep {
9978                self.dspark_trunk_picks.remove(0);
9979            }
9980        }
9981        // Grade whatever is waiting: the token just decoded sits at
9982        // `position`, so it answers the draft made at `position - 1 - i`.
9983        for p in std::mem::take(&mut self.dspark_pending) {
9984            let Some(i) = position.checked_sub(p.0 + 1) else {
9985                continue;
9986            };
9987            let mut p = p;
9988            if i < p.1.len() {
9989                if p.2 && p.1[i] == token_id {
9990                    p.3 = i + 1;
9991                } else {
9992                    p.2 = false;
9993                }
9994                if i + 1 < p.1.len() {
9995                    self.dspark_pending.push(p);
9996                    continue;
9997                }
9998            }
9999            self.dspark_hist.push(p.3);
10000            self.dspark_real.push(token_id);
10001        }
10002        let Some(b) = &mut self.dsv4 else { return };
10003        let (g, layers, cfg) = (&b.0, &b.1, b.2);
10004        let n_layers = layers.len();
10005        if self.dspark.is_none() {
10006            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
10007            if t.is_empty() {
10008                return;
10009            }
10010            eprintln!(
10011                "DSpark: захват со слоёв {t:?}, блок {}",
10012                crate::dsv4::dspark_block()
10013            );
10014            crate::dsv4::dspark_arm(&t, cfg.dim);
10015            self.dspark = Some(crate::dsv4::DsparkState::new(
10016                self.dsv4_mtp.len(),
10017                &cfg,
10018                t.len(),
10019            ));
10020        }
10021        let ds = self.dspark.as_mut().unwrap();
10022        if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
10023            return; // this token ran on a path that captures nothing
10024        }
10025        let mut conf = Vec::new();
10026        crate::dsv4::pick_tally_arm();
10027        // The trunk has already consumed the adaptive VRAM budget. Until the
10028        // draft owns an explicit bounded device pack, its tensors are an
10029        // out-of-core CPU/disk tier by contract: never let per-op probes try
10030        // to squeeze another multi-gigabyte MTP expert cache onto the card.
10031        let draft_started = std::time::Instant::now();
10032        #[cfg(feature = "gpu")]
10033        let gpu_draft = crate::dsv4::dspark_gpu_on();
10034        #[cfg(not(feature = "gpu"))]
10035        let gpu_draft = false;
10036        let props = if gpu_draft {
10037            #[cfg(feature = "gpu")]
10038            {
10039                let kv_id = b.3.kv_id;
10040                match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
10041                    Some(pk) => crate::dsv4::dspark_draft_gpu(
10042                        g,
10043                        &self.dsv4_mtp,
10044                        &cfg,
10045                        ds,
10046                        pk,
10047                        kv_id,
10048                        token_id,
10049                        position,
10050                        self.pool.as_deref(),
10051                        &mut conf,
10052                    ),
10053                    None => Vec::new(),
10054                }
10055            }
10056            #[cfg(not(feature = "gpu"))]
10057            Vec::new()
10058        } else {
10059            crate::gpu::cpu_scope(|| {
10060                crate::dsv4::dspark_draft(
10061                    g,
10062                    &self.dsv4_mtp,
10063                    &cfg,
10064                    ds,
10065                    token_id,
10066                    position,
10067                    self.pool.as_deref(),
10068                    &mut conf,
10069                )
10070            })
10071        };
10072        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
10073        let draft_picks = crate::dsv4::pick_tally_take();
10074        crate::dsv4::dspark_freq_note(&draft_picks);
10075        // Re-arm for the NEXT trunk token; the probe runs after the forward,
10076        // so this is the only place that can.
10077        crate::dsv4::pick_tally_arm();
10078        if !props.is_empty() {
10079            // Two ratios, side by side: what a batched verify over the trunk
10080            // would read against what it asks for, and the same for the
10081            // draft's three stages. Near 1.0 means a batch amortises nothing.
10082            let (tu, tt) = {
10083                let flat: Vec<(usize, Vec<usize>)> = self
10084                    .dspark_trunk_picks
10085                    .iter()
10086                    .flat_map(|v| v.iter().cloned())
10087                    .collect();
10088                // Per layer, across the window of tokens.
10089                let mut per: std::collections::HashMap<usize, Vec<usize>> =
10090                    std::collections::HashMap::new();
10091                for (li, picks) in flat {
10092                    per.entry(li).or_default().extend(picks);
10093                }
10094                let n = per.len().max(1);
10095                let mut u = 0usize;
10096                let mut t = 0usize;
10097                for (_, v) in per {
10098                    t += v.len();
10099                    u += v.iter().collect::<std::collections::HashSet<_>>().len();
10100                }
10101                (u / n, t / n)
10102            };
10103            let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
10104            self.dspark_exp.push((tu, tt, du, dt));
10105            self.dspark_pending.push((position, props, true, 0));
10106        }
10107        if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
10108            let n = self.dspark_hist.len() as f32;
10109            let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
10110            let block = crate::dsv4::dspark_block();
10111            let mut at = vec![0usize; block + 1];
10112            for &k in &self.dspark_hist {
10113                at[k] += 1;
10114            }
10115            // Prefix survival: S_i = P(the first i positions all held).
10116            let mut surv = Vec::with_capacity(block);
10117            for i in 1..=block {
10118                let k = at[i..].iter().sum::<usize>() as f32 / n;
10119                surv.push(format!("{k:.2}"));
10120            }
10121            let distinct = self
10122                .dspark_real
10123                .iter()
10124                .collect::<std::collections::HashSet<_>>()
10125                .len();
10126            let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
10127                (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
10128            });
10129            let m = self.dspark_exp.len().max(1);
10130            eprintln!(
10131                "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
10132                 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
10133                self.dspark_hist.len(),
10134                mean + 1.0,
10135                surv.join(" ")
10136            );
10137            eprintln!(
10138                "DSpark: разных токенов {distinct} из {} (вырожденность), \
10139                 эксперты ствол {}/{} на слой за {block} токенов, \
10140                 черновик {}/{} за блок, draft {:.2} мс/блок",
10141                self.dspark_real.len(),
10142                tu / m,
10143                tt / m,
10144                du / m,
10145                dt / m,
10146                self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
10147            );
10148        }
10149    }
10150
10151    fn forward_layers_upto(
10152        &mut self,
10153        hidden: &[f32],
10154        position: usize,
10155        task_mask: Option<&TaskMask>,
10156        upto: Option<usize>,
10157    ) -> Vec<f32> {
10158        // In-process multi-GPU: each segment runs pinned to its card,
10159        // and the only thing crossing the boundary is one hidden vector
10160        // that never leaves this address space. Same layer split the
10161        // network mode does, minus the second process, the socket, the
10162        // serialization and the dir_hash handshake.
10163        if let Some(plan) = self.gpu_plan.clone() {
10164            if upto.is_none() && plan.len() > 1 {
10165                let mut h = hidden.to_vec();
10166                for &(dev, from, upto_incl) in plan.iter() {
10167                    h = crate::gpu::with_device(dev, || {
10168                        self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
10169                    });
10170                }
10171                return h;
10172            }
10173        }
10174        self.forward_layers_span(hidden, position, task_mask, 0, upto)
10175    }
10176
10177    /// Split this pipeline's layer stack across local GPUs: segment i
10178    /// runs on `devices[i]`. Contiguous and even by layer count — the
10179    /// VRAM-weighted planner is the next step, and an uneven card pair
10180    /// is why it will be needed. `None` clears the plan.
10181    pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
10182        self.set_gpu_plan_at(devices, None)
10183    }
10184
10185    /// The same, with an explicit first boundary (`--peer-split`): card
10186    /// 0 takes layers `[0..at)`, the rest split what remains. Uneven
10187    /// cards, or an attention-heavy head, are why this knob exists.
10188    pub fn set_gpu_plan_at(
10189        &mut self,
10190        devices: Option<&[usize]>,
10191        at: Option<usize>,
10192    ) -> Result<(), String> {
10193        let Some(devs) = devices.filter(|d| d.len() > 1) else {
10194            self.gpu_plan = None;
10195            return Ok(());
10196        };
10197        self.split_supported()?;
10198        let n = self.num_layers;
10199        if devs.len() > n {
10200            return Err(format!("{} devices for {n} layers", devs.len()));
10201        }
10202        if let Some(k) = at {
10203            if k == 0 || k >= n {
10204                return Err(format!("split at {k}: the model has {n} layers"));
10205            }
10206            if devs.len() == 2 {
10207                self.gpu_plan = Some(std::sync::Arc::new(vec![
10208                    (devs[0], 0, k - 1),
10209                    (devs[1], k, n - 1),
10210                ]));
10211                return Ok(());
10212            }
10213            return Err(format!(
10214                "an explicit split point takes exactly 2 devices, got {}",
10215                devs.len()
10216            ));
10217        }
10218        let per = n.div_ceil(devs.len());
10219        let mut plan = Vec::with_capacity(devs.len());
10220        let mut from = 0usize;
10221        for &d in devs {
10222            if from >= n {
10223                break;
10224            }
10225            let upto = (from + per - 1).min(n - 1);
10226            plan.push((d, from, upto));
10227            from = upto + 1;
10228        }
10229        self.gpu_plan = Some(std::sync::Arc::new(plan));
10230        Ok(())
10231    }
10232
10233    /// The active in-process split, if any: (device, first layer, last).
10234    pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
10235        self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
10236    }
10237
10238    /// Layer span [from ..= upto] (upto None = last layer): the building
10239    /// block the network pipeline-split rides on. `from > 0` skips the
10240    /// arch escape hatches (the pub `forward_span` refuses those archs
10241    /// first) and the whole-token graph — the plain per-layer loop is
10242    /// the canonical executor for a partial stack.
10243    fn forward_layers_span(
10244        &mut self,
10245        hidden: &[f32],
10246        position: usize,
10247        task_mask: Option<&TaskMask>,
10248        from: usize,
10249        upto: Option<usize>,
10250    ) -> Vec<f32> {
10251        debug_assert!(
10252            from == 0
10253                || (self.dsv4.is_none()
10254                    && self.dsv41.is_none()
10255                    && self.qwen4_exp.is_none()
10256                    && self.g3n.is_none())
10257        );
10258        if let Some(b) = &mut self.qwen4_exp {
10259            let _ = (task_mask, upto);
10260            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10261            let mut logits = Vec::new();
10262            crate::qwen4_exp::forward_token(
10263                &b.0,
10264                &b.1,
10265                &b.2,
10266                &mut b.3,
10267                token_id,
10268                position,
10269                &self.inv_freq,
10270                self.pool.as_deref(),
10271                &mut logits,
10272                true,
10273            );
10274            self.graph_logits = Some(logits);
10275            return vec![0.0; self.hidden_size];
10276        }
10277        // DeepSeek-V4 runs its own stack: the state is hc_mult copies, and
10278        // the forward returns LOGITS, not a hidden — the head is inside it
10279        // (the final fold sits between the last layer and the norm). The
10280        // token id rides in `hidden[0]`, written by embed_single, because
10281        // the hash layers route by id rather than by content.
10282        if let Some(b) = &mut self.dsv4 {
10283            let _ = (task_mask, upto);
10284            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10285            let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
10286            st.pos = position;
10287            let mut logits = Vec::new();
10288            crate::dsv4::forward_token(
10289                g,
10290                layers,
10291                &cfg,
10292                st,
10293                token_id,
10294                &self.inv_freq,
10295                self.pool.as_deref(),
10296                &mut logits,
10297            );
10298            self.graph_logits = Some(logits);
10299            self.dspark_probe(position, token_id);
10300            // The caller expects a hidden; the logits went out of band, as
10301            // with the fused lm_head path.
10302            return vec![0.0; self.hidden_size];
10303        }
10304        // DeepSeek-V4.1 owns its complete stack and emits logits out of band.
10305        if let Some(b) = &mut self.dsv41 {
10306            let _ = (task_mask, upto);
10307            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10308            let mut logits = Vec::new();
10309            crate::dsv41::forward_token(
10310                &b.0,
10311                &b.1,
10312                &b.2,
10313                &mut b.3,
10314                token_id,
10315                position,
10316                self.pool.as_deref(),
10317                &mut logits,
10318            );
10319            self.graph_logits = Some(logits);
10320            return vec![0.0; self.hidden_size];
10321        }
10322        // Gemma-3n runs its own stack (4 AltUp replicas don't fit this
10323        // loop); `hidden` is the extended embedding from embed_single.
10324        if let Some(b) = &self.g3n {
10325            let _ = (task_mask, upto);
10326            return crate::g3n::g3n_forward(
10327                &b.0,
10328                &b.1,
10329                hidden,
10330                position,
10331                &mut self.kv_cache.layers,
10332                self.num_heads,
10333                self.num_kv_heads,
10334                self.head_dim,
10335                self.pool.as_deref(),
10336            );
10337        }
10338        let mut h = hidden.to_vec();
10339        // Split borrows: copy scalars / clone handles so the per-layer
10340        // cfg does not hold `&self` while the KV cache is `&mut`.
10341        let (nh, _nkv, _hd, hs, _rd, eps) = (
10342            self.num_heads,
10343            self.num_kv_heads,
10344            self.head_dim,
10345            self.hidden_size,
10346            self.rotary_dim,
10347            self.rms_eps,
10348        );
10349        let pool = self.pool.clone();
10350        // Opt-in wgpu token-graph attention (discrete Vulkan/DX12): the whole
10351        // attention sub-block runs resident in one submit. Off by default.
10352        // Whole-token wgpu graph: eligibility + arbitration.
10353        //  - explicit CMF_GPU_WGPU_GRAPH forces it on/off;
10354        //  - discrete adapters (4090: decode 76 -> 137 tok/s) and GDN
10355        //    hybrids (recurrent state device-resident, no CPU twin to
10356        //    race) TRUST it;
10357        //  - integrated/mobile adapters RACE it against the normal path
10358        //    at generation granularity (gpu::graph_race_*) — tiled
10359        //    mobile GPUs can turn the ~300-dispatch graph into seconds
10360        //    per token, while a fast phone GPU keeps its win.
10361        let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
10362        let graph_on = match graph_env.as_deref() {
10363            Some("0") => false,
10364            Some("prefill") => false, // decode keeps the per-op path
10365            Some(_) => true,
10366            // Unset: same discrete-only default as every other graph
10367            // site. "Is the GPU on" used to stand in here — which made
10368            // the 0.2 tok/s whole-token graph race-eligible on mobile
10369            // adapters and cost 12-14× on first tokens (cmfmobile
10370            // TUNING.md); integrated GPUs keep the per-op probe path.
10371            None => crate::gpu::wgpu_graph_default(),
10372        };
10373        let graph_trusted =
10374            graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
10375        let race_eligible = graph_on
10376            && upto.is_none()
10377            && task_mask.is_none()
10378            && from == 0
10379            && !crate::gpu::graph_unsupported();
10380        let mut tail_start = 0usize;
10381        if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
10382            let t_graph = std::time::Instant::now();
10383            let mut lg = Vec::new();
10384            let mut gl = 0usize;
10385            let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
10386            let declined = built.is_none();
10387            let built = match built {
10388                Some(Ok(hh)) => Some(hh),
10389                Some(Err(())) => {
10390                    // O(1) state was admitted before the device failure; the
10391                    // CPU mirrors are stale by construction.  Clear the whole
10392                    // sequence and stop rather than walking that stale state.
10393                    self.clear_sequence_state();
10394                    self.graph_failed
10395                        .store(true, std::sync::atomic::Ordering::Relaxed);
10396                    self.cancel
10397                        .store(true, std::sync::atomic::Ordering::Relaxed);
10398                    tracing::error!("token graph failed after admission; sequence state cleared");
10399                    return vec![0.0; self.hidden_size];
10400                }
10401                None => None,
10402            };
10403            // Past the transient guards (o1 still collecting, a softcap)
10404            // a refusal is about the weights and will never change —
10405            // remember it instead of walking every layer again next
10406            // token.
10407            if declined && !self.o1_active() && self.attn_softcap == 0.0 {
10408                crate::gpu::graph_mark_unsupported();
10409            }
10410            graph_note(built.is_some(), gl, self.num_layers);
10411            if let Some(hh) = built {
10412                let dur = t_graph.elapsed();
10413                if std::env::var("CMF_GRAPH_PROF").is_ok() {
10414                    eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
10415                }
10416                if gl > 0 && gl < self.num_layers {
10417                    // Device prefix: the graph ran layers 0..gl and handed
10418                    // back the boundary hidden — the loop below owns the
10419                    // tail. The prefix layers' KV/state advanced on the
10420                    // device; the tail's advances on the host below. One
10421                    // boundary crossing per token.
10422                    h = hh;
10423                    tail_start = gl;
10424                } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
10425                    if !graph_trusted {
10426                        crate::gpu::graph_race_record(true, dur);
10427                    }
10428                    if !lg.is_empty() {
10429                        // Graph produced logits (final-norm + lm_head folded in) —
10430                        // pad/cap to vocab and hand them to the sampler directly.
10431                        lg.resize(self.vocab_size, 0.0);
10432                        if let Some(c) = self.final_softcap {
10433                            for l in lg.iter_mut() {
10434                                *l = c * (*l / c).tanh();
10435                            }
10436                        }
10437                        self.graph_logits = Some(lg);
10438                    }
10439                    return hh;
10440                }
10441                // Hopeless first graph token: discard it and fall through
10442                // to the normal path. Safe exactly here — the prompt KV is
10443                // still CPU-owned (chunked prefill), so recomputing this
10444                // position is exact; the mirror's extra row is never read
10445                // (the race just settled on the normal path).
10446            }
10447        }
10448        // KIMI-LINEAR HAS NO SPLIT BUG. The 2.6× reported from the
10449        // model rotation (12.2 tok/s on one card against 4.6 on two)
10450        // was a single measurement of a model whose arm arbitration is
10451        // borderline, and it did not survive repetition. Three runs an
10452        // arm, same binary, back to back:
10453        //   probe on : 1 GPU 9.5 / 5.7 / 5.9   2 GPU 7.8 / 13.0 / 13.3
10454        //   pinned   : 1 GPU 5.6 / 5.3 / 5.2   2 GPU 3.5 / 4.2 / 3.4
10455        // With the arms pinned the split costs about 1.45×, which is
10456        // what a layer split costs. With the probe free, TWO CARDS RUN
10457        // FASTER — because for this model the CPU arm wins some op
10458        // classes and the probe finds that.
10459        //
10460        // Two things do stand, and both are measured. The token graph
10461        // builds NOTHING here (`covered 0 of 14 layers [0..14)`), so
10462        // every layer walks per-op on either arm — that is where the
10463        // headroom is, not in the split. And this model's benchmark is
10464        // unusable without `CMF_GPU_PROBE=0`: the arbitration alone
10465        // moves it by more than 2×.
10466        //
10467        // Span runs (network split): the graph covers exactly [from..=upto]
10468        // — one submit per SEGMENT per token. No race: its state is global
10469        // and calibrated on full stacks, so spans take the graph only where
10470        // it is trusted by default (discrete adapters / CMF_GPU_WGPU_GRAPH).
10471        let span = from > 0 || upto.is_some();
10472        if span && graph_on && task_mask.is_none() && graph_trusted {
10473            let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
10474            let mut lg = Vec::new();
10475            let mut gl = 0usize;
10476            let span_res =
10477                self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
10478            let span_res = match span_res {
10479                Some(Ok(hh)) => Some(hh),
10480                Some(Err(())) => {
10481                    self.clear_sequence_state();
10482                    self.graph_failed
10483                        .store(true, std::sync::atomic::Ordering::Relaxed);
10484                    self.cancel
10485                        .store(true, std::sync::atomic::Ordering::Relaxed);
10486                    tracing::error!(
10487                        "span token graph failed after admission; sequence state cleared"
10488                    );
10489                    return vec![0.0; self.hidden_size];
10490                }
10491                None => None,
10492            };
10493            graph_note(span_res.is_some(), gl, upto_excl - from);
10494            if std::env::var("CMF_GPU_DEBUG").is_ok() {
10495                // How much of the span the graph actually covered. A
10496                // prefix of nothing means every layer walks per-op and
10497                // the split's extra cost is elsewhere.
10498                static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
10499                if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
10500                    eprintln!(
10501                        "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
10502                        upto_excl - from,
10503                        span_res.is_some()
10504                    );
10505                }
10506            }
10507            if let Some(hh) = span_res {
10508                if gl == upto_excl - from {
10509                    if !lg.is_empty() {
10510                        lg.resize(self.vocab_size, 0.0);
10511                        if let Some(c) = self.final_softcap {
10512                            for l in lg.iter_mut() {
10513                                *l = c * (*l / c).tanh();
10514                            }
10515                        }
10516                        self.graph_logits = Some(lg);
10517                    }
10518                    crate::gpu::set_layer(-1);
10519                    return hh;
10520                }
10521                // Partial device prefix of the span: CPU owns the tail.
10522                h = hh;
10523                tail_start = from + gl;
10524            }
10525        }
10526        let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
10527
10528        // A partial graph is an explicit GPU-prefix / CPU-tail split. Keep
10529        // the tail PURE host-side: letting its QTensor hooks re-enter the
10530        // residency arena streams every omitted layer through Vulkan and the
10531        // driver's freed-allocation cache can grow to the full model size
10532        // (25.4 GiB observed with a 14 GiB budget on Granite 30B Q8_2F).
10533        let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
10534        let automatic_gpu_prefix = self.automatic_gpu_prefix();
10535
10536        #[cfg(target_os = "macos")]
10537        let mut gpu_skip_until = 0usize;
10538        for li in tail_start.max(from)..self.num_layers {
10539            let _capacity_tail = automatic_gpu_prefix
10540                .filter(|&prefix| li >= prefix)
10541                .map(|_| crate::gpu::enter_cpu_scope());
10542            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU (CMF_GPU_LAYERS)
10543            if let Some(u) = upto {
10544                if li > u {
10545                    break;
10546                }
10547            }
10548            if let Some(mask) = task_mask {
10549                if !mask.layer_alive(li) {
10550                    continue; // dead layer: residual pass-through
10551                }
10552            }
10553            // Whole-block q1 token graph: a run of consecutive q1
10554            // layers — GDN and full attention — executes with one sync
10555            // per CPU attend instead of per op (macOS/Metal).
10556            #[cfg(target_os = "macos")]
10557            {
10558                if li < gpu_skip_until {
10559                    continue;
10560                }
10561                if task_mask.is_none() {
10562                    let end = self.q1_graph_gpu(li, upto, position, &mut h);
10563                    if self
10564                        .graph_failed
10565                        .load(std::sync::atomic::Ordering::Relaxed)
10566                    {
10567                        // The graph may have mutated device state before a
10568                        // command-buffer error. Never continue with a CPU
10569                        // tail or read a stale host mirror after admission.
10570                        return vec![0.0; self.hidden_size];
10571                    }
10572                    if end > li {
10573                        gpu_skip_until = end;
10574                        // Looped Transformer: the graph stopped at a loop
10575                        // boundary — apply final norm before the next iteration.
10576                        if self.is_loop_end(end - 1) && end < self.num_layers {
10577                            h = inference::rms_norm(
10578                                &h,
10579                                &self.weights.final_norm,
10580                                self.rms_eps,
10581                                self.norm_style,
10582                            );
10583                        }
10584                        continue;
10585                    }
10586                }
10587            }
10588
10589            let lw = &self.weights.layers[self.phys_layer(li)];
10590            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10591                if tp.parse::<usize>().ok() == Some(position) {
10592                    let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
10593                    eprintln!(
10594                        "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10595                        h[0], h[1]
10596                    );
10597                }
10598            }
10599            // Norm into the pipeline scratch — the returning rms_norm
10600            // allocated twice per layer per token (roadmap §3 P0).
10601            inference::rms_norm_into(
10602                &h,
10603                &lw.input_norm,
10604                self.rms_eps,
10605                self.norm_style,
10606                &mut self.ws.n1,
10607            );
10608
10609            let attn_out = match &lw.attn {
10610                AttnKind::Mla(w) => {
10611                    let inv_freq_l = self.layer_inv_freq(li);
10612                    let rs = self.layer_rope_scale(li);
10613                    let eps = self.rms_eps;
10614                    let pool = self.pool.clone();
10615                    mla_attention(
10616                        w,
10617                        &self.ws.n1,
10618                        &mut self.kv_cache.layers[li],
10619                        position,
10620                        &inv_freq_l,
10621                        rs,
10622                        eps,
10623                        pool.as_deref(),
10624                    )
10625                }
10626                AttnKind::Linear(w) => {
10627                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
10628                    vmf_phase_forward(
10629                        &self.ws.n1,
10630                        w,
10631                        &cfg,
10632                        &mut self.kv_cache.layers[li].linear_state,
10633                        self.pool.as_deref(),
10634                    )
10635                }
10636                AttnKind::Kda(w) => {
10637                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10638                    crate::linear_core::kda_forward(
10639                        &self.ws.n1,
10640                        w,
10641                        &cfg,
10642                        &mut self.kv_cache.layers[li].linear_state,
10643                        self.pool.as_deref(),
10644                    )
10645                }
10646                AttnKind::LinearGdn(w) => {
10647                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10648                    gdn_forward(
10649                        &self.ws.n1,
10650                        w,
10651                        &cfg,
10652                        &mut self.kv_cache.layers[li].linear_state,
10653                        self.pool.as_deref(),
10654                    )
10655                }
10656                AttnKind::ShortConv(w) => {
10657                    let cfg = self
10658                        .short_conv_cfg
10659                        .expect("short-conv layer without short_conv_cfg");
10660                    short_conv_forward(
10661                        &self.ws.n1,
10662                        w,
10663                        &cfg,
10664                        &mut self.kv_cache.layers[li].linear_state,
10665                        self.pool.as_deref(),
10666                    )
10667                }
10668                AttnKind::Full {
10669                    wq,
10670                    wk,
10671                    wv,
10672                    wo,
10673                    q_norm,
10674                    k_norm,
10675                    output_gate,
10676                    softplus_gate,
10677                    bias,
10678                } if self.kv_cache.layers[li].o1_sealed() => {
10679                    // O(1) override: decode on the sealed Nyström state
10680                    // instead of the growing KV cache.
10681                    let inv_freq_l = self.layer_inv_freq(li);
10682                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10683                    let cfg = QwenAttnCfg {
10684                        num_heads: self.layer_num_heads(li),
10685                        num_kv_heads: nkv_l,
10686                        head_dim: hd_l,
10687                        hidden_size: hs,
10688                        position,
10689                        inv_freq: &inv_freq_l,
10690                        rotary_dim: rd_l,
10691                        scale: self.attn_scale,
10692                        softcap: self.attn_softcap,
10693                        window: None,
10694                        v_norm: self.attn_v_norm,
10695                        qk_norm_after_rope: self.qk_norm_after_rope,
10696                        q_norm: q_norm.as_deref(),
10697                        k_norm: k_norm.as_deref(),
10698                        output_gate: *output_gate,
10699                        softplus_gate: softplus_gate
10700                            .as_ref()
10701                            .map(|(gate, per_head)| (gate, *per_head)),
10702                        rope_scale: self.layer_rope_scale(li),
10703                        bias: bias
10704                            .as_ref()
10705                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10706                        rms_eps: eps,
10707                        norm_style: self.norm_style,
10708                        pool: pool.as_deref(),
10709                    };
10710                    attention::qwen_attention_nystrom(
10711                        &self.ws.n1,
10712                        wq,
10713                        wk,
10714                        wv,
10715                        wo,
10716                        &mut self.kv_cache.layers[li],
10717                        &cfg,
10718                    )
10719                }
10720                AttnKind::Full {
10721                    wq,
10722                    wk,
10723                    wv,
10724                    wo,
10725                    q_norm,
10726                    k_norm,
10727                    output_gate,
10728                    softplus_gate,
10729                    bias,
10730                } => 'attn: {
10731                    // wgpu token-graph attention (opt-in): whole sub-block in
10732                    // one submit, device K/V mirror. q1 only, no gate/bias/mask.
10733                    if graph_on
10734                        && !*output_gate
10735                        && softplus_gate.is_none()
10736                        && self.attention_heads_per_layer.is_none()
10737                        && bias.is_none()
10738                        && task_mask.is_none()
10739                    {
10740                        let inv_freq_l = self.layer_inv_freq(li);
10741                        let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10742                        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10743                        if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
10744                            wq.mapped_q1(),
10745                            wk.mapped_q1(),
10746                            wv.mapped_q1(),
10747                            wo.mapped_q1(),
10748                        ) {
10749                            let gm = gm.clone();
10750                            let mut out = vec![0f32; hs];
10751                            let cache = &self.kv_cache.layers[li];
10752                            if crate::gpu::attn_dropin(
10753                                &gm,
10754                                self.graph_kv_id,
10755                                li,
10756                                &self.ws.n1,
10757                                qi,
10758                                ki,
10759                                vi,
10760                                oi,
10761                                q_norm.as_deref(),
10762                                k_norm.as_deref(),
10763                                self.qk_norm_after_rope,
10764                                &inv_freq_l,
10765                                nh,
10766                                nkv_l,
10767                                hd_l,
10768                                rd_l,
10769                                hs,
10770                                position,
10771                                self.kv_cache.max_seq_len,
10772                                gemma,
10773                                eps as f32,
10774                                cache.k_heads(),
10775                                cache.v_heads(),
10776                                &mut out,
10777                            ) {
10778                                break 'attn out;
10779                            }
10780                        }
10781                    }
10782                    let masked = task_mask
10783                        .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
10784                        .unwrap_or(false);
10785                    let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
10786                    match (masked, f32_view) {
10787                        // Historical masked path (f32 slices; the loader
10788                        // keeps masked models in f32).
10789                        (true, (Some(q), Some(k), Some(v), Some(o))) => {
10790                            let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
10791                            attention::multi_head_attention(
10792                                &self.ws.n1,
10793                                q,
10794                                k,
10795                                v,
10796                                o,
10797                                &mut self.kv_cache.layers[li],
10798                                self.num_heads,
10799                                self.num_kv_heads,
10800                                self.head_dim,
10801                                self.hidden_size,
10802                                position,
10803                                &active_heads,
10804                                &self.inv_freq,
10805                            )
10806                        }
10807                        (masked, _) => {
10808                            if masked {
10809                                tracing::warn!(
10810                                    "layer {li}: head mask on quantized weights not \
10811                                     supported yet — executing dense"
10812                                );
10813                            }
10814                            let inv_freq_l = self.layer_inv_freq(li);
10815                            let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10816                            let cfg = QwenAttnCfg {
10817                                num_heads: self.layer_num_heads(li),
10818                                num_kv_heads: nkv_l,
10819                                head_dim: hd_l,
10820                                hidden_size: hs,
10821                                position,
10822                                inv_freq: &inv_freq_l,
10823                                rotary_dim: rd_l,
10824                                scale: self.attn_scale,
10825                                softcap: self.attn_softcap,
10826                                window: self.layer_window(li),
10827                                v_norm: self.attn_v_norm,
10828                                qk_norm_after_rope: self.qk_norm_after_rope,
10829                                q_norm: q_norm.as_deref(),
10830                                k_norm: k_norm.as_deref(),
10831                                output_gate: *output_gate,
10832                                softplus_gate: softplus_gate
10833                                    .as_ref()
10834                                    .map(|(gate, per_head)| (gate, *per_head)),
10835                                rope_scale: self.layer_rope_scale(li),
10836                                bias: bias
10837                                    .as_ref()
10838                                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10839                                rms_eps: eps,
10840                                norm_style: self.norm_style,
10841                                pool: pool.as_deref(),
10842                            };
10843                            attention::qwen_attention(
10844                                &self.ws.n1,
10845                                wq,
10846                                wk,
10847                                wv,
10848                                wo,
10849                                &mut self.kv_cache.layers[li],
10850                                &cfg,
10851                            )
10852                        }
10853                    }
10854                }
10855            };
10856            // Gemma sandwich norm: normalize the attention branch before
10857            // it joins the residual stream.
10858            let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
10859                Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
10860                None => attn_out,
10861            };
10862            let lw = &self.weights.layers[self.phys_layer(li)];
10863            inference::add_rmsnorm_fused_into(
10864                &mut h,
10865                &attn_out,
10866                &lw.post_norm,
10867                self.rms_eps,
10868                self.norm_style,
10869                &mut self.ws.p1,
10870            );
10871            let mut attn_out = attn_out;
10872            attention::recycle_buf(&mut attn_out);
10873            let post_normed = &self.ws.p1;
10874
10875            let ffn_masked = task_mask
10876                .map(|m| m.ffn_active_count(li) < self.intermediate_size)
10877                .unwrap_or(false);
10878            // One masked dense CONTRACT, dispatched by cost. The
10879            // activation-zeroing arm (the batched sweep's, validated
10880            // against the replica to 0.8%) computes the FULL fused FFN
10881            // and zeroes the dead — right whenever most neurons live.
10882            // The sparse arm reads ONLY active rows and down columns —
10883            // per-row dots are slower per element than the fused kernel,
10884            // so it pays only once the mask is deep enough. The 0.5
10885            // crossover is first-principles (fused kernels run ~2x the
10886            // per-row dot throughput); a shallow specialist (95% alive)
10887            // stays fused, a --target-sparsity bake flips arms on its
10888            // own weight.
10889            let ffn_out = match (ffn_masked, &lw.ffn) {
10890                // A defragged tube layer answers its own mask: the core
10891                // always runs, each tube runs when its bit is on, and
10892                // the tubes that are off are never read from the mmap.
10893                (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
10894                    let row = task_mask
10895                        .and_then(|tm| tm.ffn_masks.get(li))
10896                        .map(|v| v.as_slice());
10897                    tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
10898                }
10899                (true, FfnKind::Dense(d)) => {
10900                    let tm = task_mask.unwrap();
10901                    let alive = tm.ffn_active_count(li);
10902                    let deep = alive * 2 <= self.intermediate_size;
10903                    if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
10904                        let active = tm.ffn_active_indices(li);
10905                        sparse_ffn_quant(
10906                            d,
10907                            post_normed,
10908                            &active,
10909                            self.hidden_size,
10910                            self.pool.as_deref(),
10911                        )
10912                    } else if deep
10913                        && let (Some(g), Some(u), Some(dn)) = (
10914                            d.gate_proj.as_f32(),
10915                            d.up_proj.as_f32(),
10916                            d.down_proj.as_f32(),
10917                        )
10918                    {
10919                        let active = tm.ffn_active_indices(li);
10920                        inference::sparse_ffn_forward(
10921                            post_normed,
10922                            g,
10923                            u,
10924                            dn,
10925                            self.hidden_size,
10926                            self.intermediate_size,
10927                            &active,
10928                            self.pool.as_deref(),
10929                        )
10930                    } else {
10931                        let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
10932                        dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
10933                    }
10934                }
10935                (true, FfnKind::Moe(m)) => {
10936                    // MoE is sparse by expert selection; a task mask
10937                    // narrows the ROUTABLE set via its expert fields
10938                    // (spec §5) when it carries them.
10939                    let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
10940                    ffn_forward(
10941                        &lw.ffn,
10942                        post_normed,
10943                        self.pool.as_deref(),
10944                        allowed.as_deref(),
10945                    )
10946                }
10947                (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
10948                    dm,
10949                    post_normed,
10950                    &h,
10951                    self.rms_eps,
10952                    self.norm_style,
10953                    self.pool.as_deref(),
10954                ),
10955                (false, _) => match &lw.ffn {
10956                    FfnKind::DenseMoe(dm) => dense_moe_ffn(
10957                        dm,
10958                        post_normed,
10959                        &h,
10960                        self.rms_eps,
10961                        self.norm_style,
10962                        self.pool.as_deref(),
10963                    ),
10964                    _ => {
10965                        let allowed = match (&lw.ffn, task_mask) {
10966                            (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
10967                            _ => None,
10968                        };
10969                        ffn_forward(
10970                            &lw.ffn,
10971                            post_normed,
10972                            self.pool.as_deref(),
10973                            allowed.as_deref(),
10974                        )
10975                    }
10976                },
10977            };
10978            let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
10979                Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
10980                None => ffn_out,
10981            };
10982            for (i, &f) in ffn_out.iter().enumerate() {
10983                h[i] += f;
10984            }
10985            let mut ffn_out = ffn_out;
10986            attention::recycle_buf(&mut ffn_out);
10987
10988            // Gemma-4: the layer output is scaled by a learned scalar.
10989            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
10990                for v in h.iter_mut() {
10991                    *v *= sc;
10992                }
10993            }
10994
10995            // Looped Transformer: apply final norm at the end of each loop iteration.
10996            // Nanbeige 4.2: after layer 21 (virtual), apply norm before looping back to layer 0.
10997            if self.is_loop_end(li) && li + 1 < self.num_layers {
10998                h = inference::rms_norm(
10999                    &h,
11000                    &self.weights.final_norm,
11001                    self.rms_eps,
11002                    self.norm_style,
11003                );
11004            }
11005
11006            // Dynamic routing φ capture (on-policy): the
11007            // EMA of the post-residual hidden at the router's phi_layer,
11008            // updated as the context evolves during decode.
11009            if self.dyn_phi_layer == Some(li) {
11010                self.update_dyn_phi(&h);
11011            }
11012        }
11013        crate::gpu::set_layer(-1); // layers done — lm_head outside layer-split
11014        if let Some(t) = t_race_cpu {
11015            crate::gpu::graph_race_record(false, t.elapsed());
11016        }
11017
11018        h
11019    }
11020
11021    /// EMA of φ at the router layer (rolling, weight 0.2 = ~5-token
11022    /// horizon). First observation seeds it exactly.
11023    fn update_dyn_phi(&mut self, h: &[f32]) {
11024        const A: f32 = 0.2;
11025        if self.dyn_phi_ema.len() != h.len() {
11026            self.dyn_phi_ema = vec![0.0; h.len()];
11027            self.dyn_phi_seen = 0;
11028        }
11029        if self.dyn_phi_seen == 0 {
11030            self.dyn_phi_ema.copy_from_slice(h);
11031        } else {
11032            for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
11033                *e = (1.0 - A) * *e + A * v;
11034            }
11035        }
11036        self.dyn_phi_seen += 1;
11037    }
11038
11039    /// Current router φ (EMA at phi_layer); empty until first capture.
11040    pub fn dyn_phi(&self) -> &[f32] {
11041        &self.dyn_phi_ema
11042    }
11043
11044    /// Enable/disable φ capture at the router layer, reset the EMA.
11045    pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
11046        self.dyn_phi_layer = layer;
11047        self.dyn_phi_ema.clear();
11048        self.dyn_phi_seen = 0;
11049    }
11050
11051    /// Skills eligible for dynamic switching: (index, id, phi_layer).
11052    pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
11053        let Some(model) = &self.model else {
11054            return Vec::new();
11055        };
11056        model
11057            .header
11058            .skills
11059            .iter()
11060            .enumerate()
11061            .filter_map(|(i, sk)| {
11062                let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
11063                let sel = sk.selection.as_ref()?;
11064                (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
11065            })
11066            .collect()
11067    }
11068
11069    /// Index of the currently overlaid skill (None = backbone).
11070    pub fn active_skill(&self) -> Option<usize> {
11071        self.dyn_active
11072    }
11073
11074    /// Enable dynamic per-token skill routing: build the hysteresis
11075    /// router from the container's routable skills, start φ capture at
11076    /// their (shared) phi_layer. Returns the number of routable skills
11077    /// (0 = nothing to route; router stays off). Idempotent.
11078    pub fn enable_dynamic_routing(&mut self) -> usize {
11079        use crate::swarm::{DynRouter, RoutableSkill};
11080        let Some(model) = self.model.clone() else {
11081            return 0;
11082        };
11083        // A blend materialized f32 working tensors into the layers; there
11084        // is no single skill index to revert from → refuse (honest).
11085        if self.dyn_blend_loaded {
11086            tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
11087            return 0;
11088        }
11089        // A statically-overlaid skill that is NOT FFN-eligible can't be
11090        // cheaply reverted at generation start → refuse rather than
11091        // silently keep it overlaid.
11092        if let Some(a) = self.dyn_active {
11093            if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
11094                tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
11095                return 0;
11096            }
11097        }
11098        let hidden = self.hidden_size;
11099        let mut skills = Vec::new();
11100        for (idx, id, _phi) in self.dynamic_skills() {
11101            if let Some(sel) = model.header.skills[idx].selection.as_ref() {
11102                if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
11103                    skills.push(rs);
11104                }
11105            }
11106        }
11107        if skills.is_empty() {
11108            return 0;
11109        }
11110        // Skills should share a phi_layer; warn (not fail) if they don't.
11111        let phi = skills[0].phi_layer;
11112        if skills.iter().any(|s| s.phi_layer != phi) {
11113            tracing::warn!("routable skills disagree on phi_layer; using {phi}");
11114        }
11115        let n = skills.len();
11116        self.set_dyn_phi_layer(Some(phi));
11117        self.dyn_router = Some(DynRouter::new(skills));
11118        n
11119    }
11120
11121    /// Human-readable switch log from the last dynamic-routed generation.
11122    pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
11123        self.dyn_router
11124            .as_ref()
11125            .map(|r| r.switches.clone())
11126            .unwrap_or_default()
11127    }
11128
11129    /// LM head: hidden → logits [vocab_size]. The dominant matvec of
11130    /// every decode step — row-parallel on the worker pool.
11131    fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
11132        let rows = self.weights.lm_head.rows();
11133        let mut logits = attention::take_buf(rows.min(self.vocab_size));
11134        self.weights
11135            .lm_head
11136            .matvec(hidden, &mut logits, self.pool.as_deref());
11137        logits.resize(self.vocab_size, 0.0);
11138        if let Some(m) = self.logit_multiplier {
11139            for l in logits.iter_mut() {
11140                *l *= m;
11141            }
11142        }
11143        if let Some(c) = self.final_softcap {
11144            for l in logits.iter_mut() {
11145                *l = c * (*l / c).tanh();
11146            }
11147        }
11148        if let Some(cm) = self.head_clusters.as_ref() {
11149            self.hierarchical_head_logprobs(hidden, cm, &mut logits);
11150        }
11151        logits
11152    }
11153
11154    /// Two-level head (Cortiq Embryo): in place, logits[v] ← log p(v) =
11155    /// (lc[c] − lse(lc)) + (logit[v] − lse over v's cluster block), c = v / S.
11156    fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
11157        let h = hidden.len();
11158        let ncl = cm.len() / h.max(1);
11159        if ncl == 0 || logits.len() % ncl != 0 {
11160            return;
11161        }
11162        let cs = logits.len() / ncl;
11163        // cluster logits + log-softmax
11164        let mut lc = vec![0.0f32; ncl];
11165        for c in 0..ncl {
11166            let row = &cm[c * h..(c + 1) * h];
11167            let mut s = 0.0f32;
11168            for j in 0..h {
11169                s += row[j] * hidden[j];
11170            }
11171            lc[c] = s;
11172        }
11173        let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11174        let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
11175        for c in 0..ncl {
11176            let blk = &mut logits[c * cs..(c + 1) * cs];
11177            let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11178            let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
11179            let add = lc[c] - lse - bl;
11180            for v in blk.iter_mut() {
11181                *v += add;
11182            }
11183        }
11184    }
11185
11186    /// Prefill `ids` and return the next-token logits — what the model
11187    /// would predict next, WITHOUT committing to generation (introspection
11188    /// for `cortiq explain`). Clears and repopulates the KV cache; leaves
11189    /// the active overlay untouched.
11190    pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
11191        self.clear_sequence_state();
11192        // This helper is used by the pooled classification endpoint, where
11193        // every request is a fresh sequence. The shared reset also clears the
11194        // wgpu token graph's device-side recurrent state.
11195        crate::gpu::graph_race_begin_generation();
11196        if task_mask.is_none() {
11197            self.o1_begin();
11198        }
11199        let mut hidden = vec![0.0f32; self.hidden_size];
11200        for (pos, &id) in ids.iter().enumerate() {
11201            let emb = self.embed_single(id);
11202            hidden = self.forward_layers(&emb, pos, task_mask);
11203        }
11204        if let Err(err) = self.o1_seal_checked() {
11205            self.o1_fail(err);
11206        }
11207        inference::rms_norm_into(
11208            &hidden,
11209            &self.weights.final_norm,
11210            self.rms_eps,
11211            self.norm_style,
11212            &mut self.ws.n1,
11213        );
11214        self.lm_head_forward(&self.ws.n1)
11215    }
11216}
11217
11218/// Convenience: deterministic tiny pipeline for tests.
11219pub fn create_test_pipeline(
11220    hidden_size: usize,
11221    intermediate_size: usize,
11222    num_heads: usize,
11223    num_kv_heads: usize,
11224    head_dim: usize,
11225    num_layers: usize,
11226    vocab_size: usize,
11227) -> Pipeline {
11228    // Small pseudo-random weights: constant weights make attention
11229    // degenerate and hide indexing bugs.
11230    let synth = |n: usize, salt: usize| -> Vec<f32> {
11231        (0..n)
11232            .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
11233            .collect()
11234    };
11235    let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
11236        QTensor::from_f32(synth(rows * cols, salt), rows, cols)
11237    };
11238    let layer_weights: Vec<LayerWeights> = (0..num_layers)
11239        .map(|li| LayerWeights {
11240            input_norm: vec![1.0; hidden_size],
11241            post_norm: vec![1.0; hidden_size],
11242            attn_out_norm: None,
11243            ffn_out_norm: None,
11244            layer_scale: None,
11245            ffn: FfnKind::Dense(DenseFfn {
11246                gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
11247                up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
11248                down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
11249                act: Act::Silu,
11250                down_t: None,
11251                segs: Vec::new(),
11252            }),
11253            attn: AttnKind::Full {
11254                bias: None,
11255                wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
11256                wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
11257                wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
11258                wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
11259                q_norm: None,
11260                k_norm: None,
11261                output_gate: false,
11262                softplus_gate: None,
11263            },
11264        })
11265        .collect();
11266
11267    Pipeline::new(
11268        Tokenizer::byte_level(),
11269        PipelineWeights {
11270            embed_tokens: qt(vocab_size, hidden_size, 100),
11271            layers: layer_weights,
11272            lm_head: qt(vocab_size, hidden_size, 200),
11273            final_norm: vec![1.0; hidden_size],
11274        },
11275        hidden_size,
11276        intermediate_size,
11277        num_heads,
11278        num_kv_heads,
11279        head_dim,
11280        num_layers,
11281        num_layers, // physical_layers = num_layers (non-looped)
11282        false,      // loop_final_norm
11283        vocab_size,
11284        1e-6,
11285        10_000.0,
11286        NormStyle::Qwen,
11287        4096,
11288        SamplerConfig {
11289            seed: Some(42),
11290            ..Default::default()
11291        },
11292    )
11293}
11294
11295/// Batched dense-FFN: gate/up/down via matmat (element-wise the same
11296/// math as b × dense_ffn — the same dot kernels).
11297/// One mask bit, LSB-first per byte — `TaskMask::ffn_active_indices`'s
11298/// convention.
11299#[inline]
11300fn mask_bit(row: &[u8], j: usize) -> bool {
11301    (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
11302}
11303
11304/// Zero the CLOSED neurons' activations in a [rows × inter] panel — the
11305/// masked-inference fast path's whole trick: full fused quant compute,
11306/// then the mask lands on the ACTIVATIONS, which is arithmetically the
11307/// pruned network without touching a quantized weight byte. Whole open
11308/// bytes (0xFF = 8 open neurons) skip in one test.
11309/// `CMF_FFN_MASK_GAIN` — Patent 12 FIG. 4, variance-preserving
11310/// rescaling: truncation removes a share of the layer's output energy,
11311/// so the survivors are scaled up to put the variance back where the
11312/// downstream norm expects it. A scalar here; per layer it is
11313/// `sqrt(total energy / kept energy)`.
11314fn mask_gain() -> f32 {
11315    static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
11316    *G.get_or_init(|| {
11317        std::env::var("CMF_FFN_MASK_GAIN")
11318            .ok()
11319            .and_then(|v| v.parse().ok())
11320            .unwrap_or(1.0)
11321    })
11322}
11323
11324fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
11325    // With CMF_FFN_MEANFILL a closed neuron contributes its average
11326    // instead of nothing — same bytes read, one constant restored.
11327    let fill = meanfill().and_then(|(i, v)| {
11328        let li = crate::gpu::cur_layer();
11329        (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
11330    });
11331    for r in 0..rows {
11332        let base = r * inter;
11333        for (bi, &byte) in row.iter().enumerate() {
11334            if byte == 0xFF {
11335                continue;
11336            }
11337            let j0 = bi * 8;
11338            for bit in 0..8 {
11339                let j = j0 + bit;
11340                if j < inter && byte & (1 << bit) == 0 {
11341                    g[base + j] = fill.map_or(0.0, |f| f[j]);
11342                }
11343            }
11344        }
11345    }
11346    let gain = mask_gain();
11347    if gain != 1.0 {
11348        for v in g[..rows * inter].iter_mut() {
11349            *v *= gain;
11350        }
11351    }
11352}
11353
11354/// True when neuron `i`'s bit is set (no mask = everything runs).
11355#[inline]
11356fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
11357    row.is_none_or(|r| mask_bit(r, i))
11358}
11359
11360/// Every bit below `n` set — the common case for a tube file's CORE,
11361/// where only the tube bits vary per task.
11362fn all_bits_on(row: &[u8], n: usize) -> bool {
11363    (0..n).all(|i| mask_bit(row, i))
11364}
11365
11366/// `CMF_TUBE_TOPK` — how many tubes a TOKEN may open (0 = the task mask
11367/// decides alone). This is the dense FFN read as a mixture: the tubes
11368/// are the experts a k-means over `gate_proj` rows found, and the token
11369/// picks among them. `CMF_TUBE_SCORE=gate` scores a tube by its own
11370/// gate (realizable: only `up`/`down` of the losers go unread),
11371/// `=oracle` scores by the true `silu(gate)·up` mass (the ceiling —
11372/// only `down` is saved, and the selection has read what it predicts).
11373fn tube_topk() -> usize {
11374    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11375    *K.get_or_init(|| {
11376        std::env::var("CMF_TUBE_TOPK")
11377            .ok()
11378            .and_then(|v| v.parse().ok())
11379            .unwrap_or(0)
11380    })
11381}
11382
11383fn tube_score_oracle() -> bool {
11384    static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11385    *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
11386}
11387
11388/// The routed arm of `tube_ffn`: a token opens only its best `k` tubes.
11389/// At `b == 1` (decode) the losers are genuinely never read — that is
11390/// the speed. At `b > 1` (the scoring sweep) every tube is computed and
11391/// the losers' activations are zeroed instead: same arithmetic, so the
11392/// perplexity is the routed model's, measured without a per-token
11393/// gather in the middle of a GEMM.
11394fn tube_ffn_routed(
11395    d: &DenseFfn,
11396    xs: &[f32],
11397    b: usize,
11398    pool: Option<&Pool>,
11399    mask_row: Option<&[u8]>,
11400    k: usize,
11401) -> Vec<f32> {
11402    let hidden = d.down_proj.rows();
11403    let core = d.gate_proj.rows();
11404    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11405    let mut out = match (b, core_full, mask_row) {
11406        (1, true, _) => dense_ffn(d, xs, pool),
11407        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11408        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11409        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11410    };
11411    let cand: Vec<usize> = (0..d.segs.len())
11412        .filter(|&i| tube_bit(mask_row, d.segs[i].start))
11413        .collect();
11414    if cand.is_empty() {
11415        return out;
11416    }
11417    // gate (and, where the score or the batch needs it, up) per tube.
11418    // The SCORE is taken at the point the serving path could take it:
11419    // off the gate alone, or off the finished activation for the oracle.
11420    let oracle = tube_score_oracle();
11421    let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
11422    let mut scores = vec![0f32; b * cand.len()];
11423    for (ci, &i) in cand.iter().enumerate() {
11424        let seg = &d.segs[i];
11425        let w = seg.width;
11426        let mut g = vec![0.0f32; b * w];
11427        if b == 1 {
11428            seg.gate.matvec(xs, &mut g, pool);
11429        } else {
11430            seg.gate.matmat(xs, b, &mut g, pool);
11431        }
11432        for v in g.iter_mut() {
11433            *v = Act::Silu.combine(*v, 1.0);
11434        }
11435        if !oracle {
11436            for t in 0..b {
11437                scores[t * cand.len() + ci] =
11438                    g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11439            }
11440        }
11441        if oracle || b > 1 {
11442            let mut u = vec![0.0f32; b * w];
11443            if b == 1 {
11444                seg.up.matvec(xs, &mut u, pool);
11445            } else {
11446                seg.up.matmat(xs, b, &mut u, pool);
11447            }
11448            for (a, &v) in g.iter_mut().zip(u.iter()) {
11449                *a *= v;
11450            }
11451            if oracle {
11452                for t in 0..b {
11453                    scores[t * cand.len() + ci] =
11454                        g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11455                }
11456            }
11457        }
11458        acts.push(g);
11459    }
11460    // per-token scores and the winners
11461    let keep = k.min(cand.len());
11462    let mut scratch: Vec<f32> = Vec::new();
11463    for t in 0..b {
11464        let mut sc: Vec<(f32, usize)> = (0..cand.len())
11465            .map(|ci| (scores[t * cand.len() + ci], ci))
11466            .collect();
11467        sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
11468        let mut alive = vec![false; cand.len()];
11469        for &(_, ci) in sc.iter().take(keep) {
11470            alive[ci] = true;
11471        }
11472        if b > 1 {
11473            for (ci, a) in acts.iter_mut().enumerate() {
11474                if !alive[ci] {
11475                    let w = d.segs[cand[ci]].width;
11476                    a[t * w..(t + 1) * w].fill(0.0);
11477                }
11478            }
11479        } else {
11480            // decode: finish only the winners — the losers' up/down
11481            // (and, with the gate score, everything but their gate)
11482            // are never touched.
11483            for (ci, &i) in cand.iter().enumerate() {
11484                if !alive[ci] {
11485                    continue;
11486                }
11487                let seg = &d.segs[i];
11488                let w = seg.width;
11489                let g = &mut acts[ci];
11490                if !tube_score_oracle() {
11491                    scratch.clear();
11492                    scratch.resize(w, 0.0);
11493                    seg.up.matvec(xs, &mut scratch, pool);
11494                    for (a, &v) in g.iter_mut().zip(scratch.iter()) {
11495                        *a *= v;
11496                    }
11497                }
11498                let mut acc = vec![0.0f32; hidden];
11499                seg.down.matvec(g, &mut acc, pool);
11500                for (o, a) in out.iter_mut().zip(&acc) {
11501                    *o += *a;
11502                }
11503            }
11504        }
11505    }
11506    if b > 1 {
11507        for (ci, &i) in cand.iter().enumerate() {
11508            let seg = &d.segs[i];
11509            let mut acc = vec![0.0f32; b * hidden];
11510            seg.down.matmat(&acts[ci], b, &mut acc, pool);
11511            for (o, a) in out.iter_mut().zip(&acc) {
11512                *o += *a;
11513            }
11514        }
11515    }
11516    out
11517}
11518
11519/// FFN of a defragged tube layer: the always-on core plus the tubes the
11520/// task mask switches on. Each tube is a normal tensor triple, so the
11521/// same kernels run it and an inactive tube's bytes are never read —
11522/// that is the whole point of the defrag (a scattered mask cannot skip
11523/// bytes; a contiguous one is just a smaller matrix).
11524fn tube_ffn(
11525    d: &DenseFfn,
11526    xs: &[f32],
11527    b: usize,
11528    pool: Option<&Pool>,
11529    mask_row: Option<&[u8]>,
11530) -> Vec<f32> {
11531    if tube_topk() > 0 {
11532        return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
11533    }
11534    let hidden = d.down_proj.rows();
11535    let core = d.gate_proj.rows();
11536    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11537    let mut out = match (b, core_full, mask_row) {
11538        (1, true, _) => dense_ffn(d, xs, pool),
11539        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11540        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11541        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11542    };
11543    TUBE_SCRATCH.with(|sc| {
11544        let mut sc = sc.borrow_mut();
11545        let [g, u, acc] = &mut *sc;
11546        for seg in &d.segs {
11547            if !tube_bit(mask_row, seg.start) {
11548                continue;
11549            }
11550            let w = seg.width;
11551            g.resize(b * w, 0.0);
11552            if b == 1
11553                && d.act == Act::Silu
11554                && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
11555            {
11556                // g holds silu(gate)·up.
11557            } else {
11558                u.resize(b * w, 0.0);
11559                if b == 1 {
11560                    QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
11561                } else {
11562                    seg.gate.matmat(xs, b, g, pool);
11563                    seg.up.matmat(xs, b, u, pool);
11564                }
11565                for i in 0..b * w {
11566                    g[i] = d.act.combine(g[i], u[i]);
11567                }
11568            }
11569            acc.resize(b * hidden, 0.0);
11570            acc.fill(0.0);
11571            if b == 1 {
11572                seg.down.matvec(g, acc, pool);
11573            } else {
11574                seg.down.matmat(g, b, acc, pool);
11575            }
11576            for (o, a) in out.iter_mut().zip(acc.iter()) {
11577                *o += *a;
11578            }
11579        }
11580        out
11581    })
11582}
11583
11584thread_local! {
11585    /// gate / up / down-accumulator scratch for the tube loop — a tube
11586    /// runs once per layer per token, and a fresh Vec each time is a
11587    /// malloc per tube per layer per token.
11588    static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
11589        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
11590}
11591
11592fn dense_ffn_batch(
11593    d: &DenseFfn,
11594    xs: &[f32],
11595    b: usize,
11596    pool: Option<&Pool>,
11597    mask_row: Option<&[u8]>,
11598) -> Vec<f32> {
11599    let inter = d.gate_proj.rows();
11600    let hidden = d.down_proj.rows();
11601    // Fused on-device SwiGLU when the device is in play: three separate
11602    // `matmat` calls are three round trips per layer, and the gate/up
11603    // panels (b × inter — 22 MB each at a 512-token chunk) cross the bus
11604    // twice for nothing. The kernel already existed for the image DiT;
11605    // the LLM prefill was simply never wired to it. A task mask needs the
11606    // activations on the host between the halves, so it keeps the CPU
11607    // arm below.
11608    if mask_row.is_none()
11609        && d.act == Act::Silu
11610        && b >= 32
11611        && crate::gpu::enabled_here()
11612        && !crate::gpu::mm_killed()
11613        // The refit pass needs this layer's activations on the host; the
11614        // fused chain keeps them on the device. Refusing it here costs
11615        // one round trip and keeps every GEMM on the card — the
11616        // alternative was running the whole calibration on the CPU.
11617        && refit_dir().is_none()
11618        // Same for the mass/hit probes. The accumulator at the bottom of
11619        // this function only sees `g` when `g` came back to the host, so
11620        // a fused batch would leave it summing nothing — a probe that
11621        // reports zeros rather than failing, which is worse.
11622        && !ffn_probe_active()
11623    {
11624        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11625            d.gate_proj.mapped_q4t(),
11626            d.up_proj.mapped_q4t(),
11627            d.down_proj.mapped_q4t(),
11628        ) {
11629            let mut out = vec![0.0f32; b * hidden];
11630            if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11631                return out;
11632            }
11633        }
11634        // The q4tp twin (same kernel family, scale from the row ladder) —
11635        // the DiT has run it in production since the pipeline containers;
11636        // the LLM prefill was simply never wired to it, so a q4tp model's
11637        // prefill panels stayed on the CPU.
11638        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11639            d.gate_proj.mapped_q4tp(),
11640            d.up_proj.mapped_q4tp(),
11641            d.down_proj.mapped_q4tp(),
11642        ) {
11643            let mut out = vec![0.0f32; b * hidden];
11644            if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11645                return out;
11646            }
11647        }
11648    }
11649    let mut g = vec![0.0f32; b * inter];
11650    d.gate_proj.matmat(xs, b, &mut g, pool);
11651    let mut u = vec![0.0f32; b * inter];
11652    d.up_proj.matmat(xs, b, &mut u, pool);
11653    if gate_topk() > 0 && d.act == Act::Silu {
11654        for t in 0..b {
11655            let row = &mut g[t * inter..(t + 1) * inter];
11656            for v in row.iter_mut() {
11657                *v = Act::Silu.combine(*v, 1.0);
11658            }
11659            keep_top_k(row, gate_topk());
11660        }
11661        for i in 0..b * inter {
11662            g[i] *= u[i];
11663        }
11664    } else {
11665        for i in 0..b * inter {
11666            g[i] = d.act.combine(g[i], u[i]);
11667        }
11668    }
11669    if let Some(row) = mask_row {
11670        zero_masked_cols(&mut g, b, inter, row);
11671    }
11672    if oracle_topk() > 0 {
11673        for t in 0..b {
11674            keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
11675        }
11676    }
11677    let mut out = vec![0.0f32; b * hidden];
11678    d.down_proj.matmat(&g, b, &mut out, pool);
11679    if refit_dir().is_some() {
11680        let li = crate::gpu::cur_layer();
11681        if li >= 0 {
11682            refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
11683        }
11684    }
11685    // The DTG-MA probe, on the batched path: one prefill sweep gives the
11686    // same per-neuron statistic the per-position probe does, and on a 27B
11687    // that is minutes instead of hours.
11688    FFN_PROBE.with(|pr| {
11689        if let Some(acc) = pr.borrow_mut().as_mut() {
11690            let li = crate::gpu::cur_layer();
11691            if li < 0 {
11692                return;
11693            }
11694            let Some(row) = acc.get_mut(li as usize) else {
11695                return;
11696            };
11697            let sq = probe_sq();
11698            for t in 0..b {
11699                for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
11700                    *a += if sq {
11701                        (v as f64) * (v as f64)
11702                    } else {
11703                        (v as f64).abs()
11704                    };
11705                }
11706            }
11707        }
11708    });
11709    out
11710}
11711
11712/// Batched MoE-FFN: router batched, positions are GROUPED by expert —
11713/// an expert's weights are read once for all its positions in the chunk
11714/// (the main prefill-GEMM win on MoE: 960MB/token of 35B experts).
11715/// Accumulate per-channel activation energy for `CMF_RMS_TRACE`.
11716fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
11717    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11718    static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11719    let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
11720    let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
11721    if (!on && !dump) || b == 0 {
11722        return;
11723    }
11724    let hidden = xs.len() / b;
11725    if on {
11726        let mut acc = m.act_sq.borrow_mut();
11727        if acc.len() < hidden {
11728            acc.resize(hidden, 0.0);
11729        }
11730        for t in 0..b {
11731            let row = &xs[t * hidden..(t + 1) * hidden];
11732            for (a, &v) in acc.iter_mut().zip(row) {
11733                *a += (v as f64) * (v as f64);
11734            }
11735        }
11736    }
11737    if dump {
11738        // Cap the capture: the covariance needs a few thousand rows, and a
11739        // whole prefill of every layer would be gigabytes for no extra rank.
11740        let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
11741            .ok()
11742            .and_then(|v| v.parse().ok())
11743            .unwrap_or(4096);
11744        let mut rows = m.act_rows.borrow_mut();
11745        if rows.len() < cap * hidden {
11746            let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
11747            rows.extend_from_slice(&xs[..take * hidden]);
11748        }
11749    }
11750}
11751
11752/// Send-able cursor over a Vec-of-Vecs: each pool worker writes only its
11753/// own slots (disjoint by construction in the caller).
11754#[derive(Clone, Copy)]
11755struct SendVecs(*mut Vec<f32>);
11756unsafe impl Send for SendVecs {}
11757unsafe impl Sync for SendVecs {}
11758impl SendVecs {
11759    #[inline]
11760    fn at(self, i: usize) -> *mut Vec<f32> {
11761        unsafe { self.0.add(i) }
11762    }
11763}
11764
11765fn moe_ffn_batch(
11766    m: &MoeFfn,
11767    xs: &[f32],
11768    b: usize,
11769    hidden: usize,
11770    pool: Option<&Pool>,
11771    allowed: Option<&[bool]>,
11772) -> Vec<f32> {
11773    accumulate_act(m, xs, b);
11774    let ne = m.experts.len();
11775    let mut logits = vec![0.0f32; b * ne];
11776    match &m.resonance {
11777        Some(r) => {
11778            let hdim = xs.len() / b.max(1);
11779            for bi in 0..b {
11780                r.scores(
11781                    &xs[bi * hdim..(bi + 1) * hdim],
11782                    &mut logits[bi * ne..(bi + 1) * ne],
11783                );
11784            }
11785        }
11786        None => m.router.matmat(xs, b, &mut logits, pool),
11787    }
11788
11789    // Assignments: expert → [(position, weight)] — same routing as
11790    // moe_ffn, per position (see `moe_route`).
11791    let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
11792    {
11793        let mut st = m.stats.borrow_mut();
11794        if st.len() < ne {
11795            st.resize(ne, 0);
11796        }
11797        for bi in 0..b {
11798            let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
11799            for &e in &idx {
11800                st[e] += 1;
11801                assign[e].push((bi, p[e] / wsum));
11802            }
11803        }
11804    }
11805
11806    let mut out = vec![0.0f32; b * hidden];
11807    let cols = m.experts[0].gate_proj.cols();
11808    let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
11809        let sb = list.len();
11810        let mut sub = vec![0.0f32; sb * cols];
11811        for (k, &(bi, _)) in list.iter().enumerate() {
11812            sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11813        }
11814        let eo = dense_ffn_batch(d, &sub, sb, pool, None);
11815        for (k, &(bi, w)) in list.iter().enumerate() {
11816            for i in 0..hidden {
11817                out[bi * hidden + i] += w * eo[k * hidden + i];
11818            }
11819        }
11820    };
11821    // Routed experts: the panels are TINY (b·top_k spread over every
11822    // expert — a few positions each), so a pool dispatch per expert is
11823    // pure barrier cost. Invert the parallelism: workers take WHOLE
11824    // experts (serial math inside), then one deterministic scatter in
11825    // expert order — the exact accumulation order the serial loop had.
11826    let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
11827    if pool.is_some() && active.len() >= 8 {
11828        let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
11829        {
11830            let panel_ptr = SendVecs(panels.as_mut_ptr());
11831            // Capture only the expert table: `m` itself carries RefCell
11832            // stats and must not cross the pool boundary.
11833            let experts = &m.experts;
11834            let (active_r, assign_r) = (&active, &assign);
11835            let run = |start: usize, end: usize| {
11836                for ai in start..end {
11837                    let e = active_r[ai];
11838                    let list = &assign_r[e];
11839                    let sb = list.len();
11840                    let mut sub = vec![0.0f32; sb * cols];
11841                    for (k, &(bi, _)) in list.iter().enumerate() {
11842                        sub[k * cols..(k + 1) * cols]
11843                            .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11844                    }
11845                    // SAFETY: each worker owns a disjoint panels[ai].
11846                    unsafe {
11847                        *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
11848                    }
11849                }
11850            };
11851            match pool {
11852                Some(p) => p.run_rows(active.len(), &run),
11853                None => run(0, active.len()),
11854            }
11855        }
11856        for (ai, &e) in active.iter().enumerate() {
11857            for (k, &(bi, w)) in assign[e].iter().enumerate() {
11858                let eo = &panels[ai][k * hidden..(k + 1) * hidden];
11859                for i in 0..hidden {
11860                    out[bi * hidden + i] += w * eo[i];
11861                }
11862            }
11863        }
11864    } else {
11865        for &e in &active {
11866            run_expert(&m.experts[e], &assign[e], &mut out);
11867        }
11868    }
11869    if let Some((se, gate)) = &m.shared {
11870        let all: Vec<(usize, f32)> = if let Some(gate) = gate {
11871            let mut gl = vec![0.0f32; b];
11872            gate.matmat(xs, b, &mut gl, pool);
11873            (0..b)
11874                .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
11875                .collect()
11876        } else {
11877            (0..b).map(|bi| (bi, 1.0)).collect()
11878        };
11879        run_expert(se, &all, &mut out);
11880    }
11881    out
11882}
11883
11884thread_local! {
11885    /// gate/up activation scratch for the dense FFN paths (single uses
11886    /// two slots, the fused pair all four) — these were fresh
11887    /// intermediate-size Vecs on every layer of every token.
11888    static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
11889        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
11890}
11891
11892/// Dense SwiGLU FFN through QTensor matvecs (any storage).
11893fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11894    // Per-token sparsity, when the file was built for it: gate first,
11895    // then only the chosen neurons' up/down rows leave the mmap.
11896    if gate_topk() > 0
11897        && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
11898    {
11899        return out;
11900    }
11901    // Whole-FFN GPU submit (этап 4.2 increment): gate → silu·up → down
11902    // chained in ONE command buffer with the intermediate activations
11903    // resident on the device — 3 per-op polls become 1 per layer. The
11904    // moe_block backend already implements exactly this chain; a dense
11905    // FFN is one expert with weight 1. Runtime probe: the chain still
11906    // pays one submit+poll per layer — alternate it against the pure-CPU
11907    // FFN and keep whichever is faster on this machine.
11908    // q1 FFNs offload at any practical size: the q1 CPU kernel is
11909    // compute-bound, so the UMA threshold logic does not apply — the
11910    // probe measures and decides either way.
11911    // The fused GPU block has no descriptor-aware Prism path: it would either
11912    // consume an unrotated activation or decline after inspecting the mixed
11913    // q2tp/q4tp tensors.  Do not let that structural refusal enter the FFN
11914    // probe's CPU_ONLY scope; the ordinary body below dispatches each matrix
11915    // through QTensor::matvec, which owns the signed FWHT + affine q2tp route.
11916    let prism_body = d.gate_proj.has_prism_contract()
11917        || d.up_proj.has_prism_contract()
11918        || d.down_proj.has_prism_contract();
11919    if !prism_body
11920        && crate::gpu::enabled_here()
11921        && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
11922    {
11923        let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
11924            crate::gpu::ProbeArm::Gpu
11925        } else {
11926            crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
11927        };
11928        match arm {
11929            crate::gpu::ProbeArm::Gpu => {
11930                let t0 = std::time::Instant::now();
11931                if let Some(out) = dense_ffn_gpu(d, x, pool) {
11932                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
11933                    return out;
11934                }
11935                // Declined: no timing exists, so say so. Silence here is
11936                // what left `ffn` undecided for 9000 calls and cost a
11937                // failed device attempt on half of them.
11938                crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
11939            }
11940            crate::gpu::ProbeArm::CpuTimed => {
11941                let t0 = std::time::Instant::now();
11942                let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11943                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
11944                return out;
11945            }
11946            crate::gpu::ProbeArm::Cpu => {
11947                return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11948            }
11949        }
11950    }
11951    dense_ffn_cpu(d, x, pool)
11952}
11953
11954/// The pure-CPU dense-FFN body (also the fallback of every GPU refusal).
11955fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11956    let inter = d.gate_proj.rows();
11957    FFN_SCRATCH.with(|s| {
11958        let mut s = s.borrow_mut();
11959        let [g, u, ..] = &mut *s;
11960        g.resize(inter, 0.0);
11961        // Fused gate+up+silu: one dispatch, no separate silu pass.
11962        // Falls back to matvec_many + silu loop for unsupported dtypes.
11963        if gate_topk() > 0 {
11964            // Gate first, select, and only then pay for `up`: the
11965            // measurement arm computes both and zeroes the losers, which
11966            // is the same arithmetic.
11967            u.resize(inter, 0.0);
11968            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11969            for i in 0..inter {
11970                g[i] = Act::Silu.combine(g[i], 1.0);
11971            }
11972            keep_top_k(g, gate_topk());
11973            for i in 0..inter {
11974                g[i] *= u[i];
11975            }
11976        } else if d.act == Act::Silu
11977            && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
11978        {
11979            // g now holds silu(gate)·up directly.
11980        } else {
11981            u.resize(inter, 0.0);
11982            // Multi-matrix job: gate+up under one pool dispatch.
11983            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11984            for i in 0..inter {
11985                g[i] = d.act.combine(g[i], u[i]);
11986            }
11987        }
11988        // DTG-MA bake probe (Patent 2): accumulate this layer's
11989        // per-neuron activation mass while a probe pass is active.
11990        // `CMF_FFN_PROBE_TOPK=k` switches the statistic from mass to a
11991        // HIT COUNT — how many tokens rank the neuron in their own top
11992        // k. Mass asks "how loud is this neuron overall", the count
11993        // asks "how often does this task actually need it", and the two
11994        // rank neurons differently whenever a few tokens are loud.
11995        FFN_PROBE.with(|pr| {
11996            if let Some(acc) = pr.borrow_mut().as_mut() {
11997                let li = crate::gpu::cur_layer();
11998                if li >= 0 {
11999                    if let Some(row) = acc.get_mut(li as usize) {
12000                        match probe_topk() {
12001                            0 if probe_sq() => {
12002                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12003                                    *a += (v as f64) * (v as f64);
12004                                }
12005                            }
12006                            0 if probe_signed() => {
12007                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12008                                    *a += v as f64;
12009                                }
12010                            }
12011                            0 => {
12012                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12013                                    *a += (v as f64).abs();
12014                                }
12015                            }
12016                            k => {
12017                                let n = g.len();
12018                                let k = k.min(n);
12019                                let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12020                                let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12021                                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12022                                });
12023                                let thr = *kth;
12024                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12025                                    if v.abs() >= thr {
12026                                        *a += 1.0;
12027                                    }
12028                                }
12029                            }
12030                        }
12031                    }
12032                }
12033            }
12034        });
12035        if oracle_topk() > 0 {
12036            keep_top_k(g, oracle_topk());
12037        }
12038        {
12039            let li = crate::gpu::cur_layer();
12040            if li >= 0 {
12041                adump_row(li as usize, g);
12042            }
12043        }
12044        let mut out = attention::take_buf(d.down_proj.rows());
12045        d.down_proj.matvec(g, &mut out, pool);
12046        out
12047    })
12048}
12049
12050/// Online accumulators for the AWNP refit of a narrowed FFN.
12051///
12052/// The refit needs `Gss = A_SᵀA_S` and `YA = YᵀA_S` per layer, where `A_S`
12053/// are the calibration activations of the KEPT neurons and `Y` the full
12054/// FFN output. Both are small enough to hold; the thing that is not is
12055/// the activations they are built from — a 27B layer would dump a
12056/// gigabyte per thousand tokens. So they are accumulated as the
12057/// calibration runs and written once at the end.
12058///
12059/// `CMF_FFN_REFIT=<dir>` holds `support.<L>.u32` (a u32 count then the
12060/// kept indices) for every layer to accumulate; `CMF_FFN_REFIT_FROM/TO`
12061/// bound the layer span so the accumulators fit in RAM.
12062pub struct RefitAcc {
12063    pub support: Vec<u32>,
12064    pub gss: Vec<f32>,
12065    pub ya: Vec<f32>,
12066    pub hidden: usize,
12067    pub tokens: u64,
12068    /// Activations staged transposed ([ns, t] and [hidden, t]) until the
12069    /// batch is worth a GEMM. The product costs `ns²` to move and add
12070    /// REGARDLESS of how many tokens went into it, so folding 16 chunks
12071    /// into one call cuts that cost 16× — it was 15 TB of traffic per
12072    /// calibration pass at one call per 256 tokens.
12073    pub buf_g: Vec<f32>,
12074    pub buf_o: Vec<f32>,
12075    pub buf_t: usize,
12076}
12077
12078/// The product buffer is SHARED across layers — one 473 MB allocation,
12079/// not one per layer (that was 30 GB of nothing on a 64-layer model).
12080/// It lives under the same lock as the accumulators.
12081type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
12082
12083static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
12084    std::sync::OnceLock::new();
12085
12086/// Is an FFN probe accumulator installed on this thread? The fused GPU
12087/// FFN must decline while one is, or the probe silently measures zero.
12088fn ffn_probe_active() -> bool {
12089    FFN_PROBE.with(|p| p.borrow().is_some())
12090}
12091
12092fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
12093    REFIT
12094        .get_or_init(|| {
12095            std::env::var("CMF_FFN_REFIT").ok().map(|d| {
12096                (
12097                    d,
12098                    std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
12099                )
12100            })
12101        })
12102        .as_ref()
12103}
12104
12105/// Accumulate one prefill panel into the layer's refit statistics.
12106fn refit_accumulate(
12107    li: usize,
12108    g: &[f32],
12109    b: usize,
12110    inter: usize,
12111    out: &[f32],
12112    hidden: usize,
12113    pool: Option<&Pool>,
12114) {
12115    let Some((dir, map)) = refit_dir() else {
12116        return;
12117    };
12118    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12119    let (from, to) = *SPAN.get_or_init(|| {
12120        let g = |k: &str, d: usize| {
12121            std::env::var(k)
12122                .ok()
12123                .and_then(|v| v.parse().ok())
12124                .unwrap_or(d)
12125        };
12126        (
12127            g("CMF_FFN_REFIT_FROM", 0),
12128            g("CMF_FFN_REFIT_TO", usize::MAX),
12129        )
12130    });
12131    if li < from || li > to {
12132        return;
12133    }
12134    let mut guard = map.lock().unwrap();
12135    let (map, shared) = &mut *guard;
12136    let acc = match map.entry(li) {
12137        std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
12138        std::collections::hash_map::Entry::Vacant(e) => {
12139            let path = format!("{dir}/support.{li}.u32");
12140            let Ok(bytes) = std::fs::read(&path) else {
12141                eprintln!("refit: no {path} — layer {li} skipped");
12142                return;
12143            };
12144            let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
12145            let support: Vec<u32> = bytes[4..4 + n * 4]
12146                .chunks_exact(4)
12147                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12148                .collect();
12149            eprintln!(
12150                "refit: layer {li} support {n} ({:.0} MB of accumulator)",
12151                (n * n + hidden * n) as f64 * 4.0 / 1e6
12152            );
12153            e.insert(RefitAcc {
12154                gss: vec![0.0; n * n],
12155                ya: vec![0.0; hidden * n],
12156                buf_g: Vec::new(),
12157                buf_o: Vec::new(),
12158                buf_t: 0,
12159                support,
12160                hidden,
12161                tokens: 0,
12162            })
12163        }
12164    };
12165    let ns = acc.support.len();
12166    // Stage this chunk transposed; the GEMM fires once the batch is full.
12167    let cap = refit_batch();
12168    if acc.buf_g.is_empty() {
12169        acc.buf_g = vec![0.0; ns * cap];
12170        acc.buf_o = vec![0.0; hidden * cap];
12171    }
12172    let take = b.min(cap - acc.buf_t);
12173    for t in 0..take {
12174        let col = acc.buf_t + t;
12175        for (j, &n) in acc.support.iter().enumerate() {
12176            acc.buf_g[j * cap + col] = g[t * inter + n as usize];
12177        }
12178        for h in 0..hidden {
12179            acc.buf_o[h * cap + col] = out[t * hidden + h];
12180        }
12181    }
12182    acc.buf_t += take;
12183    acc.tokens += take as u64;
12184    if acc.buf_t < cap {
12185        return;
12186    }
12187    let bt = acc.buf_t;
12188    acc.buf_t = 0;
12189    // The GEMM WRITES its C (it zeroes the accumulators it uses), so the
12190    // chunk product lands in scratch and is added on — the one thing that
12191    // silently turns a Gram over 13 000 tokens into a Gram over 256.
12192    // Both products are `C[n, m] += X[n, b] · Yᵀ[b, m]` with X and Y
12193    // stored row-major [·, b] — exactly `gemm_nt_f32`'s shape, so the
12194    // card does them when it is up (this is the whole calibration's
12195    // cost: O(|S|²) per token, 2.9 PFLOP for a 27B pass). The tiled CPU
12196    // loop stays as the fallback. Neither accumulates, so the product
12197    // lands in scratch and is added on.
12198    let RefitAcc {
12199        gss,
12200        ya,
12201        buf_g,
12202        buf_o,
12203        ..
12204    } = acc;
12205    let need = (ns * ns).max(hidden * ns);
12206    if shared.len() < need {
12207        shared.resize(need, 0.0);
12208    }
12209    let scratch = &mut shared[..];
12210    let _ = bt;
12211    if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
12212        add_into(gss, &scratch[..ns * ns], pool);
12213        if crate::gpu::gemm_nt_f32_transient(
12214            buf_o,
12215            buf_g,
12216            &mut scratch[..hidden * ns],
12217            hidden,
12218            cap,
12219            ns,
12220        ) {
12221            add_into(ya, &scratch[..hidden * ns], pool);
12222        } else {
12223            accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12224        }
12225    } else {
12226        accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
12227        accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12228    }
12229    // No zeroing: the batch is always filled exactly (cap is a multiple
12230    // of the prefill chunk), and a memset of 178 MB a layer would cost
12231    // more than the GEMM.
12232}
12233
12234/// `CMF_FFN_REFIT_BATCH` — tokens staged before each GEMM (default 4096).
12235fn refit_batch() -> usize {
12236    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12237    *B.get_or_init(|| {
12238        std::env::var("CMF_FFN_REFIT_BATCH")
12239            .ok()
12240            .and_then(|v| v.parse().ok())
12241            .unwrap_or(4096)
12242    })
12243}
12244
12245/// `c[m, n] += Σ_t left[m, t]·right[n, t]` — both operands transposed,
12246/// the CPU fallback for the staged batch.
12247fn accum_outer_t(
12248    c: &mut [f32],
12249    m: usize,
12250    n: usize,
12251    b: usize,
12252    left: &[f32],
12253    right: &[f32],
12254    pool: Option<&Pool>,
12255) {
12256    let ptr = SendMut(c.as_mut_ptr());
12257    let body = |i: usize| {
12258        let ptr = &ptr;
12259        let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
12260        for t in 0..b {
12261            let a = left[i * b + t];
12262            if a == 0.0 {
12263                continue;
12264            }
12265            for (j, o) in row.iter_mut().enumerate() {
12266                *o += a * right[j * b + t];
12267            }
12268        }
12269    };
12270    match pool {
12271        Some(p) if m > 1 => p.run_rows(m, &|s, e| {
12272            for i in s..e {
12273                body(i);
12274            }
12275        }),
12276        _ => {
12277            for i in 0..m {
12278                body(i);
12279            }
12280        }
12281    }
12282}
12283
12284/// `dst += src`, spread over the pool — at 118 M floats a layer this is
12285/// not a loop to leave on one core.
12286fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
12287    let n = dst.len().min(src.len());
12288    match pool {
12289        Some(p) if n >= 1 << 16 => {
12290            let ptr = SendMut(dst.as_mut_ptr());
12291            let f = |s: usize, e: usize| {
12292                let ptr = &ptr;
12293                for blk in s..e {
12294                    let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
12295                    for i in a..b {
12296                        unsafe { *ptr.0.add(i) += src[i] };
12297                    }
12298                }
12299            };
12300            p.run_rows(n.div_ceil(4096), &f);
12301        }
12302        _ => {
12303            for (d, v) in dst.iter_mut().zip(&src[..n]) {
12304                *d += *v;
12305            }
12306        }
12307    }
12308}
12309
12310/// `c[m, n] += Σ_t left[t, m]·right[t, n]`, with `left` stored [m, t] and
12311/// `right` [t, n]. Tiled over the rows of `c` so a tile stays in cache
12312/// while each token's `right` row streams past it once, and parallel
12313/// over tiles.
12314fn accum_outer(
12315    c: &mut [f32],
12316    m: usize,
12317    n: usize,
12318    b: usize,
12319    left: &[f32],
12320    right: &[f32],
12321    pool: Option<&Pool>,
12322) {
12323    const TILE: usize = 32;
12324    let tiles = m.div_ceil(TILE);
12325    let cp = SendMut(c.as_mut_ptr());
12326    let body = |ti: usize| {
12327        let cp = &cp;
12328        let i0 = ti * TILE;
12329        let i1 = (i0 + TILE).min(m);
12330        for t in 0..b {
12331            let r = &right[t * n..t * n + n];
12332            for i in i0..i1 {
12333                let a = left[i * b + t];
12334                if a == 0.0 {
12335                    continue;
12336                }
12337                // SAFETY: tiles partition c's rows; workers never overlap.
12338                let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
12339                for (o, v) in row.iter_mut().zip(r) {
12340                    *o += a * *v;
12341                }
12342            }
12343        }
12344    };
12345    match pool {
12346        Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
12347            for ti in s..e {
12348                body(ti);
12349            }
12350        }),
12351        _ => {
12352            for ti in 0..tiles {
12353                body(ti);
12354            }
12355        }
12356    }
12357}
12358
12359/// Write what the calibration accumulated: `gss.<L>.f32` and `ya.<L>.f32`.
12360pub fn refit_flush() -> usize {
12361    let Some((dir, map)) = refit_dir() else {
12362        return 0;
12363    };
12364    let guard = map.lock().unwrap();
12365    let mut n = 0;
12366    for (li, acc) in guard.0.iter() {
12367        // A silently truncated write here is a Gram that reshapes to
12368        // nothing an hour later — say it out loud instead.
12369        let w = |name: &str, v: &[f32]| {
12370            let path = format!("{dir}/{name}.{li}.f32");
12371            let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
12372            match std::fs::write(&path, &bytes) {
12373                Ok(()) => {}
12374                Err(e) => eprintln!(
12375                    "refit: FAILED to write {path} ({} MB): {e}",
12376                    bytes.len() / 1_000_000
12377                ),
12378            }
12379        };
12380        w("gss", &acc.gss);
12381        w("ya", &acc.ya);
12382        println!(
12383            "refit L{li}: {} support, {} tokens, hidden {}",
12384            acc.support.len(),
12385            acc.tokens,
12386            acc.hidden
12387        );
12388        n += 1;
12389    }
12390    n
12391}
12392
12393/// `CMF_FFN_ADUMP=<prefix>` — append every probed token's FFN activation
12394/// row to `<prefix>.<layer>.f16`. The co-activation record: which
12395/// neurons fire together, which is what a tube has to group if a token
12396/// is ever going to open one tube instead of sixteen.
12397fn adump_row(li: usize, g: &[f32]) {
12398    use std::io::Write as _;
12399    static FILES: std::sync::OnceLock<
12400        Option<(
12401            String,
12402            std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
12403        )>,
12404    > = std::sync::OnceLock::new();
12405    let Some((prefix, map)) = FILES
12406        .get_or_init(|| {
12407            std::env::var("CMF_FFN_ADUMP")
12408                .ok()
12409                .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
12410        })
12411        .as_ref()
12412    else {
12413        return;
12414    };
12415    // `CMF_FFN_ADUMP_FROM/_TO` narrow the dump to a layer span, so a big
12416    // calibration run fits on disk in a few passes instead of one.
12417    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12418    let (from, to) = *SPAN.get_or_init(|| {
12419        let g = |k: &str, d: usize| {
12420            std::env::var(k)
12421                .ok()
12422                .and_then(|v| v.parse().ok())
12423                .unwrap_or(d)
12424        };
12425        (
12426            g("CMF_FFN_ADUMP_FROM", 0),
12427            g("CMF_FFN_ADUMP_TO", usize::MAX),
12428        )
12429    });
12430    if li < from || li > to {
12431        return;
12432    }
12433    let mut map = map.lock().unwrap();
12434    let f = map.entry(li).or_insert_with(|| {
12435        std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
12436    });
12437    let mut bytes = Vec::with_capacity(g.len() * 2);
12438    for v in g {
12439        bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
12440    }
12441    let _ = f.write_all(&bytes);
12442}
12443
12444/// `CMF_FFN_ORACLE_TOPK` — keep only the k largest |silu(g)·u| of each
12445/// token and zero the rest. Not a serving mode: it is the CEILING of
12446/// contextual sparsity — what a per-token router would be chasing —
12447/// measured by cheating, since the selection reads the very activations
12448/// it would have to predict.
12449fn oracle_topk() -> usize {
12450    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12451    *K.get_or_init(|| {
12452        std::env::var("CMF_FFN_ORACLE_TOPK")
12453            .ok()
12454            .and_then(|v| v.parse().ok())
12455            .unwrap_or(0)
12456    })
12457}
12458
12459/// `CMF_FFN_GATE_TOPK` — the REALIZABLE cousin of the oracle: rank the
12460/// neurons by their gate alone (which the kernel has computed anyway
12461/// before it reads `up`), keep the k best, and drop the rest. Every
12462/// dropped neuron's `up` row and `down` column stay unread, so this is
12463/// the sparsity a serving path can actually take without a router.
12464fn gate_topk() -> usize {
12465    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12466    *K.get_or_init(|| {
12467        std::env::var("CMF_FFN_GATE_TOPK")
12468            .ok()
12469            .and_then(|v| v.parse().ok())
12470            .unwrap_or(0)
12471    })
12472}
12473
12474/// `CMF_FFN_GATE_BLOCK` — select in blocks of B neurons instead of one
12475/// by one. A scattered per-neuron choice cannot be read efficiently (a
12476/// row at a time, no prefetch runway); a block of 32 is a contiguous
12477/// 32-row slab of `up` and of the transposed `down`, which the ordinary
12478/// kernels stream. The question the measurement answers is what the
12479/// block costs in quality.
12480fn gate_block() -> usize {
12481    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12482    *B.get_or_init(|| {
12483        std::env::var("CMF_FFN_GATE_BLOCK")
12484            .ok()
12485            .and_then(|v| v.parse().ok())
12486            .unwrap_or(1)
12487    })
12488}
12489
12490/// Zero all but the `k` largest BLOCKS (by summed square) of a row.
12491fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
12492    let n = g.len();
12493    let nb = n.div_ceil(block);
12494    let kb = (keep_n.div_ceil(block)).clamp(1, nb);
12495    if kb >= nb {
12496        return;
12497    }
12498    let mut score: Vec<f32> = (0..nb)
12499        .map(|b| {
12500            g[b * block..((b + 1) * block).min(n)]
12501                .iter()
12502                .map(|v| v * v)
12503                .sum::<f32>()
12504        })
12505        .collect();
12506    let mut ord = score.clone();
12507    let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
12508        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12509    });
12510    let thr = *kth;
12511    for b in 0..nb {
12512        if score[b] < thr {
12513            g[b * block..((b + 1) * block).min(n)].fill(0.0);
12514        }
12515    }
12516    score.clear();
12517}
12518
12519/// Zero all but the `k` largest magnitudes of one token's activation row.
12520fn keep_top_k(g: &mut [f32], k: usize) {
12521    if gate_block() > 1 {
12522        return keep_top_blocks(g, k, gate_block());
12523    }
12524    let n = g.len();
12525    if k == 0 || k >= n {
12526        return;
12527    }
12528    let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12529    let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12530        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12531    });
12532    let thr = *kth;
12533    for v in g.iter_mut() {
12534        if v.abs() < thr {
12535            *v = 0.0;
12536        }
12537    }
12538}
12539
12540/// `CMF_FFN_PROBE_SQ` — accumulate Σa², so the dump divided by the token
12541/// count and square-rooted is the RMS activation trace Patent 12 weights
12542/// its matrices by.
12543fn probe_sq() -> bool {
12544    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12545    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
12546}
12547
12548/// `CMF_FFN_PROBE_SIGNED` — accumulate the SIGNED activation sum
12549/// instead of its magnitude: what a dropped neuron contributes ON
12550/// AVERAGE, which is the bias a narrowed FFN can add back for free.
12551fn probe_signed() -> bool {
12552    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12553    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
12554}
12555
12556/// `CMF_FFN_MEANFILL=<file>` — a masked-out neuron contributes its MEAN
12557/// activation instead of zero (`u32 layers, u32 inter, f32[…]`, the mass
12558/// dump layout, holding per-neuron means). Dropping a neuron outright
12559/// also drops its average contribution, which shifts the layer output by
12560/// a constant; filling the mean back is one add per layer and costs no
12561/// bytes off the bus. This is the measurement arm — in a tube file the
12562/// same correction ships as a per-task bias vector.
12563fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
12564    static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
12565    M.get_or_init(|| {
12566        let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
12567        let b = std::fs::read(&p).ok()?;
12568        let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
12569        let vals: Vec<f32> = b[8..]
12570            .chunks_exact(4)
12571            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12572            .collect();
12573        eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
12574        Some((inter, vals))
12575    })
12576    .as_ref()
12577}
12578
12579/// `CMF_FFN_PROBE_TOPK` — 0 (default) = accumulate mass, k>0 = count
12580/// how often a neuron lands in a token's top k.
12581fn probe_topk() -> usize {
12582    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12583    *K.get_or_init(|| {
12584        std::env::var("CMF_FFN_PROBE_TOPK")
12585            .ok()
12586            .and_then(|v| v.parse().ok())
12587            .unwrap_or(0)
12588    })
12589}
12590
12591thread_local! {
12592    /// DTG-MA activation probe: per-layer per-neuron Σ|silu(g)·u|
12593    /// accumulator, alive only during `Pipeline::probe_ffn_mass`.
12594    static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
12595        const { std::cell::RefCell::new(None) };
12596}
12597
12598/// Per-token structured sparsity, paid for in bytes.
12599///
12600/// The gate is the cheapest third of an FFN and it already says which
12601/// neurons matter: `silu(gate)` near zero means the neuron contributes
12602/// nothing whatever `up` says. So compute every gate, keep the `k`
12603/// loudest, and read ONLY those neurons' `up` rows and `down` rows —
12604/// the latter needs `down_proj` stored transposed, otherwise a neuron's
12605/// down weights are a strided column and "reading only those" costs a
12606/// full cache line each.
12607///
12608/// Returns `None` when the file has no transposed `down` (the caller
12609/// then runs the ordinary dense path).
12610fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
12611    // The scatter path reads individual rows/columns and cannot express the
12612    // per-matrix signed FWHT boundary.  Let the descriptor-aware dense path
12613    // handle Prism files rather than silently running an unrotated sparse
12614    // approximation.
12615    if d.gate_proj.has_prism_contract()
12616        || d.up_proj.has_prism_contract()
12617        || d.down_proj.has_prism_contract()
12618    {
12619        return None;
12620    }
12621    let dt = d.down_t.as_ref()?;
12622    let inter = d.gate_proj.rows();
12623    let hidden = dt.cols();
12624    if k == 0 || k >= inter || d.act != Act::Silu {
12625        return None;
12626    }
12627    DYN_SCRATCH.with(|sc| {
12628        let mut sc = sc.borrow_mut();
12629        let DynScratch {
12630            g,
12631            mag,
12632            live,
12633            parts,
12634        } = &mut *sc;
12635        g.resize(inter, 0.0);
12636        d.gate_proj.matvec(x, g, pool);
12637        for v in g.iter_mut() {
12638            *v = inference::silu(*v);
12639        }
12640        // The k-th largest |silu(gate)| is the threshold; ties keep more,
12641        // which is the safe side.
12642        mag.clear();
12643        mag.extend(g.iter().map(|v| v.abs()));
12644        let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12645            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12646        });
12647        let thr = *kth;
12648        live.clear();
12649        live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
12650        let mut out = vec![0.0f32; hidden];
12651        match pool {
12652            Some(p) if live.len() >= 64 => {
12653                let nw = p.n_workers() + 1;
12654                parts.clear();
12655                parts.resize(nw * hidden, 0.0);
12656                let ptr = SendMut(parts.as_mut_ptr());
12657                let n = live.len();
12658                let live_ref: &[u32] = live;
12659                let g_ref: &[f32] = g;
12660                p.run(&|w, workers| {
12661                    let chunk = n.div_ceil(workers);
12662                    let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
12663                    if s >= e {
12664                        return;
12665                    }
12666                    WORKER_SCRATCH.with(|ws| {
12667                        let mut ws = ws.borrow_mut();
12668                        let [scratch, acc] = &mut *ws;
12669                        scratch.resize(hidden.max(x.len()), 0.0);
12670                        acc.clear();
12671                        acc.resize(hidden, 0.0);
12672                        for (o, &nrm) in live_ref[s..e].iter().enumerate() {
12673                            // One neuron of runway: the next row's lines
12674                            // start moving while this one is multiplied.
12675                            if let Some(&nx) = live_ref[s..e].get(o + 1) {
12676                                d.up_proj.prefetch_row(nx as usize);
12677                                dt.prefetch_row(nx as usize);
12678                            }
12679                            let idx = nrm as usize;
12680                            let up = d.up_proj.row_dot(idx, x, scratch);
12681                            let a = g_ref[idx] * up;
12682                            if a != 0.0 {
12683                                dt.add_row_scaled(idx, a, acc, scratch);
12684                            }
12685                        }
12686                        for (j, v) in acc.iter().enumerate() {
12687                            unsafe { *ptr.at(w * hidden + j) = *v };
12688                        }
12689                    });
12690                });
12691                for w in 0..nw {
12692                    for (j, o) in out.iter_mut().enumerate() {
12693                        *o += parts[w * hidden + j];
12694                    }
12695                }
12696            }
12697            _ => {
12698                WORKER_SCRATCH.with(|ws| {
12699                    let mut ws = ws.borrow_mut();
12700                    let [scratch, _acc] = &mut *ws;
12701                    scratch.resize(hidden.max(x.len()), 0.0);
12702                    for &nrm in live.iter() {
12703                        let idx = nrm as usize;
12704                        let up = d.up_proj.row_dot(idx, x, scratch);
12705                        let a = g[idx] * up;
12706                        if a != 0.0 {
12707                            dt.add_row_scaled(idx, a, &mut out, scratch);
12708                        }
12709                    }
12710                });
12711            }
12712        }
12713        Some(out)
12714    })
12715}
12716
12717/// Caller-side scratch of the dynamic path — one allocation per thread,
12718/// not one per layer per token (that alone cost a third of the decode).
12719struct DynScratch {
12720    g: Vec<f32>,
12721    mag: Vec<f32>,
12722    live: Vec<u32>,
12723    parts: Vec<f32>,
12724}
12725
12726thread_local! {
12727    static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
12728        std::cell::RefCell::new(DynScratch {
12729            g: Vec::new(),
12730            mag: Vec::new(),
12731            live: Vec::new(),
12732            parts: Vec::new(),
12733        })
12734    };
12735    /// Pool-worker scratch: the row buffer and this worker's partial sum.
12736    static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
12737        const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
12738}
12739
12740/// `dense_ffn_cpu` with a per-visit mask landing on the activations —
12741/// the masked-inference fast path's decode arm. Full fused quant
12742/// compute, closed neurons zeroed before down: arithmetically the
12743/// pruned network, no dequant, no weight bytes touched.
12744fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
12745    let inter = d.gate_proj.rows();
12746    FFN_SCRATCH.with(|s| {
12747        let mut s = s.borrow_mut();
12748        let [g, u, ..] = &mut *s;
12749        g.resize(inter, 0.0);
12750        if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
12751            // g holds silu(gate)·up.
12752        } else {
12753            u.resize(inter, 0.0);
12754            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12755            for i in 0..inter {
12756                g[i] = d.act.combine(g[i], u[i]);
12757            }
12758        }
12759        zero_masked_cols(g, 1, inter, mask_row);
12760        let mut out = attention::take_buf(d.down_proj.rows());
12761        d.down_proj.matvec(g, &mut out, pool);
12762        out
12763    })
12764}
12765
12766/// Dense FFN as one GPU submission via the MoE block path (single
12767/// expert, weight 1.0): gate → silu·up → down chained in one command
12768/// buffer, intermediate activations device-resident. None → weights
12769/// not q8-mapped in the primary shard / over the VRAM budget / backend
12770/// refusal → honest CPU path.
12771fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
12772    if d.gate_proj.has_prism_contract()
12773        || d.up_proj.has_prism_contract()
12774        || d.down_proj.has_prism_contract()
12775    {
12776        return None;
12777    }
12778    // The GPU block hardcodes SiLU; GeLU FFNs (Gemma) stay on CPU.
12779    if d.act != Act::Silu {
12780        return None;
12781    }
12782    // Threshold: tiny FFNs are not worth a submission (q1 excepted —
12783    // see the caller's gate).
12784    if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
12785        return None;
12786    }
12787    let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
12788    let mut model_ref = None;
12789    moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
12790    let model = model_ref?;
12791    let hidden = jobs[0].down.1;
12792    let mut out = attention::take_buf(hidden);
12793    if crate::gpu::moe_block(&model, &jobs, &mut out) {
12794        Some(out)
12795    } else {
12796        let mut out = out;
12797        attention::recycle_buf(&mut out);
12798        None
12799    }
12800}
12801
12802/// q8-mapped primary-shard tensor parts for a GPU job: q8_2f carries
12803/// its column field, q8_row runs with empty col slices (the backend
12804/// skips the multiply). Shared by the MoE block and the dense-FFN
12805/// single-job path.
12806#[allow(clippy::type_complexity)]
12807#[allow(clippy::type_complexity)]
12808pub(crate) fn moe_parts(
12809    t: &QTensor,
12810) -> Option<(
12811    &std::sync::Arc<cortiq_core::CmfModel>,
12812    usize,
12813    usize,
12814    usize,
12815    &[f32],
12816    &[f32],
12817    bool,
12818    bool,
12819    bool,
12820)> {
12821    match t {
12822        QTensor::Mapped {
12823            model,
12824            idx,
12825            dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
12826            rows,
12827            cols,
12828            row_scale,
12829            col_field,
12830            ..
12831        } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
12832            model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
12833        )),
12834        // q1: tile-embedded scales — empty rs/col slices, raw xs.
12835        QTensor::Mapped {
12836            model,
12837            idx,
12838            dtype: cortiq_core::TensorDtype::Q1,
12839            rows,
12840            cols,
12841            ..
12842        } => Some((
12843            model,
12844            *idx,
12845            *rows,
12846            *cols,
12847            &[][..],
12848            &[][..],
12849            true,
12850            false,
12851            false,
12852        )),
12853        // q4_tiled: 18-byte tiles with embedded f16 scales — raw xs.
12854        QTensor::Mapped {
12855            model,
12856            idx,
12857            dtype: cortiq_core::TensorDtype::Q4Tiled,
12858            rows,
12859            cols,
12860            ..
12861        } => Some((
12862            model,
12863            *idx,
12864            *rows,
12865            *cols,
12866            &[][..],
12867            &[][..],
12868            false,
12869            true,
12870            false,
12871        )),
12872        // q4tp: same raw-xs contract, different stride and scale plane.
12873        QTensor::Mapped {
12874            model,
12875            idx,
12876            dtype: cortiq_core::TensorDtype::Q4TiledP,
12877            rows,
12878            cols,
12879            ..
12880        } => Some((
12881            model,
12882            *idx,
12883            *rows,
12884            *cols,
12885            &[][..],
12886            &[][..],
12887            false,
12888            true,
12889            false,
12890        )),
12891        // q2tp: the 2-bit expert plane of the mixed profile — q4 family
12892        // for stride bookkeeping, flagged q2 so the trio validation can
12893        // demand a q4tp down.
12894        QTensor::Mapped {
12895            model,
12896            idx,
12897            dtype: cortiq_core::TensorDtype::Q2TiledP,
12898            rows,
12899            cols,
12900            ..
12901        } => Some((
12902            model,
12903            *idx,
12904            *rows,
12905            *cols,
12906            &[][..],
12907            &[][..],
12908            false,
12909            true,
12910            true,
12911        )),
12912        _ => None,
12913    }
12914}
12915
12916/// Map a softmax-router MoE onto the Metal token graph's contract:
12917/// f32 router, gated shared expert, experts uniformly q4tp (or the
12918/// mixed profile: q2tp gate/up over a q4tp down). Sigmoid/bias/τ
12919/// routers, masks, per-expert scales and Gemma's router-input norm
12920/// refuse here — those semantics stay on the CPU path.
12921#[cfg(target_os = "macos")]
12922fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
12923    if m.router_sigmoid
12924        || m.router_input_norm
12925        || m.expert_bias.is_some()
12926        || m.route_tau.is_some()
12927        || m.mask.is_some()
12928        || m.per_expert_scale.is_some()
12929        || m.experts.is_empty()
12930        || m.top_k == 0
12931        || m.resonance.is_some()
12932    {
12933        return None;
12934    }
12935    // The select kernel hard-codes the gated shared expert; an
12936    // ungated one would need its own weight-1 slot.
12937    let (sh, sg) = match &m.shared {
12938        Some((sh, Some(sg))) => (sh, sg),
12939        _ => return None,
12940    };
12941    let (rf, rr, rc) = m.router.f32_parts()?;
12942    if rr != m.experts.len() || rc != hidden {
12943        return None;
12944    }
12945    let (sf, sr, sc) = sg.f32_parts()?;
12946    if sr * sc != hidden {
12947        return None;
12948    }
12949    let inter = m.experts[0].gate_proj.rows();
12950    // The first expert's gate decides the profile; every trio (shared
12951    // included) must agree — the jobs ladder flips ONE kernel for all.
12952    let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
12953    let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
12954        if e.act != Act::Silu
12955            || e.gate_proj.rows() != inter
12956            || e.gate_proj.cols() != hidden
12957            || e.up_proj.rows() != inter
12958            || e.up_proj.cols() != hidden
12959            || e.down_proj.rows() != hidden
12960            || e.down_proj.cols() != inter
12961        {
12962            return None;
12963        }
12964        let pick = |t: &QTensor| -> Option<usize> {
12965            if gu_q2 {
12966                t.mapped_q2tp().map(|(_, i)| i)
12967            } else {
12968                t.mapped_q4tp().map(|(_, i)| i)
12969            }
12970        };
12971        Some((
12972            pick(&e.gate_proj)?,
12973            pick(&e.up_proj)?,
12974            e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
12975        ))
12976    };
12977    let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
12978    let shared = trio(sh)?;
12979    Some(crate::gpu::GpuMoe {
12980        router: rf,
12981        sgate: sf,
12982        experts,
12983        shared,
12984        n_exp: m.experts.len(),
12985        top_k: m.top_k,
12986        inter,
12987        norm_topk: m.norm_topk_prob,
12988        route_scale: m.routed_scaling,
12989        gu_q2,
12990    })
12991}
12992
12993/// Build one gate/up/down GPU job from three tensors. `moe_push_job` is the
12994/// DenseFfn-shaped caller; architectures that keep their experts in their own
12995/// structs (DeepSeek-V4) come here directly.
12996pub(crate) fn moe_push_job_parts<'a>(
12997    gate: &'a QTensor,
12998    up: &'a QTensor,
12999    down: &'a QTensor,
13000    x: &[f32],
13001    w: f32,
13002    swiglu_limit: f32,
13003    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13004    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13005) -> Option<()> {
13006    use crate::qtensor::prescale;
13007    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
13008    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
13009    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
13010    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13011        return None; // mixed-dtype trio — honest CPU path
13012    }
13013    // The 2-bit profile is gate/up q2tp over a PLAIN q4tp down; any other
13014    // 2-bit arrangement stays on the CPU.
13015    if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
13016        return None;
13017    }
13018    if !gq2 && dq2 {
13019        return None;
13020    }
13021    model_ref.get_or_insert_with(|| gm.clone());
13022    let dt = |cf: &[f32]| {
13023        if cf.is_empty() {
13024            cortiq_core::TensorDtype::Q8Row
13025        } else {
13026            cortiq_core::TensorDtype::Q8_2f
13027        }
13028    };
13029    jobs.push(crate::gpu::MoeJob {
13030        gate: (gi, gr, gc, grs),
13031        up: (ui, ur, uc, urs),
13032        down: (di, dr, dc, drs),
13033        xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
13034        xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
13035        down_col: dcf,
13036        w,
13037        q1: gq1,
13038        q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
13039        q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
13040        gu_q2: gq2,
13041        swiglu_limit,
13042    });
13043    Some(())
13044}
13045
13046/// Build one gate/up/down GPU job (see `moe_parts`).
13047fn moe_push_job<'a>(
13048    d: &'a DenseFfn,
13049    x: &[f32],
13050    w: f32,
13051    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13052    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13053) -> Option<()> {
13054    use crate::qtensor::prescale;
13055    if d.act != Act::Silu {
13056        return None; // GPU block hardcodes SiLU
13057    }
13058    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
13059    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
13060    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
13061    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13062        return None; // mixed-dtype trio — honest CPU path
13063    }
13064    if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
13065        return None;
13066    }
13067    if !gq2 && dq2 {
13068        return None;
13069    }
13070    model_ref.get_or_insert_with(|| gm.clone());
13071    let gdt = if gcf.is_empty() {
13072        cortiq_core::TensorDtype::Q8Row
13073    } else {
13074        cortiq_core::TensorDtype::Q8_2f
13075    };
13076    let udt = if ucf.is_empty() {
13077        cortiq_core::TensorDtype::Q8Row
13078    } else {
13079        cortiq_core::TensorDtype::Q8_2f
13080    };
13081    jobs.push(crate::gpu::MoeJob {
13082        gate: (gi, gr, gc, grs),
13083        up: (ui, ur, uc, urs),
13084        down: (di, dr, dc, drs),
13085        xs_gate: prescale(x, gcf, gdt).into_owned(),
13086        xs_up: prescale(x, ucf, udt).into_owned(),
13087        down_col: dcf,
13088        w,
13089        q1: gq1,
13090        q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
13091        q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
13092        gu_q2: gq2,
13093        swiglu_limit: 0.0,
13094    });
13095    Some(())
13096}
13097
13098/// Sparse dense-FFN directly on QUANTIZED weights (mask × mmap): reads
13099/// ONLY the active neurons' gate/up rows and down columns from the mmap
13100/// — no full-matrix dequant, no f32 model copy. This is what lets a
13101/// masked big model run at quantized RSS (the historical mask path
13102/// forced the whole model to f32). Semantics identical to the f32
13103/// sparse path within quant tolerance.
13104fn sparse_ffn_quant(
13105    d: &DenseFfn,
13106    x: &[f32],
13107    active: &[u16],
13108    hidden: usize,
13109    pool: Option<&Pool>,
13110) -> Vec<f32> {
13111    let n = active.len();
13112    let inter = d.gate_proj.rows();
13113    let mut act = vec![0.0f32; n];
13114    // Scratch is needed if EITHER projection is group-packed (q4/vbit);
13115    // gate/up normally share a dtype but sizing on both is robust.
13116    let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
13117    let compute = |ai: usize| -> f32 {
13118        let idx = active[ai] as usize;
13119        if idx >= inter {
13120            return 0.0; // defensive parity with the f32 sparse path
13121        }
13122        let mut s = if need_scratch {
13123            vec![0.0f32; hidden]
13124        } else {
13125            Vec::new()
13126        };
13127        let gate = d.gate_proj.row_dot(idx, x, &mut s);
13128        let up = d.up_proj.row_dot(idx, x, &mut s);
13129        d.act.combine(gate, up)
13130    };
13131    match pool {
13132        Some(p) if n >= 256 => {
13133            let ptr = SendMut(act.as_mut_ptr());
13134            p.run(&|widx, nw| {
13135                let chunk = n.div_ceil(nw);
13136                let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
13137                for ai in s..e {
13138                    unsafe { *ptr.at(ai) = compute(ai) };
13139                }
13140            });
13141        }
13142        _ => {
13143            for (ai, a) in act.iter_mut().enumerate() {
13144                *a = compute(ai);
13145            }
13146        }
13147    }
13148    // Scatter through active down columns (reads only those columns).
13149    let mut out = vec![0.0f32; hidden];
13150    for (ai, &idx) in active.iter().enumerate() {
13151        let w = act[ai];
13152        if w.abs() >= 1e-12 && (idx as usize) < inter {
13153            d.down_proj.add_col_scaled(idx as usize, w, &mut out);
13154        }
13155    }
13156    out
13157}
13158
13159/// Test-only re-export of the private sparse-quant FFN (mask × mmap gate).
13160#[doc(hidden)]
13161pub fn sparse_ffn_quant_for_test(
13162    d: &DenseFfn,
13163    x: &[f32],
13164    active: &[u16],
13165    hidden: usize,
13166) -> Vec<f32> {
13167    sparse_ffn_quant(d, x, active, hidden, None)
13168}
13169
13170/// Dequantize a DenseFfn's three matrices to f32 (transient; only the
13171/// q4/vbit-masked fallback uses it — the memory-lean path is
13172/// sparse_ffn_quant). Reuses row_f32 row-by-row.
13173fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
13174    let deq = |t: &QTensor| -> Vec<f32> {
13175        let (rows, cols) = (t.rows(), t.cols());
13176        let mut out = vec![0.0f32; rows * cols];
13177        for r in 0..rows {
13178            t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
13179        }
13180        out
13181    };
13182    (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
13183}
13184
13185/// Pointer wrapper for the worker-pool scatter (same pattern as qtensor).
13186struct SendMut(*mut f32);
13187unsafe impl Send for SendMut {}
13188unsafe impl Sync for SendMut {}
13189impl SendMut {
13190    #[inline]
13191    // Deliberate unsynchronized scatter: pool workers write disjoint indices
13192    // in parallel, so returning `&mut` from `&self` is intentional here.
13193    #[allow(clippy::mut_from_ref)]
13194    unsafe fn at(&self, i: usize) -> &mut f32 {
13195        unsafe { &mut *self.0.add(i) }
13196    }
13197}
13198
13199/// Router → (selected experts in torch.topk order, per-expert score
13200/// vector, normalizer). The final weight of expert `e` is `p[e] / wsum`.
13201///
13202/// Two regimes share this. Qwen: softmax over ALL experts, top-k of the
13203/// probabilities, optional renorm — `router_sigmoid=false`, no bias,
13204/// scale 1 → bit-identical to the historical path. LFM2-MoE /
13205/// DeepSeek-V3 `noaux_tc`: per-expert sigmoid scores, an optional
13206/// selection bias (top-k CHOICE only; weights stay unbiased), a 1e-6 renorm
13207/// floor and a routed scale.
13208pub(crate) fn moe_route(
13209    logits: &[f32],
13210    m: &MoeFfn,
13211    allowed: Option<&[bool]>,
13212) -> (Vec<usize>, Vec<f32>, f32) {
13213    let ne = logits.len();
13214    let p: Vec<f32> = if m.router_sigmoid {
13215        logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
13216    } else {
13217        let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
13218        let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
13219        let s: f32 = e.iter().sum();
13220        for v in &mut e {
13221            *v /= s;
13222        }
13223        e
13224    };
13225    // Expert restriction: the static env mask (CMF_MOE_MASK) AND the
13226    // active task mask's expert fields (spec §5) both narrow the
13227    // candidate set; selection happens over the admitted experts only.
13228    // With norm_topk the kept weights renormalize below; without it
13229    // the excluded mass is honestly dropped.
13230    let admit = |e: usize| {
13231        m.mask.as_ref().is_none_or(|mk| mk[e])
13232            && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
13233    };
13234    let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
13235    // Descending by selection score, lower index wins ties (torch.topk).
13236    match &m.expert_bias {
13237        Some(b) => idx.sort_unstable_by(|&x, &y| {
13238            (p[y] + b[y])
13239                .partial_cmp(&(p[x] + b[x]))
13240                .unwrap()
13241                .then(x.cmp(&y))
13242        }),
13243        None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
13244    }
13245    idx.truncate(m.top_k);
13246    // Adaptive τ-routing: trim the tail experts once the kept mass is
13247    // enough. wsum below renormalizes over the KEPT set, so the output
13248    // stays a proper weighted average.
13249    if let Some(tau) = m.route_tau {
13250        let total: f32 = idx.iter().map(|&e| p[e]).sum();
13251        if total > 0.0 {
13252            let mut acc = 0.0f32;
13253            let mut keep = idx.len();
13254            for (i, &e) in idx.iter().enumerate() {
13255                acc += p[e];
13256                if acc >= tau * total {
13257                    keep = i + 1;
13258                    break;
13259                }
13260            }
13261            idx.truncate(keep);
13262        }
13263    }
13264    let wsum: f32 = if m.norm_topk_prob {
13265        let s: f32 = idx.iter().map(|&e| p[e]).sum();
13266        // LFM2 floors the denom (matches HF `+ 1e-6`); the softmax path's
13267        // probs already sum near 1, so it stays exactly as before.
13268        (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
13269    } else {
13270        1.0 / m.routed_scaling
13271    };
13272    (idx, p, wsum)
13273}
13274
13275/// See the call site: one `layer:e1,e2,…` line per routed token.
13276fn moe_trace(idx: &[usize]) {
13277    moe_trace_at(crate::gpu::cur_layer() as i32, idx)
13278}
13279
13280/// The same, for callers that know their layer (DSV4 owns its layers and
13281/// never sets the pipeline's current-layer marker).
13282pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
13283    use std::io::Write;
13284    static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
13285        std::sync::OnceLock::new();
13286    let Some(f) = F.get_or_init(|| {
13287        let p = std::env::var("CMF_MOE_TRACE").ok()?;
13288        Some(std::sync::Mutex::new(
13289            std::fs::OpenOptions::new()
13290                .create(true)
13291                .append(true)
13292                .open(p)
13293                .ok()?,
13294        ))
13295    }) else {
13296        return;
13297    };
13298    let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
13299    let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
13300}
13301
13302/// MoE FFN: router → top-k experts (see `moe_route`). Only selected
13303/// experts' pages are touched in mmap.
13304pub(crate) fn moe_ffn(
13305    m: &MoeFfn,
13306    x: &[f32],
13307    pool: Option<&Pool>,
13308    allowed: Option<&[bool]>,
13309) -> Vec<f32> {
13310    accumulate_act(m, x, 1);
13311    let ne = m.experts.len();
13312    let mut logits = vec![0.0f32; ne];
13313    match &m.resonance {
13314        Some(r) => r.scores(x, &mut logits),
13315        None => m.router.matvec(x, &mut logits, pool),
13316    }
13317    let (idx, p, wsum) = moe_route(&logits, m, allowed);
13318    {
13319        let mut st = m.stats.borrow_mut();
13320        if st.len() < ne {
13321            st.resize(ne, 0);
13322        }
13323        for &e in &idx {
13324            st[e] += 1;
13325        }
13326    }
13327    // `CMF_MOE_TRACE=<file>`: append one line per (layer, token) with the
13328    // selected expert ids. The cumulative `stats` above answer "which
13329    // experts are popular"; a residency design needs the question they
13330    // cannot answer — whether CONSECUTIVE tokens reuse experts (the
13331    // temporal locality an LRU cache lives on, FreeToken §4).
13332    moe_trace(&idx);
13333    // D5: the whole layer MoE block in one GPU command buffer (experts — the
13334    // same mmap via a no-copy buffer; intermediate activations on the GPU).
13335    // Same Ffn probe class as the dense chain: one submit per layer
13336    // either wins on this driver stack or it doesn't.
13337    if crate::gpu::enabled_here() {
13338        match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
13339            crate::gpu::ProbeArm::Gpu => {
13340                let t0 = std::time::Instant::now();
13341                if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
13342                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
13343                    return out;
13344                }
13345            }
13346            crate::gpu::ProbeArm::CpuTimed => {
13347                let t0 = std::time::Instant::now();
13348                let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13349                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
13350                return out;
13351            }
13352            crate::gpu::ProbeArm::Cpu => {
13353                return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13354            }
13355        }
13356    }
13357    moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
13358}
13359
13360/// One-shot report of whether the whole-token wgpu graph actually formed.
13361/// A refusal silently reverts to the per-op path, which is how a model can
13362/// look "GPU-accelerated" while every layer walks the host.  A device prefix
13363/// is tracked separately because it still pays a host boundary for the tail.
13364fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
13365    use std::sync::atomic::{AtomicBool, Ordering};
13366    if built {
13367        GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
13368        if total_layers > 0 && layers_run < total_layers {
13369            GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
13370        } else {
13371            GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
13372        }
13373    } else {
13374        GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
13375    }
13376    static SAID: AtomicBool = AtomicBool::new(false);
13377    if !SAID.swap(true, Ordering::Relaxed) {
13378        if built {
13379            tracing::info!("wgpu whole-token graph: ACTIVE");
13380        } else {
13381            tracing::warn!("wgpu whole-token graph refused — per-op path");
13382        }
13383    }
13384}
13385
13386/// Whole-token graph outcomes, process-wide: a benchmark that claims a
13387/// GPU number while MISS climbs is measuring the CPU — the honest-bench
13388/// contract makes that an error, not a footnote.
13389pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13390pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13391/// Graph calls that returned a hidden after running only a leading device
13392/// prefix.  These are valid hybrid executions but must not be reported as a
13393/// full GPU graph in benchmark evidence.
13394pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13395/// Graph calls that covered the complete requested layer span.
13396pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13397
13398/// Native Metal TokenGraph completion counters. These are incremented only
13399/// after checked command-buffer completion and successful readback, so a
13400/// fused-head NLL report can prove the route rather than infer it from env.
13401pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
13402    std::sync::atomic::AtomicU64::new(0);
13403pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
13404    std::sync::atomic::AtomicU64::new(0);
13405pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
13406    std::sync::atomic::AtomicU64::new(0);
13407pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
13408    std::sync::atomic::AtomicU64::new(0);
13409pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
13410    std::sync::atomic::AtomicU64::new(0);
13411/// Ordinary native-Metal rows-prefill admissions and completed rows.  These
13412/// counters are separate from TokenGraph token/head counts so a batch NLL
13413/// receipt cannot accidentally claim serial execution as batched.
13414pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
13415    std::sync::atomic::AtomicU64::new(0);
13416pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
13417    std::sync::atomic::AtomicU64::new(0);
13418pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
13419    std::sync::atomic::AtomicU64::new(0);
13420pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
13421    std::sync::atomic::AtomicU64::new(0);
13422
13423/// `CMF_MOE_BATCH=0` restores the per-expert serial loop — the A/B lever
13424/// for the batched kernel, and how its bit-identity is checked.
13425fn moe_batch_enabled() -> bool {
13426    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13427    *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
13428}
13429
13430/// Two-dispatch CPU MoE: every routed expert (and the shared one) fused
13431/// into one gate/up/SiLU dispatch and one down dispatch, instead of two
13432/// pool barriers per expert. Bit-identical to the serial loop below —
13433/// see `moe_gate_up_many` / `moe_down_many`. `None` = the batched kernel
13434/// does not cover this layer, walk the serial path.
13435fn moe_ffn_cpu_batched(
13436    m: &MoeFfn,
13437    x: &[f32],
13438    idx: &[usize],
13439    p: &[f32],
13440    wsum: f32,
13441    pool: Option<&Pool>,
13442) -> Option<Vec<f32>> {
13443    if idx.is_empty() || !moe_batch_enabled() {
13444        return None;
13445    }
13446    // The bake probe reads per-neuron activation mass out of the
13447    // single-expert path; batching would skip it. Rare and offline —
13448    // hand those runs to the serial loop.
13449    if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
13450        return None;
13451    }
13452    let n = idx.len() + usize::from(m.shared.is_some());
13453    let mut pairs = Vec::with_capacity(n);
13454    let mut downs = Vec::with_capacity(n);
13455    let mut ws = Vec::with_capacity(n);
13456    for &e in idx {
13457        let d = &m.experts[e];
13458        if d.act != Act::Silu {
13459            return None;
13460        }
13461        pairs.push((&d.gate_proj, &d.up_proj));
13462        downs.push(&d.down_proj);
13463        ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
13464    }
13465    // The shared expert goes last, matching the serial loop's order —
13466    // the f32 accumulation order is part of the bit-identity claim.
13467    if let Some((se, gate)) = &m.shared {
13468        if se.act != Act::Silu {
13469            return None;
13470        }
13471        let g = gate.as_ref().map_or(1.0, |gate| {
13472            let mut gl = [0.0f32; 1];
13473            gate.matvec(x, &mut gl, pool);
13474            1.0 / (1.0 + (-gl[0]).exp())
13475        });
13476        pairs.push((&se.gate_proj, &se.up_proj));
13477        downs.push(&se.down_proj);
13478        ws.push(g);
13479    }
13480    let inter = pairs[0].0.rows();
13481    let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
13482    if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
13483        return None;
13484    }
13485    let mut out = attention::take_buf(x.len());
13486    if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
13487        attention::recycle_buf(&mut out);
13488        return None;
13489    }
13490    Some(out)
13491}
13492
13493/// Exact CPU completion for the routed experts a dynamic device cache did
13494/// not contain. The weights are already the router's final normalized mix.
13495/// Keeping this independent of `MoeFfn` makes the job `Sync`: its routing
13496/// statistics live in a `RefCell`, while the immutable expert tensors can be
13497/// evaluated safely in parallel with the GPU's resident subset.
13498pub(crate) fn moe_cold_experts_cpu(
13499    experts: &[(&DenseFfn, f32)],
13500    x: &[f32],
13501    pool: Option<&Pool>,
13502) -> Vec<f32> {
13503    let mut out = attention::take_buf(x.len());
13504    if experts.is_empty() {
13505        return out;
13506    }
13507    let pairs: Vec<_> = experts
13508        .iter()
13509        .map(|(e, _)| (&e.gate_proj, &e.up_proj))
13510        .collect();
13511    let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
13512    let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
13513    let inter = experts[0].0.gate_proj.rows();
13514    let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
13515    if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
13516        && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
13517    {
13518        return out;
13519    }
13520    out.fill(0.0);
13521    for &(expert, weight) in experts {
13522        let mut one = dense_ffn(expert, x, pool);
13523        for (o, v) in out.iter_mut().zip(&one) {
13524            *o += weight * v;
13525        }
13526        attention::recycle_buf(&mut one);
13527    }
13528    out
13529}
13530
13531/// The pure-CPU MoE expert loop (also the fallback of every GPU refusal).
13532fn moe_ffn_cpu(
13533    m: &MoeFfn,
13534    x: &[f32],
13535    idx: &[usize],
13536    p: &[f32],
13537    wsum: f32,
13538    pool: Option<&Pool>,
13539) -> Vec<f32> {
13540    if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
13541        return out;
13542    }
13543    let mut out = attention::take_buf(x.len());
13544    for &e in idx {
13545        let mut eo = dense_ffn(&m.experts[e], x, pool);
13546        let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
13547        for i in 0..out.len() {
13548            out[i] += w * eo[i];
13549        }
13550        attention::recycle_buf(&mut eo);
13551    }
13552    if let Some((se, gate)) = &m.shared {
13553        let mut so = dense_ffn(se, x, pool);
13554        let g = gate.as_ref().map_or(1.0, |gate| {
13555            let mut gl = [0.0f32; 1];
13556            gate.matvec(x, &mut gl, pool);
13557            1.0 / (1.0 + (-gl[0]).exp())
13558        });
13559        for i in 0..out.len() {
13560            out[i] += g * so[i];
13561        }
13562        attention::recycle_buf(&mut so);
13563    }
13564    out
13565}
13566
13567/// DeepSeek-V2 MLA forward, expand-to-MHA form (see `AttnKind::Mla`):
13568/// per token the latent expands to every head's K/V and the ordinary
13569/// cache + grouped attend do the rest. K head layout is [rope | nope]
13570/// (rotary_dim = qk_rope rotates the shared rope key and each q head's
13571/// prefix); V rows are zero-padded to the K head_dim inside the cache
13572/// and the pad is sliced off before O. Attention importance is not
13573/// accumulated for MLA yet (no eviction interplay).
13574#[allow(clippy::too_many_arguments)]
13575fn mla_attention(
13576    w: &MlaWeights,
13577    normed: &[f32],
13578    cache: &mut crate::kv_cache::LayerKvCache,
13579    position: usize,
13580    inv_freq: &[f32],
13581    rope_scale: f32,
13582    eps: f64,
13583    pool: Option<&Pool>,
13584) -> Vec<f32> {
13585    let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
13586    let hd = dr + dn;
13587    let mut q = vec![0.0f32; nh * hd];
13588    match (&w.q_a, &w.q_a_norm) {
13589        (Some(qa), Some(qn)) => {
13590            let mut t = vec![0.0f32; qa.rows()];
13591            qa.matvec(normed, &mut t, pool);
13592            let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
13593            w.q_proj.matvec(&tn, &mut q, pool);
13594        }
13595        _ => w.q_proj.matvec(normed, &mut q, pool),
13596    }
13597    let mut ca = vec![0.0f32; lora + dr];
13598    w.kv_a.matvec(normed, &mut ca, pool);
13599    let (c_lat, k_rope) = ca.split_at_mut(lora);
13600    let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
13601    let mut kvb = vec![0.0f32; nh * (dn + dv)];
13602    w.kv_b.matvec(&latn, &mut kvb, pool);
13603    if !w.nope {
13604        attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
13605    }
13606    for h in 0..nh {
13607        if !w.nope {
13608            attention::rope_rotate_scaled(
13609                &mut q[h * hd..h * hd + dr],
13610                position,
13611                inv_freq,
13612                rope_scale,
13613            );
13614        }
13615    }
13616    let mut k = vec![0.0f32; nh * hd];
13617    let mut v = vec![0.0f32; nh * hd];
13618    for h in 0..nh {
13619        k[h * hd..h * hd + dr].copy_from_slice(k_rope);
13620        k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
13621        v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
13622    }
13623    cache.append(&k, &v, &vec![true; nh]);
13624    let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
13625    attention::recycle_buf(&mut imp);
13626    let mut ov = vec![0.0f32; nh * dv];
13627    for h in 0..nh {
13628        ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
13629    }
13630    let mut out = vec![0.0f32; w.o_proj.rows()];
13631    w.o_proj.matvec(&ov, &mut out, pool);
13632    out
13633}
13634
13635/// Gemma-4 dual-branch FFN (spec: see `FfnKind::DenseMoe`). The dense
13636/// branch reads the pre-FFN-normed activation; the router and the
13637/// expert branch read the RAW residual — the router through a
13638/// scale-less rms norm (its constant gain is folded into the weights),
13639/// the experts through `pre_norm_2`. CPU path; GPU graphs refuse the
13640/// layer kind honestly.
13641fn dense_moe_ffn(
13642    dm: &DenseMoeFfn,
13643    x_normed: &[f32],
13644    h_raw: &[f32],
13645    eps: f64,
13646    norm_style: NormStyle,
13647    pool: Option<&Pool>,
13648) -> Vec<f32> {
13649    let mut d = dense_ffn(&dm.dense, x_normed, pool);
13650    d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
13651    let m = &dm.moe;
13652    let ne = m.experts.len();
13653    let mut logits = vec![0.0f32; ne];
13654    if m.router_input_norm {
13655        let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
13656        let inv = 1.0 / (ss + eps as f32).sqrt();
13657        let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
13658        m.router.matvec(&xr, &mut logits, pool);
13659    } else {
13660        m.router.matvec(h_raw, &mut logits, pool);
13661    }
13662    let (idx, p, wsum) = moe_route(&logits, m, None);
13663    {
13664        let mut st = m.stats.borrow_mut();
13665        if st.len() < ne {
13666            st.resize(ne, 0);
13667        }
13668        for &e in &idx {
13669            st[e] += 1;
13670        }
13671    }
13672    let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
13673    let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
13674    let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
13675    for (di, mi) in d.iter_mut().zip(&mo) {
13676        *di += mi;
13677    }
13678    d
13679}
13680
13681/// Building the MoE-layer GPU jobs: all selected experts (+shared) must
13682/// be q8_2f-Mapped from the primary mapping; otherwise None → CPU path.
13683/// One-shot report of why the MoE GPU block refused. A silent `?` here
13684/// sends every expert to the CPU with nothing in the logs to say so —
13685/// which is exactly how a q4tp MoE model looked "GPU-accelerated" while
13686/// running entirely on the host.
13687fn moe_gpu_refused(why: &'static str) {
13688    use std::sync::atomic::{AtomicBool, Ordering};
13689    static SAID: AtomicBool = AtomicBool::new(false);
13690    if !SAID.swap(true, Ordering::Relaxed) {
13691        tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
13692    }
13693}
13694
13695fn moe_ffn_gpu(
13696    m: &MoeFfn,
13697    x: &[f32],
13698    idx: &[usize],
13699    p: &[f32],
13700    wsum: f32,
13701    pool: Option<&Pool>,
13702) -> Option<Vec<f32>> {
13703    use crate::gpu::MoeJob;
13704
13705    let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
13706    let mut model_ref = None;
13707    for &e in idx {
13708        if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
13709            moe_gpu_refused("push_job(expert)");
13710            return None;
13711        }
13712    }
13713    if let Some((se, gate)) = &m.shared {
13714        let g = gate.as_ref().map_or(1.0, |gate| {
13715            let mut gl = [0.0f32; 1];
13716            gate.matvec(x, &mut gl, pool);
13717            1.0 / (1.0 + (-gl[0]).exp())
13718        });
13719        if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
13720            moe_gpu_refused("push_job(shared)");
13721            return None;
13722        }
13723    }
13724    let Some(model) = model_ref else {
13725        moe_gpu_refused("no model_ref");
13726        return None;
13727    };
13728    let hidden = jobs[0].down.1;
13729    let mut out = vec![0.0f32; hidden];
13730    if crate::gpu::moe_block(&model, &jobs, &mut out) {
13731        Some(out)
13732    } else {
13733        moe_gpu_refused("gpu::moe_block");
13734        None
13735    }
13736}
13737
13738/// Single-position FFN dispatch.
13739fn ffn_forward(
13740    ffn: &FfnKind,
13741    x: &[f32],
13742    pool: Option<&Pool>,
13743    experts_allowed: Option<&[bool]>,
13744) -> Vec<f32> {
13745    match ffn {
13746        FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
13747        FfnKind::Dense(d) => dense_ffn(d, x, pool),
13748        FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
13749        // Dual-branch layers need the raw residual — their callers
13750        // dispatch dense_moe_ffn directly; the auxiliary paths that land
13751        // here (MTP draft, o1 replay) do not co-occur with gemma-4 MoE.
13752        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13753    }
13754}
13755
13756/// Fused two-position FFN: gate/up/down streamed once (dense). MoE
13757/// falls back to two singles — expert sets differ per position, there
13758/// is nothing to fuse.
13759fn ffn_forward_pair(
13760    ffn: &FfnKind,
13761    x1: &[f32],
13762    x2: &[f32],
13763    pool: Option<&Pool>,
13764    experts_allowed: Option<&[bool]>,
13765) -> (Vec<f32>, Vec<f32>) {
13766    let d = match ffn {
13767        // A tube layer has nothing to fuse across the pair — the tubes
13768        // are separate matrices; two singles are the honest path.
13769        FfnKind::Dense(d) if !d.segs.is_empty() => {
13770            return (
13771                tube_ffn(d, x1, 1, pool, None),
13772                tube_ffn(d, x2, 1, pool, None),
13773            );
13774        }
13775        FfnKind::Dense(d) => d,
13776        FfnKind::Moe(m) => {
13777            return (
13778                moe_ffn(m, x1, pool, experts_allowed),
13779                moe_ffn(m, x2, pool, experts_allowed),
13780            );
13781        }
13782        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13783    };
13784    let inter = d.gate_proj.rows();
13785    FFN_SCRATCH.with(|s| {
13786        let mut s = s.borrow_mut();
13787        let [g1, g2, u1, u2] = &mut *s;
13788        g1.resize(inter, 0.0);
13789        g2.resize(inter, 0.0);
13790        u1.resize(inter, 0.0);
13791        u2.resize(inter, 0.0);
13792        // Multi-matrix pair job: gate+up under one pool dispatch
13793        // (o1s = lane-1 outputs across tensors, o2s = lane-2).
13794        QTensor::matvec2_many(
13795            [&d.gate_proj, &d.up_proj],
13796            x1,
13797            x2,
13798            [g1.as_mut_slice(), u1.as_mut_slice()],
13799            [g2.as_mut_slice(), u2.as_mut_slice()],
13800            pool,
13801        );
13802        for i in 0..inter {
13803            g1[i] = d.act.combine(g1[i], u1[i]);
13804            g2[i] = d.act.combine(g2[i], u2[i]);
13805        }
13806        let mut o1 = attention::take_buf(d.down_proj.rows());
13807        let mut o2 = attention::take_buf(d.down_proj.rows());
13808        d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
13809        (o1, o2)
13810    })
13811}
13812
13813#[cfg(test)]
13814mod tests {
13815
13816    #[test]
13817    fn nll_graph_policy_scopes_only_the_fused_head() {
13818        for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
13819            // A Vulkan/Wgpu hidden-only graph remains the quality route.
13820            ("vulkan graph", true, true, false, true, false),
13821            // Native Metal adds the strict fused graph-head contract.
13822            ("native Metal graph", true, true, true, true, true),
13823            // Masked NLL and the explicit non-graph fallback remain unchanged.
13824            ("masked", false, true, false, false, false),
13825            ("graph disabled", true, false, true, false, false),
13826        ] {
13827            let (graph_quality, graph_head_required) =
13828                super::nll_graph_policy(unmasked, prefer_graph, native_metal);
13829            assert_eq!(graph_quality, want_graph, "{label}: graph quality");
13830            assert_eq!(graph_head_required, want_head, "{label}: fused head");
13831        }
13832    }
13833
13834    #[test]
13835    fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
13836        assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
13837        assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
13838        assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
13839        assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
13840        assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
13841    }
13842
13843    #[test]
13844    fn cancel_flag_stops_generation() {
13845        let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
13846        // Set before the call: the prefill loops honour it, the run
13847        // returns immediately with the cancelled reason and no tokens.
13848        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13849        let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
13850        assert_eq!(r.finish_reason, "cancelled");
13851        assert!(
13852            r.token_ids.is_empty(),
13853            "no tokens after cancel: {:?}",
13854            r.token_ids
13855        );
13856        assert_eq!(p.kv_cache.seq_len(), 0);
13857        assert!(p.kv_history.is_empty());
13858        assert!(!p.graph_want_logits);
13859        assert!(p.graph_logits.is_none());
13860        // Flag auto-cleared: the next call generates normally.
13861        let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
13862        assert_ne!(r2.finish_reason, "cancelled");
13863    }
13864    use super::*;
13865
13866    /// sparse_ffn_quant must equal a dense FFN where inactive neurons are
13867    /// zeroed (mask × mmap correctness). On F32 tensors this is EXACT —
13868    /// it validates the row_dot / add_col_scaled / scatter indexing, the
13869    /// bug-prone part. The q8 branches reuse the golden-tested linear
13870    /// The per-token sparse path reads a transposed `down`; it must
13871    /// agree with the arm that computes everything and zeroes the
13872    /// losers, or the speed measurement is measuring a different model.
13873    #[test]
13874    fn dynamic_ffn_equals_the_zeroing_arm() {
13875        let (hidden, inter) = (8usize, 32usize);
13876        let synth = |n: usize, salt: usize| -> Vec<f32> {
13877            (0..n)
13878                .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
13879                .collect()
13880        };
13881        let down = synth(hidden * inter, 3);
13882        let mut down_t = vec![0.0f32; inter * hidden];
13883        for r in 0..hidden {
13884            for c in 0..inter {
13885                down_t[c * hidden + r] = down[r * inter + c];
13886            }
13887        }
13888        let d = DenseFfn {
13889            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
13890            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
13891            down_proj: QTensor::from_f32(down.clone(), hidden, inter),
13892            act: Act::Silu,
13893            down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
13894            segs: Vec::new(),
13895        };
13896        let x = synth(hidden, 11);
13897        let k = 12usize;
13898        let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
13899        // Reference: full compute, keep the k loudest |silu(gate)|.
13900        let mut g = vec![0.0f32; inter];
13901        d.gate_proj.matvec(&x, &mut g, None);
13902        let mut u = vec![0.0f32; inter];
13903        d.up_proj.matvec(&x, &mut u, None);
13904        for v in g.iter_mut() {
13905            *v = inference::silu(*v);
13906        }
13907        keep_top_k(&mut g, k);
13908        for i in 0..inter {
13909            g[i] *= u[i];
13910        }
13911        let mut want = vec![0.0f32; hidden];
13912        d.down_proj.matvec(&g, &mut want, None);
13913        for (a, b) in want.iter().zip(&got) {
13914            assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
13915        }
13916    }
13917
13918    /// A tube layer is the same layer, re-cut. With every tube open the
13919    /// answer must equal the dense FFN over the concatenated neurons
13920    /// (the permutation is an identity on the layer's function); with a
13921    /// tube closed it must equal the dense FFN with those neurons
13922    /// zeroed — the mask semantics, now paid for in bytes not read.
13923    #[test]
13924    fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
13925        let (hidden, core, tube) = (8usize, 12usize, 8usize);
13926        let inter = core + tube;
13927        let synth = |n: usize, salt: usize| -> Vec<f32> {
13928            (0..n)
13929                .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
13930                .collect()
13931        };
13932        let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
13933        let d_all = synth(hidden * inter, 3);
13934        // The dense layer, and the same weights cut into core + tube.
13935        let dense = DenseFfn {
13936            gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
13937            up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
13938            down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
13939            act: Act::Silu,
13940            down_t: None,
13941            segs: Vec::new(),
13942        };
13943        let rows =
13944            |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
13945        let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
13946            let mut o = Vec::with_capacity(hidden * (b - a));
13947            for r in 0..hidden {
13948                o.extend_from_slice(&v[r * inter + a..r * inter + b]);
13949            }
13950            o
13951        };
13952        let tubed = DenseFfn {
13953            down_t: None,
13954            gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
13955            up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
13956            down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
13957            act: Act::Silu,
13958            segs: vec![FfnSeg {
13959                gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
13960                up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
13961                down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
13962                start: core,
13963                width: tube,
13964            }],
13965        };
13966        let x = synth(hidden, 7);
13967        let want = dense_ffn(&dense, &x, None);
13968        let got = tube_ffn(&tubed, &x, 1, None, None);
13969        for (a, b) in want.iter().zip(&got) {
13970            assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
13971        }
13972        // Closed tube: bits on for the core, off for the tube.
13973        let mut bits = vec![0u8; inter.div_ceil(8)];
13974        for n in 0..core {
13975            bits[n / 8] |= 1 << (n % 8);
13976        }
13977        let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
13978        let masked = dense_ffn_masked(&dense, &x, None, &bits);
13979        for (a, b) in masked.iter().zip(&closed) {
13980            assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
13981        }
13982        // The batched arm must agree with the single-position one.
13983        let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
13984        for (a, b) in closed.iter().zip(&batch) {
13985            assert_eq!(a, b, "batch arm disagrees with decode arm");
13986        }
13987    }
13988
13989    /// scale, structurally identical to the matvec kernels.
13990    #[test]
13991    fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
13992        let (hidden, inter) = (16usize, 40usize);
13993        let synth = |n: usize, salt: usize| -> Vec<f32> {
13994            (0..n)
13995                .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
13996                .collect()
13997        };
13998        let d = DenseFfn {
13999            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
14000            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
14001            down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
14002            act: Act::Silu,
14003            down_t: None,
14004            segs: Vec::new(),
14005        };
14006        let x = synth(hidden, 9);
14007        // Active = every 3rd neuron.
14008        let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
14009
14010        let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
14011
14012        // Reference: full dense FFN but g[i]=0 for inactive neurons.
14013        let mut g = vec![0.0f32; inter];
14014        d.gate_proj.matvec(&x, &mut g, None);
14015        let mut u = vec![0.0f32; inter];
14016        d.up_proj.matvec(&x, &mut u, None);
14017        let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
14018        for i in 0..inter {
14019            g[i] = if act_set.contains(&(i as u16)) {
14020                inference::silu(g[i]) * u[i]
14021            } else {
14022                0.0
14023            };
14024        }
14025        let mut reference = vec![0.0f32; hidden];
14026        d.down_proj.matvec(&g, &mut reference, None);
14027
14028        let max_d = sparse
14029            .iter()
14030            .zip(&reference)
14031            .map(|(a, b)| (a - b).abs())
14032            .fold(0.0f32, f32::max);
14033        assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
14034    }
14035
14036    /// Attach a synthetic MTP head (same structure as a main layer).
14037    fn attach_test_mtp(p: &mut Pipeline) {
14038        let (h, inter, heads, kv, hd) = (
14039            p.hidden_size,
14040            p.intermediate_size,
14041            p.num_heads,
14042            p.num_kv_heads,
14043            p.head_dim,
14044        );
14045        let synth = |n: usize, salt: usize| -> Vec<f32> {
14046            (0..n)
14047                .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
14048                .collect()
14049        };
14050        let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
14051            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14052        };
14053        p.mtp = Some(MtpModule {
14054            enorm: vec![1.0; h],
14055            hnorm: vec![1.0; h],
14056            eh_proj: qt(h, 2 * h, 301),
14057            layer: LayerWeights {
14058                input_norm: vec![1.0; h],
14059                post_norm: vec![1.0; h],
14060                attn_out_norm: None,
14061                ffn_out_norm: None,
14062                layer_scale: None,
14063                ffn: FfnKind::Dense(DenseFfn {
14064                    gate_proj: qt(inter, h, 315),
14065                    up_proj: qt(inter, h, 316),
14066                    down_proj: qt(h, inter, 317),
14067                    act: Act::Silu,
14068                    down_t: None,
14069                    segs: Vec::new(),
14070                }),
14071                attn: AttnKind::Full {
14072                    bias: None,
14073                    wq: qt(heads * hd, h, 311),
14074                    wk: qt(kv * hd, h, 312),
14075                    wv: qt(kv * hd, h, 313),
14076                    wo: qt(h, heads * hd, 314),
14077                    q_norm: None,
14078                    k_norm: None,
14079                    output_gate: false,
14080                    softplus_gate: None,
14081                },
14082            },
14083            final_norm: vec![1.0; h],
14084            kv: crate::kv_cache::LayerKvCache::new(kv, hd),
14085        });
14086    }
14087
14088    #[test]
14089    fn speculative_equals_vanilla_greedy() {
14090        // Speculative decode and the wgpu token graph are mutually
14091        // exclusive; a leaked CMF_GPU=wgpu from a parallel gpu test
14092        // would silently disable drafting. Pin the graph off.
14093        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14094        let run = |spec: bool| {
14095            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14096            p.sampler_config.temperature = 0.0;
14097            attach_test_mtp(&mut p);
14098            p.speculative = spec;
14099            let r = p.generate("abcdef", 12, None, None).unwrap();
14100            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
14101        };
14102        let (vanilla, d0, _) = run(false);
14103        let (spec, d1, a1) = run(true);
14104        assert_eq!(d0, 0, "vanilla path must not draft");
14105        assert!(d1 > 0, "speculative path must draft");
14106        assert_eq!(
14107            vanilla, spec,
14108            "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
14109        );
14110    }
14111
14112    #[test]
14113    fn speculative_accepts_constant_oracle() {
14114        // See speculative_equals_vanilla_greedy: pin the wgpu graph off.
14115        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14116        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14117        p.sampler_config.temperature = 0.0;
14118        p.sampler_config.repetition_penalty = 1.0;
14119        // Constant lm_head → every logit equal → both the main model and
14120        // the draft head argmax to token 0: acceptance must be 100%.
14121        p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
14122        attach_test_mtp(&mut p);
14123        p.speculative = true;
14124        let r = p.generate("abcd", 10, None, None).unwrap();
14125        assert!(r.mtp_drafted > 0);
14126        assert_eq!(
14127            r.mtp_accepted, r.mtp_drafted,
14128            "constant logits → every draft accepted"
14129        );
14130        // Ties resolve to the same token in both the main and draft
14131        // heads — the sequence is one repeated token.
14132        assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
14133    }
14134
14135    #[test]
14136    fn empty_prompt_is_an_error_not_a_panic() {
14137        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14138        let r = p.generate("", 4, None, None);
14139        assert!(r.is_err(), "empty prompt must be a clean error");
14140    }
14141
14142    #[test]
14143    fn every_token_enters_kv_exactly_once() {
14144        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14145        // Greedy so no RNG variance; byte tokenizer → 3 prompt tokens.
14146        p.sampler_config.temperature = 0.0;
14147        let r = p.generate("abc", 2, None, None).unwrap();
14148        assert_eq!(r.prompt_tokens, 3);
14149        // prompt(3) + first sampled token forwarded before second logits:
14150        // step0 samples from prefill hidden (no extra forward), then
14151        // forwards t1 → cache 4; step1 samples, loop ends (max_tokens).
14152        assert_eq!(
14153            p.kv_cache.seq_len(),
14154            3 + r.tokens_generated - 1,
14155            "each token must be cached exactly once (v1 cached the last prompt token twice)"
14156        );
14157    }
14158
14159    #[test]
14160    fn generation_is_reproducible_with_seed() {
14161        let run = || {
14162            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14163            p.generate("hello", 8, None, None).unwrap().token_ids
14164        };
14165        assert_eq!(run(), run());
14166    }
14167
14168    #[test]
14169    fn resetting_sampler_restarts_the_seeded_stream() {
14170        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14171        let config = SamplerConfig {
14172            seed: Some(1234),
14173            ..SamplerConfig::default()
14174        };
14175        p.set_sampler_config(config.clone());
14176        let first = p.generate("hello", 8, None, None).unwrap().token_ids;
14177        p.set_sampler_config(config);
14178        let second = p.generate("hello", 8, None, None).unwrap().token_ids;
14179        assert_eq!(first, second);
14180    }
14181
14182    #[test]
14183    fn eviction_bounds_the_cache() {
14184        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14185        p.kv_cache.max_seq_len = 6;
14186        p.sampler_config.temperature = 0.0;
14187        let _ = p.generate("abcd", 12, None, None).unwrap();
14188        assert!(
14189            p.kv_cache.seq_len() <= 6 + 1,
14190            "cache must stay bounded by max_seq_len (got {})",
14191            p.kv_cache.seq_len()
14192        );
14193    }
14194
14195    #[test]
14196    fn confidence_matches_tokens_and_is_a_probability() {
14197        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14198        p.sampler_config.temperature = 0.0;
14199        p.sampler_config.repetition_penalty = 1.0;
14200        let r = p.generate("abcd", 10, None, None).unwrap();
14201        assert_eq!(
14202            r.token_confidence.len(),
14203            r.token_ids.len(),
14204            "one confidence per emitted token"
14205        );
14206        for &c in &r.token_confidence {
14207            assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
14208        }
14209        // top1_prob is a valid softmax probability.
14210        let logits = [1.0f32, 3.0, 0.5, 3.0];
14211        let p0 = top1_prob_t(&logits, 1, 1.0);
14212        let p1 = top1_prob_t(&logits, 3, 1.0);
14213        assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
14214        assert!(p0 > 0.0 && p0 < 1.0);
14215        // Calibration temperature > 1 softens an over-confident peak.
14216        let sharp = top1_prob_t(&logits, 1, 1.0);
14217        let soft = top1_prob_t(&logits, 1, 2.0);
14218        assert!(soft < sharp, "higher temperature lowers peak confidence");
14219    }
14220
14221    #[test]
14222    fn trace_is_opt_in_and_parallels_the_output() {
14223        // Off by default: the runtime is silent unless observation asked.
14224        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14225        p.sampler_config.temperature = 0.0;
14226        p.sampler_config.repetition_penalty = 1.0;
14227        let r = p.generate("abcd", 10, None, None).unwrap();
14228        assert!(r.traces.is_empty(), "trace must be empty unless enabled");
14229
14230        // On: exactly one row per emitted token, aligned with the output.
14231        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14232        p.sampler_config.temperature = 0.0;
14233        p.sampler_config.repetition_penalty = 1.0;
14234        p.set_trace(true);
14235        let r = p.generate("abcd", 10, None, None).unwrap();
14236        assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
14237        for (i, tr) in r.traces.iter().enumerate() {
14238            assert_eq!(tr.t, i, "trace index is sequential");
14239            assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
14240            assert_eq!(
14241                tr.confidence, r.token_confidence[i],
14242                "trace confidence matches the confidence channel"
14243            );
14244            // No dynamic router in this pipeline → no skill, no coherence.
14245            assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
14246        }
14247    }
14248
14249    #[test]
14250    fn explain_prefill_logits_match_greedy_first_token() {
14251        // `cortiq explain` shows the next-token distribution from
14252        // prefill_next_logits; its argmax must equal what greedy generate
14253        // actually emits first — otherwise explain would lie.
14254        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14255        p.sampler_config.temperature = 0.0;
14256        p.sampler_config.repetition_penalty = 1.0;
14257        let ids = p.tokenizer.encode("abcd");
14258        let logits = p.prefill_next_logits(&ids, None);
14259        let argmax = logits
14260            .iter()
14261            .enumerate()
14262            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14263            .unwrap()
14264            .0 as u32;
14265        let r = p.generate("abcd", 1, None, None).unwrap();
14266        assert_eq!(
14267            argmax, r.token_ids[0],
14268            "explain preview must match greedy emit"
14269        );
14270    }
14271
14272    #[test]
14273    fn laguna_shared_expert_is_unconditionally_added() {
14274        let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
14275        let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
14276        let zero_dense = || DenseFfn {
14277            gate_proj: matrix(vec![0.0; 4]),
14278            up_proj: matrix(vec![0.0; 4]),
14279            down_proj: matrix(vec![0.0; 4]),
14280            act: Act::Silu,
14281            down_t: None,
14282            segs: Vec::new(),
14283        };
14284        let shared = DenseFfn {
14285            gate_proj: identity(),
14286            up_proj: identity(),
14287            down_proj: identity(),
14288            act: Act::Silu,
14289            down_t: None,
14290            segs: Vec::new(),
14291        };
14292        let x = [1.0, 2.0];
14293        let expected = dense_ffn(&shared, &x, None);
14294        let moe = MoeFfn {
14295            router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
14296            experts: vec![zero_dense()],
14297            top_k: 1,
14298            norm_topk_prob: true,
14299            router_sigmoid: true,
14300            expert_bias: None,
14301            routed_scaling: 1.0,
14302            route_tau: None,
14303            shared: Some((shared, None)),
14304            stats: std::cell::RefCell::new(Vec::new()),
14305            act_sq: std::cell::RefCell::new(Vec::new()),
14306            act_rows: std::cell::RefCell::new(Vec::new()),
14307            mask: None,
14308            per_expert_scale: None,
14309            router_input_norm: false,
14310            resonance: None,
14311        };
14312        let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
14313        for (actual, expected) in actual.iter().zip(expected) {
14314            assert!((actual - expected).abs() < 1e-6);
14315        }
14316    }
14317
14318    #[test]
14319    fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
14320        const B: usize = 19;
14321        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14322        p.set_o1(Some(crate::nystrom::O1Cfg {
14323            layers: crate::nystrom::O1Layers::All,
14324            m: 4,
14325            w: 8,
14326            sink: 2,
14327            rect: crate::nystrom::O1Rect::Aggregate,
14328        }));
14329        p.o1_begin_with_prefix(Some(B));
14330        let ids: Vec<u32> = (0..B as u32).collect();
14331        let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
14332
14333        assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
14334        assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
14335        let next = p.embed_single(B as u32);
14336        let _ = p.forward_layers(&next, B, None);
14337        assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
14338    }
14339
14340    #[test]
14341    fn o1_pair_transition_commits_scratch_before_epoch_publication() {
14342        const B: usize = 19;
14343        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14344        // Keep a real recurrent layer ahead of the Full O(1) layer so the
14345        // pair test observes the GDN lane-2 scratch swap at the same
14346        // boundary, rather than only exercising an artificial scratch vec.
14347        let gdn_cfg = crate::linear_core::GdnCfg {
14348            num_v_heads: 2,
14349            num_k_heads: 1,
14350            key_head_dim: 2,
14351            value_head_dim: 4,
14352            conv_kernel: 3,
14353            hidden_size: 8,
14354            rms_eps: 1e-6,
14355            output_gate_sigmoid: false,
14356        };
14357        let synth = |n: usize, salt: usize| -> Vec<f32> {
14358            (0..n)
14359                .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
14360                .collect()
14361        };
14362        let qt = |rows: usize, cols: usize, salt: usize| {
14363            crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14364        };
14365        let c_dim = gdn_cfg.conv_dim();
14366        let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
14367        p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
14368            in_proj_qkv: qt(c_dim, 8, 1),
14369            in_proj_z: qt(vd, 8, 2),
14370            in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
14371            in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
14372            conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
14373            a_log: vec![0.2, 0.5],
14374            dt_bias: synth(gdn_cfg.num_v_heads, 6),
14375            norm: vec![1.0; gdn_cfg.value_head_dim],
14376            out_proj: qt(8, vd, 7),
14377        });
14378        p.gdn_cfg = Some(gdn_cfg);
14379        p.set_o1(Some(crate::nystrom::O1Cfg {
14380            layers: crate::nystrom::O1Layers::All,
14381            m: 4,
14382            w: 8,
14383            sink: 2,
14384            rect: crate::nystrom::O1Rect::Aggregate,
14385        }));
14386        p.o1_begin_with_prefix(Some(B));
14387        for pos in 0..B - 2 {
14388            let emb = p.embed_single(pos as u32);
14389            let _ = p.forward_layers(&emb, pos, None);
14390        }
14391        let lane1_state = p.kv_cache.layers[0].linear_state.clone();
14392
14393        let e1 = p.embed_single((B - 2) as u32);
14394        let e2 = p.embed_single((B - 1) as u32);
14395        let _ = p.forward_pair(&e1, &e2, B - 2);
14396
14397        assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
14398        assert!(
14399            p.kv_cache
14400                .layers
14401                .iter()
14402                .enumerate()
14403                .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
14404        );
14405        assert!(!p.kv_cache.layers[0].linear_state.is_empty());
14406        assert_ne!(
14407            p.kv_cache.layers[0].linear_state, lane1_state,
14408            "real pair must commit GDN lane 2 before returning"
14409        );
14410        assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
14411        let next = p.embed_single(B as u32);
14412        let _ = p.forward_layers(&next, B, None);
14413        assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
14414    }
14415
14416    #[test]
14417    fn o1_error_observation_stays_terminal_until_reset() {
14418        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14419        p.set_o1(Some(crate::nystrom::O1Cfg {
14420            layers: crate::nystrom::O1Layers::All,
14421            m: 4,
14422            w: 8,
14423            sink: 2,
14424            rect: crate::nystrom::O1Rect::Aggregate,
14425        }));
14426        p.o1_begin();
14427        p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
14428
14429        assert!(p.o1_seal_checked().is_err());
14430        assert!(
14431            p.o1_seal_checked().is_err(),
14432            "retry must see the sticky error"
14433        );
14434        let k = vec![0.2f32; 4];
14435        let v = vec![0.3f32; 4];
14436        p.kv_cache.layers[0].append(&k, &v, &[]);
14437        assert_eq!(p.kv_cache.layers[0].seq_len, 0);
14438
14439        p.reset_session();
14440        p.o1_begin();
14441        p.kv_cache.layers[0].append(&k, &v, &[]);
14442        assert_eq!(p.kv_cache.layers[0].seq_len, 1);
14443    }
14444
14445    #[test]
14446    fn nll_graph_failure_is_terminal_and_request_is_reusable() {
14447        let ids = vec![1u32, 2, 3, 4, 5, 6];
14448        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14449        p.graph_logits = Some(vec![123.0]);
14450        p.graph_want_logits = true;
14451        p.graph_failed
14452            .store(true, std::sync::atomic::Ordering::Relaxed);
14453        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14454        let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
14455        assert!(err.contains("before NLL"));
14456        assert!(p.graph_logits.is_none());
14457        assert!(!p.graph_want_logits);
14458        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14459        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14460
14461        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14462        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14463        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14464        assert_eq!(actual.1, expected.1);
14465        assert!((actual.0 - expected.0).abs() < 1e-9);
14466    }
14467
14468    #[test]
14469    fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
14470        let ids = vec![1u32, 2, 3, 4, 5, 6];
14471        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14472        p.nll_test_fail_at = Some(1);
14473        let err = p
14474            .nll_ids_from(&ids, 0)
14475            .expect_err("one-shot forward failure");
14476        assert!(err.contains("forward") || err.contains("score row"));
14477        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14478        assert!(!p.graph_want_logits);
14479        assert!(p.graph_logits.is_none());
14480        assert!(p.kv_history.is_empty());
14481
14482        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14483        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14484        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14485        assert_eq!(actual.1, expected.1);
14486        assert!((actual.0 - expected.0).abs() < 1e-9);
14487    }
14488
14489    #[test]
14490    fn nll_serial_failure_before_first_row_is_reported() {
14491        let ids = vec![1u32, 2, 3, 4];
14492        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14493        p.nll_test_force_serial = true;
14494        p.nll_test_fail_at = Some(0);
14495        let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
14496        assert!(err.contains("serial forward"));
14497        assert!(p.kv_history.is_empty());
14498        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14499        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14500    }
14501
14502    #[test]
14503    fn ffn_probe_failure_discards_recorder_and_state() {
14504        let ids = vec![1u32, 2, 3, 4];
14505        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14506        p.nll_test_fail_at = Some(0);
14507        let err = p
14508            .probe_ffn_mass_batch(&ids)
14509            .expect_err("probe forward failure");
14510        assert!(err.contains("NLL"));
14511        assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
14512        assert!(p.kv_history.is_empty());
14513        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14514    }
14515
14516    #[test]
14517    fn nll_test_controls_are_pipeline_scoped() {
14518        let ids = vec![1u32, 2, 3, 4];
14519        let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14520        let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14521        failing.nll_test_force_serial = true;
14522        failing.nll_test_fail_at = Some(0);
14523
14524        assert!(!failing.can_prefill_batched());
14525        assert!(unaffected.can_prefill_batched());
14526        let expected = unaffected
14527            .nll_ids_from(&ids, 0)
14528            .expect("unaffected pipeline remains usable");
14529        let err = failing
14530            .nll_ids_from(&ids, 0)
14531            .expect_err("failure injection belongs to failing pipeline");
14532        assert!(err.contains("serial forward"));
14533        assert!(failing.nll_test_fail_at.is_none());
14534        assert!(unaffected.can_prefill_batched());
14535        let actual = unaffected
14536            .nll_ids_from(&ids, 0)
14537            .expect("unaffected pipeline remains reusable");
14538        assert_eq!(actual.1, expected.1);
14539        assert!((actual.0 - expected.0).abs() < 1e-9);
14540    }
14541
14542    #[test]
14543    fn forward_ids_failure_channel_is_terminal_and_reusable() {
14544        let ids = vec![1u32, 2, 3, 4, 5, 6];
14545        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14546        p.graph_logits = Some(vec![123.0]);
14547        p.graph_want_logits = true;
14548        p.graph_failed
14549            .store(true, std::sync::atomic::Ordering::Relaxed);
14550        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14551
14552        let err = p
14553            .forward_ids(&ids, None)
14554            .expect_err("a failed forward must not become a valid head result");
14555        assert!(err.contains("forward_ids setup"));
14556        assert!(p.graph_logits.is_none());
14557        assert!(!p.graph_want_logits);
14558        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14559        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14560        assert_eq!(p.kv_cache.seq_len(), 0);
14561
14562        let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
14563            .forward_ids(&ids, None)
14564            .expect("fresh forward_ids");
14565        let actual = p
14566            .forward_ids(&ids, None)
14567            .expect("pipeline remains reusable after a failed forward");
14568        assert_eq!(actual.len(), expected.len());
14569        assert!(
14570            actual
14571                .iter()
14572                .zip(expected)
14573                .all(|(a, b)| (a - b).abs() < 1e-9)
14574        );
14575        assert_eq!(p.kv_cache.seq_len(), ids.len());
14576    }
14577}