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        if self
1175            .weights
1176            .layers
1177            .iter()
1178            .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1179        {
1180            return true;
1181        }
1182        // MoE models too: the chunked CPU prefill runs every expert on the
1183        // host (Hy-MT2-30B-A3B on a Xeon: 8 tok/s of ingest against 53 of
1184        // graph decode), while the token graph — and the batched graph under
1185        // CMF_BATCH_K — keep the experts resident. Full attention in the
1186        // graph writes the KV mirror that decode reads, exactly as it does
1187        // for the hybrids' attention layers. Only when the whole stack is
1188        // resident: with a device prefix the per-position walk finishes
1189        // every token on the host, and the chunked prefill (GEMMs on the
1190        // card, the expert loop batched on the host) is the faster ingest
1191        // (the 8 GB ladder point: 7 tok/s chunked against ~1 walked).
1192        self.weights
1193            .layers
1194            .iter()
1195            .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1196            && self.automatic_gpu_prefix().is_none()
1197    }
1198
1199    #[cfg(target_os = "macos")]
1200    fn q1_graph_gpu(
1201        &mut self,
1202        start: usize,
1203        upto: Option<usize>,
1204        position: usize,
1205        h: &mut [f32],
1206    ) -> usize {
1207        let _mt0 = std::time::Instant::now(); // CMF_METAL_HOSTPROF
1208        use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1209        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1210        if self.attn_softcap > 0.0 // capped scores: no graph kernel — CPU path
1211            || !crate::gpu::enabled_here()
1212            || !graph_force
1213            || std::env::var("CMF_GPU_BLOCK")
1214                .map(|v| v == "0")
1215                .unwrap_or(false)
1216        {
1217            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1218                eprintln!(
1219                    "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
1220                    self.attn_softcap > 0.0,
1221                    crate::gpu::enabled_here(),
1222                    graph_force,
1223                );
1224            }
1225            if self.graph_head_required {
1226                self.fail_metal_graph("native graph front gate refused");
1227            }
1228            return start;
1229        }
1230        // The graph encodes SiLU FFN and full-context attention with an
1231        // explicit model scale. Architectures with sliding windows,
1232        // sandwich norms or non-SiLU FFNs still fall back to the CPU path.
1233        if self.swa.is_some()
1234            || self.global_attn.is_some()
1235            || self.attention_heads_per_layer.is_some()
1236            || self.attn_v_norm
1237            || self.weights.layers.iter().any(|lw| {
1238                lw.attn_out_norm.is_some()
1239                    || lw.ffn_out_norm.is_some()
1240                    || lw.layer_scale.is_some()
1241                    || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1242            })
1243        {
1244            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1245                eprintln!(
1246                    "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1247                    self.swa.is_some(),
1248                    self.global_attn.is_some(),
1249                    self.attention_heads_per_layer.is_some(),
1250                    self.attn_v_norm,
1251                    (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1252                );
1253            }
1254            if self.graph_head_required {
1255                self.fail_metal_graph("native graph architecture gate refused");
1256            }
1257            return start;
1258        }
1259        // Looped Transformer: the graph covers ALL loop iterations;
1260        // encode_loop_norm is inserted on-device at each boundary.
1261        let limit = upto
1262            .map(|u| u + 1)
1263            .unwrap_or(self.num_layers)
1264            .min(self.num_layers);
1265
1266        enum Item<'a> {
1267            Gdn {
1268                run: Vec<GdnGpuLayer<'a>>,
1269                first: usize,
1270            },
1271            Attn {
1272                l: AttnGpuLayer<'a>,
1273                li: usize,
1274                q_norm: Option<&'a [f32]>,
1275                k_norm: Option<&'a [f32]>,
1276                output_gate: bool,
1277                bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1278                /// Attend on the device too (no sync): F32 KV, no
1279                /// o1/bias, dims inside the kernels' contract.
1280                full_gpu: bool,
1281            },
1282        }
1283
1284        // Device-attend KERNEL contract, shared by every Full layer. The
1285        // hd>128 default-off POLICY is applied after the scan: it was
1286        // measured on dense models, and a MoE plan inverts it — with the
1287        // experts on device each CPU-attend sandwich costs a
1288        // commit+wait, ~30 submits/token (W2 on M4: 14.7 tok/s
1289        // sandwiched vs 27.1 device-attend vs 18.8 pure CPU).
1290        let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1291        let attend_contract = attend_mode != "0"
1292            && attend_mode != "off"
1293            && self.head_dim % 4 == 0
1294            && self.head_dim <= 256
1295            && self.rotary_dim >= 2
1296            && self.rotary_dim <= self.head_dim
1297            && (self.rotary_dim / 2) % 32 == 0
1298            && self.num_kv_heads > 0
1299            && self.num_heads % self.num_kv_heads == 0;
1300
1301        let mut plan: Vec<Item> = Vec::new();
1302        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1303        // Break-reason diagnostics ride the same env as the plan summary.
1304        let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1305        let mut scan = start;
1306        while scan < limit {
1307            let lw = &self.weights.layers[self.phys_layer(scan)];
1308            let ffn = match &lw.ffn {
1309                FfnKind::Dense(d) if d.segs.is_empty() => {
1310                    let (Some(g), Some(u), Some(dn)) = (
1311                        d.gate_proj.metal_graph_parts(),
1312                        d.up_proj.metal_graph_parts(),
1313                        d.down_proj.metal_graph_parts(),
1314                    ) else {
1315                        if block_diag {
1316                            eprintln!(
1317                                "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1318                            );
1319                        }
1320                        break;
1321                    };
1322                    MetalFfn::Dense {
1323                        gate: g,
1324                        up: u,
1325                        down: dn,
1326                    }
1327                }
1328                FfnKind::Moe(m) => {
1329                    let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1330                        if block_diag {
1331                            eprintln!(
1332                                "block-graph: L{scan} MoE outside the graph contract — run ends"
1333                            );
1334                        }
1335                        break;
1336                    };
1337                    if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1338                        model_ref.get_or_insert_with(|| model.clone());
1339                    }
1340                    MetalFfn::Moe(moe)
1341                }
1342                _ => {
1343                    if block_diag {
1344                        eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1345                    }
1346                    break;
1347                }
1348            };
1349            match &lw.attn {
1350                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1351                    let parts = (
1352                        w.in_proj_qkv.metal_graph_parts(),
1353                        w.in_proj_z.metal_graph_parts(),
1354                        w.in_proj_a.f32_parts(),
1355                        w.in_proj_b.f32_parts(),
1356                        w.out_proj.metal_graph_parts(),
1357                    );
1358                    let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1359                        if block_diag {
1360                            eprintln!(
1361                                "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1362                                w.in_proj_qkv.metal_graph_parts().is_some(),
1363                                w.in_proj_z.metal_graph_parts().is_some(),
1364                                w.in_proj_a.f32_parts().is_some(),
1365                                w.in_proj_b.f32_parts().is_some(),
1366                                w.out_proj.metal_graph_parts().is_some(),
1367                            );
1368                        }
1369                        break;
1370                    };
1371                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1372                        model_ref.get_or_insert_with(|| model.clone());
1373                    }
1374                    let gl = GdnGpuLayer {
1375                        attn_norm: &lw.input_norm,
1376                        post_norm: &lw.post_norm,
1377                        qkv,
1378                        z,
1379                        a,
1380                        b,
1381                        out,
1382                        ffn,
1383                        conv1d: &w.conv1d,
1384                        a_log: &w.a_log,
1385                        dt_bias: &w.dt_bias,
1386                        gnorm: &w.norm,
1387                    };
1388                    match plan.last_mut() {
1389                        Some(Item::Gdn { run, .. }) => run.push(gl),
1390                        _ => plan.push(Item::Gdn {
1391                            run: vec![gl],
1392                            first: scan,
1393                        }),
1394                    }
1395                }
1396                AttnKind::Full {
1397                    wq,
1398                    wk,
1399                    wv,
1400                    wo,
1401                    q_norm,
1402                    k_norm,
1403                    output_gate,
1404                    softplus_gate: None,
1405                    bias,
1406                } if !self.kv_cache.layers[scan].o1_sealed()
1407                    // Sealed o1 stays plannable when the Metal o1 port
1408                    // is on: full_gpu attends through the device state,
1409                    // and any refusal falls to the sandwich, whose CPU
1410                    // core routes sealed layers through the nystrom step.
1411                    || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1412                {
1413                    let parts = (
1414                        wq.metal_graph_parts(),
1415                        wk.metal_graph_parts(),
1416                        wv.metal_graph_parts(),
1417                        wo.metal_graph_parts(),
1418                    );
1419                    let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1420                        break;
1421                    };
1422                    if let QTensor::Mapped { model, .. } = wq {
1423                        model_ref.get_or_insert_with(|| model.clone());
1424                    }
1425                    let cache = &self.kv_cache.layers[scan];
1426                    // O(1) layer on Metal: the device attends through the
1427                    // sealed Nystrom state (opt-in while the port proves
1428                    // itself). Unsealed -> sandwich path = the CPU o1 step.
1429                    let o1_metal = cache.o1.is_some()
1430                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1431                        && cache.o1_views().is_some();
1432                    let full_gpu = attend_contract
1433                        && cache.mode == crate::kv_cache::KvMode::F32
1434                        && (cache.o1.is_none() || o1_metal)
1435                        && bias.is_none()
1436                        && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1437                        && pk.1 == self.num_kv_heads * self.head_dim
1438                        && pv.1 == self.num_kv_heads * self.head_dim
1439                        && po.2 == self.num_heads * self.head_dim;
1440                    plan.push(Item::Attn {
1441                        l: AttnGpuLayer {
1442                            attn_norm: &lw.input_norm,
1443                            post_norm: &lw.post_norm,
1444                            wq: pq,
1445                            wk: pk,
1446                            wv: pv,
1447                            wo: po,
1448                            ffn,
1449                        },
1450                        li: scan,
1451                        q_norm: q_norm.as_deref(),
1452                        k_norm: k_norm.as_deref(),
1453                        output_gate: *output_gate,
1454                        bias: bias
1455                            .as_ref()
1456                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1457                        full_gpu,
1458                    });
1459                }
1460                _ => break,
1461            }
1462            scan += 1;
1463        }
1464        let Some(model) = model_ref else {
1465            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1466                eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1467            }
1468            if self.graph_head_required {
1469                self.fail_metal_graph("native graph has no mapped model reference");
1470            }
1471            return start;
1472        };
1473        if plan.is_empty() {
1474            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1475                eprintln!("q1-graph: empty plan at layer {start}");
1476            }
1477            if self.graph_head_required {
1478                self.fail_metal_graph("native graph plan is empty");
1479            }
1480            return start;
1481        }
1482        let has_moe = plan.iter().any(|it| match it {
1483            Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1484            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1485        });
1486        let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1487        let dev_attend = attend_contract
1488            && (self.head_dim <= 128
1489                || has_moe
1490                // A GDN hybrid attends on a quarter of its layers: the
1491                // hd>128 caution was measured on pure-dense models where
1492                // gqa_attend dominates, and on Qwen3.8-27B (hd 256, 48
1493                // GDN + 16 attn) the sandwich costs 2x the whole decode
1494                // (1.2 vs 2.21 tok/s measured before the arena fix).
1495                || (self.head_dim <= 256 && has_gdn)
1496                || attend_mode == "force"
1497                || attend_mode == "256");
1498        if !dev_attend {
1499            for it in &mut plan {
1500                if let Item::Attn { li, full_gpu, .. } = it {
1501                    // The hd>128 policy is about gqa_attend; an o1 layer
1502                    // attends through its own kernel set.
1503                    let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1504                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1505                    if !keep_o1 {
1506                        *full_gpu = false;
1507                    }
1508                }
1509            }
1510        }
1511        if std::env::var("CMF_GRAPH_DBG").is_ok() {
1512            use std::sync::atomic::{AtomicBool, Ordering};
1513            static SAID: AtomicBool = AtomicBool::new(false);
1514            if !SAID.swap(true, Ordering::Relaxed) {
1515                let fg = plan
1516                    .iter()
1517                    .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1518                    .count();
1519                let att = plan
1520                    .iter()
1521                    .filter(|it| matches!(it, Item::Attn { .. }))
1522                    .count();
1523                eprintln!(
1524                    "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1525                    plan.len(),
1526                    self.head_dim,
1527                    self.rotary_dim,
1528                    self.num_kv_heads,
1529                    self.num_heads,
1530                );
1531            }
1532        }
1533        let dims = GraphDims {
1534            hidden: self.hidden_size,
1535            eps: self.rms_eps as f32,
1536            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1537        };
1538        let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1539            if self.graph_head_required {
1540                self.fail_metal_graph("native TokenGraph allocation refused");
1541            }
1542            return start;
1543        };
1544        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1545            nv: cfg.num_v_heads,
1546            nk: cfg.num_k_heads,
1547            dk: cfg.key_head_dim,
1548            dv: cfg.value_head_dim,
1549            kk: cfg.conv_kernel,
1550            hidden: self.hidden_size,
1551            inter: self.intermediate_size,
1552            c_dim: cfg.conv_dim(),
1553            eps: cfg.rms_eps as f32,
1554            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1555        });
1556        // Validate the whole plan BEFORE encoding anything: after the
1557        // first sync a refused layer would leave the token
1558        // half-executed, so truncate to the provably encodable prefix.
1559        let mut valid = 0usize;
1560        let mut end = start;
1561        crate::gpu::stageprof(1, _mt0.elapsed()); // конец планирования
1562        if std::env::var("CMF_PLAN_DUMP").is_ok() {
1563            static ONCE: std::sync::Once = std::sync::Once::new();
1564            ONCE.call_once(|| {
1565                for it in &plan {
1566                    match it {
1567                        Item::Gdn { first, run } => {
1568                            eprintln!("plan: Gdn first={first} len={}", run.len())
1569                        }
1570                        Item::Attn { li, full_gpu, .. } => {
1571                            eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1572                        }
1573                    }
1574                }
1575            });
1576        }
1577        for item in &plan {
1578            let ok = match item {
1579                Item::Gdn { run, .. } => gcfg
1580                    .as_ref()
1581                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1582                    .unwrap_or(false),
1583                Item::Attn { l, .. } => graph.attn_ok(l),
1584            };
1585            if !ok {
1586                if block_diag {
1587                    eprintln!(
1588                        "block-graph: plan item {} ({}) failed graph preflight",
1589                        valid,
1590                        match item {
1591                            Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1592                            Item::Attn { li, .. } => format!("Attn L{li}"),
1593                        }
1594                    );
1595                }
1596                break;
1597            }
1598            valid += 1;
1599            end += match item {
1600                Item::Gdn { run, .. } => run.len(),
1601                Item::Attn { .. } => 1,
1602            };
1603        }
1604        plan.truncate(valid);
1605        if plan.is_empty() {
1606            if self.graph_head_required {
1607                self.fail_metal_graph("native graph preflight produced no valid items");
1608            }
1609            return start;
1610        }
1611
1612        if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
1613            self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
1614            return start;
1615        }
1616
1617        let inv_freq = self.inv_freq.clone();
1618        let pool = self.pool.clone();
1619        let (nh, nkv, hd, hs, rd, eps) = (
1620            self.num_heads,
1621            self.num_kv_heads,
1622            self.head_dim,
1623            self.hidden_size,
1624            self.rotary_dim,
1625            self.rms_eps,
1626        );
1627        let norm_style = self.norm_style;
1628        let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1629        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1630        let kv_id = self.graph_kv_id;
1631        // GDN runs whose states await readback after the next sync
1632        // (device-attended layers add no sync, so several may stack).
1633        let mut pending: Vec<(usize, usize)> = Vec::new();
1634        // Device-attended layers: their K/V/imp are pulled from the
1635        // mirror after the final sync.
1636        let mut dev_attn: Vec<usize> = Vec::new();
1637        for item in &plan {
1638            let _xt0 = std::time::Instant::now();
1639            let _xkind: u32 = match item {
1640                Item::Gdn { .. } => 2,
1641                Item::Attn { .. } => 3,
1642            };
1643            // Looped Transformer: insert on-device norm at loop boundaries.
1644            if self.loop_final_norm {
1645                let item_start = match item {
1646                    Item::Gdn { first, .. } => *first,
1647                    Item::Attn { li, .. } => *li,
1648                };
1649                if item_start > start && self.is_loop_end(item_start - 1) {
1650                    graph.encode_loop_norm(&self.weights.final_norm);
1651                }
1652            }
1653            match item {
1654                Item::Gdn { run, first } => {
1655                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1656                        if l.linear_state.len() != want {
1657                            l.linear_state = vec![0f32; want];
1658                        }
1659                    }
1660                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1661                        .iter()
1662                        .map(|l| l.linear_state.as_slice())
1663                        .collect();
1664                    let _ig = std::time::Instant::now();
1665                    if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1666                        // Unreachable: the plan was validated above.
1667                        tracing::error!("q1 graph: GDN run refused after validation");
1668                        return start;
1669                    }
1670                    // Early commit: the GPU starts the run while the
1671                    // CPU encodes the next layer (nothing to wait on).
1672                    graph.commit_kind = 2;
1673                    graph.commit();
1674                    crate::gpu::stageprof(0, _ig.elapsed());
1675                    pending.push((*first, run.len()));
1676                }
1677                Item::Attn {
1678                    l,
1679                    li,
1680                    q_norm,
1681                    k_norm,
1682                    output_gate,
1683                    bias,
1684                    full_gpu,
1685                } => {
1686                    let _ia = std::time::Instant::now();
1687                    // ── Fully device-resident attention: no sync at all.
1688                    if *full_gpu {
1689                        let cache = &self.kv_cache.layers[*li];
1690                        let o1p = if cache.o1.is_some() {
1691                            match cache.o1_views() {
1692                                Some(views) => Some(crate::gpu::O1AttnParams {
1693                                    views,
1694                                    epoch: self.o1_epoch,
1695                                }),
1696                                // Sealed state gone mid-run: sandwich.
1697                                None => None,
1698                            }
1699                        } else {
1700                            None
1701                        };
1702                        let o1_layer = cache.o1.is_some();
1703                        if o1_layer && o1p.is_none() {
1704                            // fall to the sandwich (CPU o1 step)
1705                        }
1706                        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1707                        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1708                        let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1709                        let p = crate::gpu::AttnDeviceParams {
1710                            kv_id,
1711                            layer: *li,
1712                            nh,
1713                            nkv,
1714                            hd,
1715                            rd,
1716                            position,
1717                            scale: self.attn_scale,
1718                            eps: eps as f32,
1719                            gemma,
1720                            late_qk_norm: self.qk_norm_after_rope,
1721                            output_gate: *output_gate,
1722                            q_norm: *q_norm,
1723                            k_norm: *k_norm,
1724                            inv_freq: &inv_freq,
1725                            cpu_k,
1726                            cpu_v,
1727                            cpu_stored,
1728                            o1: o1p,
1729                        };
1730                        let o1_bad = o1_layer && p.o1.is_none();
1731                        if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1732                        {
1733                            // o1 layers leave no mirror row to pull.
1734                            if p.o1.is_none() {
1735                                dev_attn.push(*li);
1736                            }
1737                            graph.commit_kind = 3;
1738                            graph.commit();
1739                            // The footer below is skipped by `continue`:
1740                            // account the device-attn item here or its
1741                            // cost hides from the stage profile entirely.
1742                            crate::gpu::stageprof(_xkind, _xt0.elapsed());
1743                            continue;
1744                        }
1745                        // Mirror refused (nothing encoded) → sandwich.
1746                    }
1747                    graph.encode_attn_prefix(l);
1748                    if let Err(err) = graph.sync_checked() {
1749                        self.fail_metal_graph(&err);
1750                        return start;
1751                    }
1752                    if !pending.is_empty() {
1753                        let idxs: Vec<usize> =
1754                            pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1755                        let mut outs: Vec<&mut [f32]> = self
1756                            .kv_cache
1757                            .layers
1758                            .iter_mut()
1759                            .enumerate()
1760                            .filter(|(i, _)| idxs.binary_search(i).is_ok())
1761                            .map(|(_, s)| s.linear_state.as_mut_slice())
1762                            .collect();
1763                        graph.read_states(&mut outs);
1764                    }
1765                    let mut q_raw = attention::take_buf(l.wq.1);
1766                    let mut k = attention::take_buf(l.wk.1);
1767                    let mut v = attention::take_buf(l.wv.1);
1768                    graph.read_qkv(&mut q_raw, &mut k, &mut v);
1769                    let cfg = QwenAttnCfg {
1770                        num_heads: nh,
1771                        num_kv_heads: nkv,
1772                        head_dim: hd,
1773                        hidden_size: hs,
1774                        position,
1775                        inv_freq: &inv_freq,
1776                        rotary_dim: rd,
1777                        scale: self.attn_scale,
1778                        softcap: self.attn_softcap,
1779                        window: None,
1780                        v_norm: false,
1781                        qk_norm_after_rope: self.qk_norm_after_rope,
1782                        q_norm: *q_norm,
1783                        k_norm: *k_norm,
1784                        output_gate: *output_gate,
1785                        softplus_gate: None,
1786                        rope_scale: 1.0,
1787                        bias: *bias,
1788                        rms_eps: eps,
1789                        norm_style,
1790                        pool: pool.as_deref(),
1791                    };
1792                    // CMF_ATTN_ORACLE=1: diff the device attend against
1793                    // this CPU attend on identical inputs (bring-up).
1794                    let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1795                        || std::env::var("CMF_ATTN_DUMP").is_ok();
1796                    let _ = full_gpu;
1797                    let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1798                    let mut ao = attention::qwen_attention_core(
1799                        q_raw,
1800                        k,
1801                        v,
1802                        &mut self.kv_cache.layers[*li],
1803                        &cfg,
1804                    );
1805                    // CMF_ATTN_DUMP=<dir>: this token's rope'd Q and the layer's whole
1806                    // K/V cache as raw f32 (offline attention-statistics probes:
1807                    // block bounds, mass concentration). Needs CMF_GPU_ATTEND=0.
1808                    if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1809                        if let Some((qr0, k0, v0)) = oracle_in.clone() {
1810                            let (cq, _cg, _ck, _cv) =
1811                                attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1812                            let cache = &self.kv_cache.layers[*li];
1813                            let n = cache.head_keys(0).len() / hd;
1814                            let mut bytes: Vec<u8> = Vec::new();
1815                            for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1816                                bytes.extend_from_slice(&v.to_le_bytes());
1817                            }
1818                            for v in &cq {
1819                                bytes.extend_from_slice(&v.to_le_bytes());
1820                            }
1821                            for g in 0..nkv {
1822                                for v in cache.head_keys(g) {
1823                                    bytes.extend_from_slice(&v.to_le_bytes());
1824                                }
1825                            }
1826                            for g in 0..nkv {
1827                                for v in cache.head_values(g) {
1828                                    bytes.extend_from_slice(&v.to_le_bytes());
1829                                }
1830                            }
1831                            let _ =
1832                                std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1833                        }
1834                    }
1835                    if let Some((qr0, k0, v0)) =
1836                        oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
1837                    {
1838                        let (cq, _cg, ck, cv) =
1839                            attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1840                        let mut h_now = vec![0f32; hs];
1841                        graph.read_h(&mut h_now);
1842                        let cache = &self.kv_cache.layers[*li];
1843                        let n_after = cache.head_keys(0).len() / hd;
1844                        // A sealed O(1) cache may have no dense current-row
1845                        // entry. The oracle is a debug probe, so let it see
1846                        // zero stored exact rows instead of underflowing.
1847                        let stored = n_after.saturating_sub(1);
1848                        let cpu_k: Vec<&[f32]> = (0..nkv)
1849                            .map(|g| &cache.head_keys(g)[..stored * hd])
1850                            .collect();
1851                        let cpu_v: Vec<&[f32]> = (0..nkv)
1852                            .map(|g| &cache.head_values(g)[..stored * hd])
1853                            .collect();
1854                        let p = crate::gpu::AttnDeviceParams {
1855                            kv_id,
1856                            layer: *li,
1857                            nh,
1858                            nkv,
1859                            hd,
1860                            rd,
1861                            position,
1862                            scale: self.attn_scale,
1863                            eps: eps as f32,
1864                            gemma,
1865                            late_qk_norm: self.qk_norm_after_rope,
1866                            output_gate: *output_gate,
1867                            q_norm: *q_norm,
1868                            k_norm: *k_norm,
1869                            inv_freq: &inv_freq,
1870                            cpu_k,
1871                            cpu_v,
1872                            cpu_stored: stored,
1873                            o1: None,
1874                        };
1875                        if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1876                            let md = |a: &[f32], b: &[f32]| {
1877                                a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
1878                            };
1879                            let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1880                            eprintln!(
1881                                "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}",
1882                                nn(&cq),
1883                                md(&cq, &dq),
1884                                nn(&ck),
1885                                md(&ck, &dk),
1886                                nn(&cv),
1887                                md(&cv, &dv),
1888                                nn(&ao),
1889                                md(&ao, &dao)
1890                            );
1891                        } else {
1892                            eprintln!("attn-oracle L{li}: device probe declined");
1893                        }
1894                    }
1895                    graph.encode_attn_suffix(l, &ao);
1896                    // Early commit: the GPU starts O+FFN while the CPU
1897                    // encodes the following GDN run / attention prefix.
1898                    graph.commit();
1899                    attention::recycle_buf(&mut ao);
1900                }
1901            }
1902
1903            crate::gpu::stageprof(_xkind, _xt0.elapsed());
1904        }
1905        // Ride the final norm + lm_head in the same command buffer when
1906        // this run reaches the model's end and the caller wants logits:
1907        // the separate per-op lm_head submit (a full round trip) folds
1908        // into the sync that already happens here.
1909        let mut lm_rows = None;
1910        if self.graph_want_logits
1911            && upto.is_none()
1912            && end == self.num_layers
1913            && std::env::var("CMF_GPU_LMHEAD")
1914                .map(|v| v != "0")
1915                .unwrap_or(true)
1916        {
1917            if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
1918                if graph.lm_head_ok(lm) {
1919                    graph.encode_lm_head(&self.weights.final_norm, lm);
1920                    lm_rows = Some(lm.1);
1921                }
1922            }
1923        }
1924        if self.graph_head_required && lm_rows.is_none() {
1925            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1926            self.fail_metal_graph("fused graph head was requested but not encodable");
1927            return start;
1928        }
1929        let _sy0 = std::time::Instant::now();
1930        if let Err(err) = graph.sync_checked() {
1931            self.fail_metal_graph(&err);
1932            return start;
1933        }
1934        let _rs0 = std::time::Instant::now();
1935        if !pending.is_empty() {
1936            let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1937            let mut outs: Vec<&mut [f32]> = self
1938                .kv_cache
1939                .layers
1940                .iter_mut()
1941                .enumerate()
1942                .filter(|(i, _)| idxs.binary_search(i).is_ok())
1943                .map(|(_, s)| s.linear_state.as_mut_slice())
1944                .collect();
1945            graph.read_states(&mut outs);
1946        }
1947        if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1948            use std::sync::atomic::{AtomicU64, Ordering};
1949            static SY: AtomicU64 = AtomicU64::new(0);
1950            static RS: AtomicU64 = AtomicU64::new(0);
1951            static N: AtomicU64 = AtomicU64::new(0);
1952            SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1953            RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1954            let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1955            if n % 100 == 0 {
1956                eprintln!(
1957                    "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1958                    SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1959                    RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1960                );
1961            }
1962        }
1963        if let Some(rows) = lm_rows {
1964            crate::gpu::hostprof_encode_done(_mt0);
1965            let mut lg = attention::take_buf(rows.min(self.vocab_size));
1966            graph.read_logits(&mut lg);
1967            crate::gpu::hostprof_total(_mt0);
1968            lg.resize(self.vocab_size, 0.0);
1969            if let Some(c) = self.final_softcap {
1970                for l in lg.iter_mut() {
1971                    *l = c * (*l / c).tanh();
1972                }
1973            }
1974            self.graph_logits = Some(lg);
1975        }
1976        graph.read_h(h);
1977        if self.graph_head_required && self.graph_logits.is_none() {
1978            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1979            self.fail_metal_graph("fused graph head completed without logits readback");
1980            return start;
1981        }
1982        METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1983        METAL_GRAPH_LAYERS.fetch_add(
1984            end.saturating_sub(start) as u64,
1985            std::sync::atomic::Ordering::Relaxed,
1986        );
1987        if self.graph_head_required {
1988            METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1989        }
1990        // Device-attended layers: replay the CPU bookkeeping — append
1991        // the mirror's new K/V row (rope'd on the GPU) into the owner
1992        // cache, then bank this token's attention-importance mass.
1993        for li in dev_attn {
1994            let mut krow = attention::take_buf(nkv * hd);
1995            let mut vrow = attention::take_buf(nkv * hd);
1996            if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1997                let cache = &mut self.kv_cache.layers[li];
1998                cache.append(&krow, &vrow, &[]);
1999                let n = cache.seq_len;
2000                let mut imp = attention::take_buf(n);
2001                crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
2002                cache.accumulate_imp(&imp);
2003                attention::recycle_buf(&mut imp);
2004            }
2005            attention::recycle_buf(&mut krow);
2006            attention::recycle_buf(&mut vrow);
2007        }
2008        end
2009    }
2010
2011    pub fn new(
2012        tokenizer: Tokenizer,
2013        weights: PipelineWeights,
2014        hidden_size: usize,
2015        intermediate_size: usize,
2016        num_heads: usize,
2017        num_kv_heads: usize,
2018        head_dim: usize,
2019        num_layers: usize,
2020        physical_layers: usize,
2021        loop_final_norm: bool,
2022        vocab_size: usize,
2023        rms_eps: f64,
2024        rope_base: f32,
2025        norm_style: NormStyle,
2026        max_seq_len: usize,
2027        sampler_config: SamplerConfig,
2028    ) -> Self {
2029        let rng = match sampler_config.seed {
2030            Some(s) => SplitMix64::new(s),
2031            None => SplitMix64::from_entropy(),
2032        };
2033        let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
2034        let pool = Pool::from_env();
2035        if let Some(p) = &pool {
2036            tracing::info!("worker pool: {} threads", p.n_workers());
2037        }
2038        Self {
2039            gpu_plan: None,
2040            tokenizer: std::sync::Arc::new(tokenizer),
2041            kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
2042            sampler_config,
2043            weights,
2044            hidden_size,
2045            intermediate_size,
2046            num_heads,
2047            num_kv_heads,
2048            head_dim,
2049            num_layers,
2050            physical_layers,
2051            loop_final_norm,
2052            vocab_size,
2053            rms_eps,
2054            rope_base,
2055            norm_style,
2056            rotary_dim: head_dim,
2057            attention_heads_per_layer: None,
2058            vmf_cfg: None,
2059            gdn_cfg: None,
2060            kda_cfg: None,
2061            g3n: None,
2062            dsv4: None,
2063            dsv41: None,
2064            dsv41_vision: None,
2065            dsv41_prefill: None,
2066            qwen4_exp: None,
2067            dsv4_mtp: Vec::new(),
2068            dspark: None,
2069            dspark_pending: Vec::new(),
2070            dspark_hist: Vec::new(),
2071            dspark_real: Vec::new(),
2072            dspark_trunk_picks: Vec::new(),
2073            dspark_exp: Vec::new(),
2074            dspark_draft_ns: 0,
2075            logit_multiplier: None,
2076            cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2077            graph_failed: std::sync::atomic::AtomicBool::new(false),
2078            kv_history: Vec::new(),
2079            short_conv_cfg: None,
2080            mtp: None,
2081            speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
2082            rng,
2083            sampler_scratch: SamplerScratch::default(),
2084            spec_forced: None,
2085            spec_q: Vec::new(),
2086            spec_p: Vec::new(),
2087            spec_res: Vec::new(),
2088            spec_qs: Vec::new(),
2089            spec_ps: Vec::new(),
2090            spec_ress: Vec::new(),
2091            mtp_graph_mode: None,
2092            #[cfg(target_os = "macos")]
2093            metal_verify: None,
2094            inv_freq,
2095            ws: ForwardScratch::new(hidden_size),
2096            pool,
2097            model: None,
2098            dyn_force_f32: false,
2099            dyn_skill_layers: Vec::new(),
2100            dyn_active: None,
2101            dyn_blend_loaded: false,
2102            dyn_phi_layer: None,
2103            dyn_phi_ema: Vec::new(),
2104            dyn_phi_seen: 0,
2105            dyn_router: None,
2106            o1_cfg: None,
2107            o1_epoch: 0,
2108            o1_flags: Vec::new(),
2109            trace: false,
2110            calib_temp: 1.0,
2111            confidence_on: true,
2112            embed_multiplier: 1.0,
2113            attn_scale: 1.0 / (head_dim as f32).sqrt(),
2114            swa: None,
2115            sliding_layers: None,
2116            inv_freq_local: None,
2117            rotary_dim_local: None,
2118            rope_scale: 1.0,
2119            rope_scale_local: 1.0,
2120            global_attn: None,
2121            inv_freq_global: None,
2122            attn_v_norm: false,
2123            qk_norm_after_rope: false,
2124            final_softcap: None,
2125            head_clusters: None,
2126            attn_softcap: 0.0,
2127            graph_want_logits: false,
2128            graph_head_required: false,
2129            graph_logits: None,
2130            graph_kv_id: {
2131                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2132                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2133            },
2134            #[cfg(test)]
2135            nll_test_fail_at: None,
2136            #[cfg(test)]
2137            nll_test_force_serial: false,
2138        }
2139    }
2140
2141    /// Enable/disable per-layer O(1) Nyström attention. Only Full
2142    /// layers are eligible (a linear layer keeps its own operator).
2143    /// Applies to generation (`generate*`/`forward_ids`): the prompt
2144    /// pass stays exact, then the state seals after prefill or at the
2145    /// deferred skeleton-safe boundary for short prompts; decode runs on
2146    /// the O(1) state. Teacher-forced scoring (`ppl_ids`) intentionally
2147    /// stays exact.
2148    pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
2149        if let Some(c) = &cfg {
2150            if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
2151                tracing::error!(
2152                    "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
2153                    c.w,
2154                    c.sink
2155                );
2156                self.o1_flags.clear();
2157                self.o1_cfg = None;
2158                return;
2159            }
2160        }
2161        self.o1_flags = match &cfg {
2162            Some(c) => {
2163                let mut flags = c.layer_flags(self.num_layers);
2164                for (li, f) in flags.iter_mut().enumerate() {
2165                    if *f
2166                        && !matches!(
2167                            self.weights.layers[self.phys_layer(li)].attn,
2168                            AttnKind::Full { .. }
2169                        )
2170                    {
2171                        *f = false;
2172                    }
2173                }
2174                flags
2175            }
2176            None => Vec::new(),
2177        };
2178        if let Some(c) = &cfg {
2179            let n = self.o1_flags.iter().filter(|&&f| f).count();
2180            tracing::info!(
2181                "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2182                self.num_layers,
2183                c.m,
2184                c.w,
2185                c.sink,
2186                c.rect
2187            );
2188        }
2189        self.o1_cfg = cfg;
2190    }
2191
2192    /// True when at least one layer runs the O(1) kernel.
2193    pub fn o1_active(&self) -> bool {
2194        self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2195    }
2196
2197    /// Whether generation's prompt ingest is routed through the whole-token
2198    /// graph.  The bench uses this to label the measured generation prefill
2199    /// honestly; keep the predicate in Pipeline so CLI labels cannot drift
2200    /// from the production route.
2201    pub fn generation_graph_prefill(&self) -> bool {
2202        let graph = self.graph_prefill_preferred();
2203        // On wgpu, an active MTP head now consumes the trunk's graph batches
2204        // and warms its own block from those returned rows.  The selected
2205        // generation measurement is therefore the batched path, even though
2206        // the underlying GDN model still satisfies the graph-prefill
2207        // predicate.  Keep the CLI label tied to the actual route.  Native
2208        // Metal has a separate prefill-batch arm and retains its historical
2209        // label here.
2210        #[cfg(not(target_os = "macos"))]
2211        if graph
2212            && self.mtp.is_some()
2213            && std::env::var("CMF_BATCH_K")
2214                .ok()
2215                .and_then(|v| v.parse::<usize>().ok())
2216                .is_some_and(|k| k > 0)
2217            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2218        {
2219            return false;
2220        }
2221        graph
2222    }
2223
2224    /// Device-side O(1) mirrors currently uploaded for this pipeline's
2225    /// sequence.  The count/bytes are zero before seal or after a fresh
2226    /// reset; callers use this to distinguish logical host state from the
2227    /// GPU allocation that actually serves decode.
2228    pub fn o1_device_stats(&self) -> (usize, u64) {
2229        crate::gpu::o1_device_stats(self.graph_kv_id)
2230    }
2231
2232    /// Arm query collection on the o1 layers (fresh prompt pass).
2233    /// Reset the o1 layers to Collecting for a fresh sequence. Pub for the
2234    /// network split: each side runs the o1 lifecycle over ITS OWN layers
2235    /// (begin before prefill, seal at the prefill barrier).
2236    pub fn o1_begin(&mut self) {
2237        self.o1_begin_with_prefix(None);
2238    }
2239
2240    /// Arm collection and optionally request a positive calibration prefix.
2241    /// The effective barrier is always at least the skeleton-safe floor, so
2242    /// a short requested prefix cannot create an exact-only runtime state.
2243    pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2244        if let Some(c) = &self.o1_cfg {
2245            let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2246            let boundary = requested_prefix.map(|p| {
2247                p.max(
2248                    crate::nystrom::o1_deferred_boundary(w, sink)
2249                        .expect("o1 config boundary validated in set_o1"),
2250                )
2251            });
2252            for (li, &f) in self.o1_flags.iter().enumerate() {
2253                if f {
2254                    self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2255                }
2256            }
2257        }
2258    }
2259
2260    /// Effective deferred boundary for a positive prefix request.
2261    fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2262        self.o1_cfg.as_ref().and_then(|c| {
2263            crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2264                .map(|floor| requested_prefix.max(floor))
2265        })
2266    }
2267
2268    fn o1_note_transition(&mut self) {
2269        // Drain every layer's one-shot bit before publishing one pipeline
2270        // epoch. `any()` would short-circuit on the first layer and leak the
2271        // remaining bits into later forwards, causing one epoch per layer.
2272        let mut transitioned = false;
2273        for (li, &flagged) in self.o1_flags.iter().enumerate() {
2274            if flagged {
2275                transitioned |= self.kv_cache.layers[li].take_o1_transition();
2276            }
2277        }
2278        if transitioned {
2279            self.o1_epoch = self.o1_epoch.wrapping_add(1);
2280        }
2281    }
2282
2283    fn o1_pending(&self) -> bool {
2284        self.o1_flags.iter().enumerate().any(|(li, &f)| {
2285            f && self.kv_cache.layers[li].seq_len > 0
2286                && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2287        })
2288    }
2289
2290    fn o1_fail(&mut self, err: String) {
2291        tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2292        self.clear_sequence_state();
2293        self.graph_failed
2294            .store(true, std::sync::atomic::Ordering::Relaxed);
2295        self.cancel
2296            .store(true, std::sync::atomic::Ordering::Relaxed);
2297    }
2298
2299    /// Seal participating layers while retaining the exact state when the
2300    /// prompt is below the deferred boundary. A split worker may have
2301    /// collecting layers outside its owned span; zero-depth layers remain
2302    /// armed and are intentionally skipped until their peer runs them.
2303    pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2304        if self.o1_cfg.is_none() {
2305            return Ok(false);
2306        }
2307        let mut participating = false;
2308        for li in 0..self.num_layers {
2309            if !self.o1_flags.get(li).copied().unwrap_or(false) {
2310                continue;
2311            }
2312            if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2313                return Err(err);
2314            }
2315            if self.kv_cache.layers[li].seq_len == 0 {
2316                continue;
2317            }
2318            participating = true;
2319            let num_heads = self.layer_num_heads(li);
2320            self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2321        }
2322        self.o1_note_transition();
2323        for li in 0..self.num_layers {
2324            if self.o1_flags.get(li).copied().unwrap_or(false) {
2325                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2326                    return Err(err);
2327                }
2328            }
2329        }
2330        Ok(participating
2331            && (0..self.num_layers).all(|li| {
2332                !self.o1_flags.get(li).copied().unwrap_or(false)
2333                    || self.kv_cache.layers[li].seq_len == 0
2334                    || self.kv_cache.layers[li].o1_sealed()
2335            }))
2336    }
2337
2338    /// Complete a deferred boundary after a full position/span forward.
2339    /// This is the pipeline owner for epoch publication and failure cleanup.
2340    fn o1_progress(&mut self) {
2341        if !self.o1_active() {
2342            return;
2343        }
2344        for li in 0..self.num_layers {
2345            if self.o1_flags.get(li).copied().unwrap_or(false) {
2346                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2347                    self.o1_fail(err);
2348                    return;
2349                }
2350            }
2351        }
2352        // A qwen_attention row can seal in the middle of a complete layer
2353        // walk. Consume its transition even though the pending boundary has
2354        // already disappeared from the cache.
2355        self.o1_note_transition();
2356        if !self.o1_pending() {
2357            return;
2358        }
2359        if let Err(err) = self.o1_seal_checked() {
2360            self.o1_fail(err);
2361        }
2362    }
2363
2364    /// Turn a deferred O(1) failure raised by a hidden-only forward into the
2365    /// Result error its public batch/span caller must return. The failure
2366    /// path already cleared host/device sequence state; consume only the
2367    /// side-channel marker here and leave the pipeline reusable.
2368    fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
2369        if self
2370            .graph_failed
2371            .swap(false, std::sync::atomic::Ordering::Relaxed)
2372        {
2373            self.cancel
2374                .store(false, std::sync::atomic::Ordering::Relaxed);
2375            self.clear_sequence_state();
2376            return Err(format!("{phase}: deferred O(1) transition failed"));
2377        }
2378        Ok(())
2379    }
2380
2381    /// Freeze landmarks + skeleton state after the prompt pass and drop
2382    /// the o1 layers' full KV; decode then runs `step()` per token.
2383    /// Pub for the network split (see `o1_begin`).
2384    pub fn o1_seal(&mut self) {
2385        if let Err(err) = self.o1_seal_checked() {
2386            self.o1_fail(err);
2387        }
2388    }
2389
2390    /// Enable/disable the structured per-token telemetry trace (B4).
2391    pub fn set_trace(&mut self, on: bool) {
2392        self.trace = on;
2393    }
2394
2395    /// Replace all request-scoped sampler options and reset the random stream.
2396    /// This is required for deterministic `seed` semantics in pooled servers.
2397    pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2398        self.rng = match config.seed {
2399            Some(seed) => SplitMix64::new(seed),
2400            None => SplitMix64::from_entropy(),
2401        };
2402        self.sampler_config = config;
2403    }
2404
2405    /// Toggle the per-token confidence reduction (a full-vocab
2406    /// softmax each token). `bench --core` turns it off so the timed
2407    /// loop matches llama-bench's core contract; the result's
2408    /// `confidence` vec is empty while off.
2409    pub fn set_confidence(&mut self, on: bool) {
2410        self.confidence_on = on;
2411    }
2412
2413    /// Set the confidence-calibration temperature (B1). Values ≤0 are
2414    /// clamped to raw (1.0).
2415    pub fn set_calib_temp(&mut self, t: f32) {
2416        self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2417    }
2418
2419    /// The active calibration temperature (1.0 = raw probability).
2420    pub fn calib_temp(&self) -> f32 {
2421        self.calib_temp
2422    }
2423
2424    /// Partial rotary (Qwen3.5): rotate only the first `rotary_dim` dims;
2425    /// the frequency table is rebuilt over the rotary dims.
2426    pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2427        self.rotary_dim = rotary_dim.min(self.head_dim);
2428        self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2429    }
2430
2431    fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2432        QwenAttnCfg {
2433            num_heads: self.num_heads,
2434            num_kv_heads: self.num_kv_heads,
2435            head_dim: self.head_dim,
2436            hidden_size: self.hidden_size,
2437            position,
2438            inv_freq: &self.inv_freq,
2439            rotary_dim: self.rotary_dim,
2440            scale: self.attn_scale,
2441            softcap: self.attn_softcap,
2442            window: None,
2443            v_norm: false,
2444            qk_norm_after_rope: self.qk_norm_after_rope,
2445            q_norm: None,
2446            k_norm: None,
2447            output_gate: false,
2448            softplus_gate: None,
2449            rope_scale: self.rope_scale,
2450            bias: None,
2451            rms_eps: self.rms_eps,
2452            norm_style: self.norm_style,
2453            pool: self.pool.as_deref(),
2454        }
2455    }
2456
2457    /// Generate text from a plain-text prompt. Streams tokens via `on_token`.
2458    pub fn generate(
2459        &mut self,
2460        prompt: &str,
2461        max_tokens: usize,
2462        task_mask: Option<&TaskMask>,
2463        on_token: Option<TokenCallback>,
2464    ) -> Result<GenerateResult, String> {
2465        let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2466        self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2467    }
2468
2469    /// Generate from a V4.1 multimodal prompt prepared by the vision module.
2470    /// Vision rows are encoded once and fed through the same bounded token walk as text.
2471    pub fn generate_from_vl(
2472        &mut self,
2473        input: &crate::dsv41_vision::PreparedVlInputs,
2474        max_tokens: usize,
2475        task_mask: Option<&TaskMask>,
2476        on_token: Option<TokenCallback>,
2477    ) -> Result<GenerateResult, String> {
2478        let Some(dsv41) = &self.dsv41 else {
2479            return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
2480        };
2481        if input.token_ids.is_empty() {
2482            return Err("empty V4.1 multimodal prompt".into());
2483        }
2484        if input.token_types.len() != input.token_ids.len() {
2485            return Err(format!(
2486                "V4.1 token type count {} != token count {}",
2487                input.token_types.len(),
2488                input.token_ids.len()
2489            ));
2490        }
2491        let dim = dsv41.2.dim;
2492        let mut embeddings = vec![None; input.token_ids.len()];
2493        let mut participates = vec![true; input.token_ids.len()];
2494        if !input.images.is_empty() {
2495            let vision = self
2496                .dsv41_vision
2497                .as_ref()
2498                .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
2499            for image in &input.images {
2500                let end = image.start.saturating_add(image.types.len());
2501                if end > input.token_ids.len() {
2502                    return Err(format!(
2503                        "V4.1 image span {}..{} exceeds prompt length {}",
2504                        image.start,
2505                        end,
2506                        input.token_ids.len()
2507                    ));
2508                }
2509                let mut span = vec![0.0f32; image.types.len() * dim];
2510                vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
2511                for (offset, &kind) in image.types.iter().enumerate() {
2512                    let pos = image.start + offset;
2513                    if input.token_types[pos] != kind {
2514                        return Err(format!(
2515                            "V4.1 image type mismatch at position {pos}: {} != {kind}",
2516                            input.token_types[pos]
2517                        ));
2518                    }
2519                    embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
2520                    participates[pos] = false;
2521                }
2522            }
2523        }
2524        for (pos, &kind) in input.token_types.iter().enumerate() {
2525            if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
2526                return Err(format!("V4.1 text position {pos} has an image embedding"));
2527            }
2528            if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
2529                return Err(format!("V4.1 image position {pos} has no image embedding"));
2530            }
2531        }
2532        self.dsv41_prefill = Some((embeddings, participates));
2533        let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
2534        self.dsv41_prefill = None;
2535        result
2536    }
2537
2538    /// `None` when the mask forbids nothing (see `TaskMask::fully_open`).
2539    fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
2540        m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
2541    }
2542
2543    /// Generate from prepared token ids (e.g. a chat template).
2544    ///
2545    /// With an MTP head, greedy generation without a task mask takes the
2546    /// speculative path: the MTP module drafts the token after next and
2547    /// the main model verifies both in one fused two-position forward
2548    /// (weights streamed once). The output is EXACTLY the vanilla greedy
2549    /// sequence — a rejected draft is rolled back — MTP only buys speed.
2550    pub fn generate_from_ids(
2551        &mut self,
2552        input_ids: &[u32],
2553        max_tokens: usize,
2554        task_mask: Option<&TaskMask>,
2555        mut on_token: Option<TokenCallback>,
2556    ) -> Result<GenerateResult, String> {
2557        if std::env::var("CMF_TRACE_H").is_ok() {
2558            eprintln!("input_ids: {input_ids:?}");
2559        }
2560        if input_ids.is_empty() {
2561            return Err("empty prompt: nothing to generate from".to_string());
2562        }
2563        // A prior graph failure is terminal for that sequence but must not
2564        // poison the next independent request.  Keep this flag separate from
2565        // the externally-owned cooperative cancel bit.
2566        self.graph_failed
2567            .store(false, std::sync::atomic::Ordering::Relaxed);
2568        // A mask that forbids nothing still costs every fused path and
2569        // whole-token graph, all of which are gated on `is_none()`. A
2570        // narrowed file whose one segment is always on carries exactly
2571        // such a mask — drop it here rather than pay 5x for a no-op.
2572        let task_mask = self.drop_open_mask(task_mask);
2573
2574        // Cross-turn KV reuse: a chat app resends the whole history
2575        // every turn; when the new ids strictly EXTEND what the cache
2576        // already holds, prefill only the tail — turn latency stays
2577        // proportional to the new text instead of the whole session.
2578        // Extension-only (no rollback), so it is exact for every layer
2579        // kind including recurrent state; MTP/o1/task-mask runs keep
2580        // the fresh-sequence path. CMF_KV_REUSE=0 disables.
2581        let reuse_from = {
2582            let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
2583            let h = &self.kv_history;
2584            if on
2585                && task_mask.is_none()
2586                && self.mtp.is_none()
2587                && self.o1_cfg.is_none()
2588                && self.dsv41.is_none()
2589                && !h.is_empty()
2590                && h.len() < input_ids.len()
2591                && input_ids[..h.len()] == h[..]
2592            {
2593                h.len()
2594            } else {
2595                0
2596            }
2597        };
2598        if reuse_from == 0 {
2599            // Fresh sequence — the cache holds absolute positions.
2600            self.clear_sequence_state();
2601        } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
2602            eprintln!(
2603                "kv-reuse: {} of {} prompt positions already cached",
2604                reuse_from,
2605                input_ids.len()
2606            );
2607        }
2608        crate::gpu::graph_race_begin_generation();
2609        // Optional bounded calibration prefix. Keep the requested value
2610        // even when it is longer than the prompt; the collecting layer will
2611        // defer at the effective boundary and remain exact for short input.
2612        let o1_prefill = if self.o1_active() && task_mask.is_none() {
2613            std::env::var("CMF_O1_PREFILL")
2614                .ok()
2615                .and_then(|v| v.parse::<usize>().ok())
2616                .filter(|&p| p > 0)
2617        } else {
2618            None
2619        };
2620        if task_mask.is_none() {
2621            self.o1_begin_with_prefix(o1_prefill);
2622        }
2623
2624        // Speculative decode is off under o1: a rejected draft can't be
2625        // rolled back out of the far accumulators / ring window (the
2626        // Nyström insertion is irreversible by design).
2627        // The wgpu token graph owns a device K/V mirror that speculative
2628        // rollback would desync — the two are mutually exclusive.
2629        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2630        // Graph speculative decode (`CMF_GRAPH_SPEC=1`): the MTP head
2631        // drafts, ONE batched graph submit verifies the whole chain.
2632        //
2633        // It now PAYS on Qwen3.6-27B / RTX 5090 — 51.1 tok/s against a
2634        // plain 49.4 at k=3, medians of three, 89% of drafts accepted,
2635        // and the greedy continuation is byte-identical to the plain
2636        // path. That took the batch matvec sharing its nibble unpack
2637        // across the batch (`CMF_MV_BK=2`); before it, the same round
2638        // measured 43.6, an 11% LOSS, which is what the earlier note
2639        // here described.
2640        //
2641        // Still opt-in. One model's win is not a default: the verify
2642        // rides `gdn_spec_restore` and a batched frame whose numerics
2643        // are the batch kernels', and that has to be shown on more than
2644        // one architecture before every greedy decode takes it.
2645        // Greedy (with or without penalties) verifies by argmax equality.
2646        // Sampling (temperature > 0) can go through speculative SAMPLING —
2647        // draft from the MTP head's own post-chain distribution, accept
2648        // with min(1, p/q), correct from max(0, p − q); the emitted stream
2649        // is distributed exactly as the plain sampler's — but it is
2650        // OPT-IN (`CMF_GRAPH_SPEC_SAMPLE=1`): measured on Qwen3.8-27B /
2651        // RTX 5090 at the instruct row (0.7 / 0.80 / 20 / presence 1.5)
2652        // it decoded 19-22 tok/s against a plain 40 — nine post-chain
2653        // distributions a round plus a lower acceptance than greedy's,
2654        // against a verify that costs 2.7 single tokens. The greedy arms
2655        // pay +10%; the sampling arm needs a cheaper verify first.
2656        let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2657            || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2658        // ON by default for greedy on the wgpu graph: with the draft on
2659        // the graph and the verify bit-exact, it measured 58.7 tok/s
2660        // against a plain 48.1 on Qwen3.8-27B q4tp / RTX 5090 (k=4) and
2661        // 51.1 against 49.4 on Qwen3.6-27B, and a round that stops
2662        // paying turns itself off below (acceptance watchdog).
2663        // `CMF_GRAPH_SPEC=0` disables; `=1` was the old opt-in spelling.
2664        // …but only where the batched verify has its register-blocked
2665        // kernel: q4tp dense FFNs (graph kind 6). q4t and q8_2f verify
2666        // through tile GEMMs today and measured a LOSS (q8_2f 22 against
2667        // 29 tok/s), the 2-bit plane the same; those stay opt-in
2668        // (`CMF_GRAPH_SPEC=1`).
2669        // …at least in nine dense FFNs of ten: a healed file carries its
2670        // last two layers at q8_2f, and two tile-GEMM verifies among 64 do
2671        // not change the arithmetic (measured: the healed q4tp file
2672        // decodes at the plain file's rate and would otherwise sit out).
2673        let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2674        for lw in &self.weights.layers {
2675            if let FfnKind::Dense(d) = &lw.ffn {
2676                dense_n += 1;
2677                if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2678                    && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2679                    && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2680                {
2681                    dense_q4tp += 1;
2682                }
2683            }
2684        }
2685        let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2686        // Penalties break the draft head's agreement with the trunk (a
2687        // 1.1 repetition penalty measured 2 of 16 accepted): not by
2688        // default there either.
2689        let penalized = self.sampler_config.repetition_penalty != 1.0
2690            || self.sampler_config.presence_penalty != 0.0
2691            || !self.sampler_config.suppress_tokens.is_empty();
2692        // …and not on wgpu-over-Metal: the batched verify graph there
2693        // returned 0 accepted drafts and garbage text on a GDN hybrid
2694        // (16.08, Qwen3.5-0.8B) while Vulkan is bit-exact; the Mac's
2695        // default backend is native Metal without a batch graph anyway.
2696        #[cfg(feature = "gpu")]
2697        let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2698        #[cfg(not(feature = "gpu"))]
2699        let metal_wgpu = false;
2700        let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2701        let spec_wanted = match spec_env.as_deref() {
2702            Some("0") => false,
2703            Some(_) => {
2704                if metal_wgpu {
2705                    tracing::warn!(
2706                        "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2707                         verified on this backend (garbage measured on Qwen3.5-0.8B)"
2708                    );
2709                }
2710                true
2711            }
2712            None => spec_default_ok && !penalized && !metal_wgpu,
2713        };
2714        // Native Metal: the b-row verify graph (`try_batch_graph_metal`)
2715        // stands where the wgpu batch graph stands on discrete cards.
2716        #[cfg(target_os = "macos")]
2717        let metal_graph = crate::gpu::q1_force()
2718            && crate::gpu::enabled_here()
2719            && std::env::var("CMF_GPU_BLOCK")
2720                .map(|v| v != "0")
2721                .unwrap_or(true);
2722        #[cfg(not(target_os = "macos"))]
2723        let metal_graph = false;
2724        let graph_spec = self.speculative
2725            && (graph_on || metal_graph)
2726            && self.mtp.is_some()
2727            && task_mask.is_none()
2728            && !self.o1_active()
2729            && spec_sampling_ok
2730            && spec_wanted;
2731        // GDN hybrids sit the fused-pair speculation out by default: the
2732        // recurrence is sequential, so the pair lane cannot parallelize
2733        // (the bench's own Pair line reads fused 1.28x TWO singles on the
2734        // 35B) and the draft's full-vocab head rides on top — measured 2x
2735        // SLOWER end to end (16.1 vs 32.4 tok/s on the 48-core stand).
2736        // CMF_MTP=1 forces it back for study.
2737        let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2738        let spec_active = self.speculative
2739            && self.mtp.is_some()
2740            && task_mask.is_none()
2741            && !self.o1_active()
2742            && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2743        // The MTP module is detached during generation so its mutable
2744        // state does not fight the borrow on `self`.
2745        let mut mtp = if spec_active { self.mtp.take() } else { None };
2746        if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2747            eprintln!(
2748                "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2749                mtp.is_some(),
2750                self.speculative,
2751                self.sampler_config.temperature < 1e-6,
2752            );
2753        }
2754        if let Some(m) = &mut mtp {
2755            m.kv.clear();
2756            // The MTP block's own device mirror starts over with its cache.
2757            crate::gpu::graph_kv_reset(self.mtp_kv_id());
2758            self.mtp_graph_mode = None;
2759        }
2760        // Dynamic router detached during decode (same borrow trick as MTP).
2761        // Speculative decode and dynamic routing are mutually exclusive
2762        // for now — the fused-pair path doesn't carry per-token φ.
2763        let mut router = if mtp.is_none() {
2764            self.dyn_router.take()
2765        } else {
2766            None
2767        };
2768        if let Some(r) = &mut router {
2769            r.reset(); // active=backbone, matching a fresh overlay
2770            self.dyn_phi_seen = 0; // fresh φ EMA per generation
2771            let _ = self.set_active_skill(None);
2772        }
2773
2774        let mut all_ids = input_ids.to_vec();
2775        let mut generated = 0usize;
2776        let mut finish_reason = "max_tokens".to_string();
2777        let mut drafted = 0usize;
2778        let mut accepted = 0usize;
2779        // DeepSeek-V4's draft quality is strongly content-dependent.  Two
2780        // consecutive paid rounds with no extra token put it on a bounded
2781        // cooldown; predictable text keeps batching, ordinary prose falls
2782        // back to the exact walk instead of paying a slow draft forever.
2783        // Local to one generation so one difficult request cannot poison the
2784        // next one, and deliberately automatic — this is not a user knob.
2785        let mut dsv4_spec_bad = 0usize;
2786        let mut dsv4_spec_retry_at = 0usize;
2787        let mut confidence: Vec<f32> = Vec::new();
2788        let trace_on = self.trace;
2789        let calib_temp = self.calib_temp;
2790        let mut traces: Vec<TokenTrace> = Vec::new();
2791
2792        // ── Prefill: forward each prompt token once, KEEP the last hidden.
2793        //    Dense prefill runs in fused pairs (weights streamed once per
2794        //    two positions — bit-identical to sequential, proven by the
2795        //    pair tests). With MTP: warm the draft head on
2796        //    (hidden_p, token_{p+1}) pairs.
2797        let mut hidden = vec![0.0f32; self.hidden_size];
2798        let mut pos = reuse_from;
2799        // lm_head-in-graph is only sound when the very next logits
2800        // consumer is this loop's own (MTP and skill routing interleave
2801        // other forwards / can swap lm_head between forward and sample).
2802        // CMF_GPU_LMHEAD=0 keeps lm_head off the graph: the token reads back
2803        // the 8 KB hidden instead of ~1 MB of logits, and the head runs on
2804        // the host. A probe for how much of the graph's fixed per-token cost
2805        // is the logits readback (the layer sweep puts that fixed part at
2806        // 3.88 ms of an 18.5 ms frame).
2807        let fuse_lm = mtp.is_none()
2808            && router.is_none()
2809            && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2810        self.graph_logits = None;
2811        self.graph_want_logits = false;
2812        let _tpf = std::time::Instant::now();
2813        let batch_k = std::env::var("CMF_BATCH_K")
2814            .ok()
2815            .and_then(|v| v.parse::<usize>().ok())
2816            .unwrap_or(0);
2817        // DeepSeek-V4 owns a separate hyper-connection stack. Route it
2818        // before the generic prefill choices: those correctly reject an
2819        // empty `weights.layers`, but their final per-position fallback used
2820        // to consume the whole prompt before `dsv4::forward_chunk` could see
2821        // it. The batch implementation therefore existed without a live
2822        // production entry point.
2823        //
2824        // Bounded chunks preserve cancellation responsiveness. Only the
2825        // prompt's final chunk asks for logits; every earlier head projection
2826        // would produce 129 280 values that no caller reads.
2827        while self.qwen4_exp.is_some()
2828            && mtp.is_none()
2829            && pos < input_ids.len()
2830            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2831        {
2832            let token_id = input_ids[pos];
2833            let want_logits = pos + 1 == input_ids.len();
2834            let mut lg = Vec::new();
2835            if let Some(b) = &mut self.qwen4_exp {
2836                crate::qwen4_exp::forward_token(
2837                    &b.0,
2838                    &b.1,
2839                    &b.2,
2840                    &mut b.3,
2841                    token_id,
2842                    pos,
2843                    &self.inv_freq,
2844                    self.pool.as_deref(),
2845                    &mut lg,
2846                    want_logits,
2847                );
2848            }
2849            if want_logits {
2850                self.graph_logits = Some(lg);
2851            }
2852            pos += 1;
2853            hidden.fill(0.0);
2854        }
2855        while self.dsv4.is_some()
2856            && mtp.is_none()
2857            && pos < input_ids.len()
2858            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2859        {
2860            let end = (pos + prefill_chunk()).min(input_ids.len());
2861            let ids: Vec<u32> = input_ids[pos..end].to_vec();
2862            let mut lg = Vec::new();
2863            if let Some(b) = &mut self.dsv4 {
2864                let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2865                crate::dsv4::forward_chunk(
2866                    g,
2867                    layers,
2868                    &cfg,
2869                    st,
2870                    &ids,
2871                    pos,
2872                    &self.inv_freq,
2873                    self.pool.as_deref(),
2874                    &mut lg,
2875                    end == input_ids.len(),
2876                );
2877            }
2878            if end == input_ids.len() {
2879                self.graph_logits = Some(lg);
2880            }
2881            pos = end;
2882            hidden = vec![0.0; self.hidden_size];
2883        }
2884        let dsv41_prefill = self.dsv41_prefill.take();
2885        while self.dsv41.is_some()
2886            && mtp.is_none()
2887            && pos < input_ids.len()
2888            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2889        {
2890            let end = (pos + prefill_chunk()).min(input_ids.len());
2891            let ids: Vec<u32> = input_ids[pos..end].to_vec();
2892            let mut lg = Vec::new();
2893            if let Some(b) = &mut self.dsv41 {
2894                let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
2895                if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
2896                    crate::dsv41::forward_chunk_masked_with_embeddings(
2897                        g,
2898                        layers,
2899                        cfg,
2900                        st,
2901                        &ids,
2902                        pos,
2903                        &embeddings[pos..end],
2904                        &participates[pos..end],
2905                        self.pool.as_deref(),
2906                        &mut lg,
2907                    );
2908                } else {
2909                    crate::dsv41::forward_chunk(
2910                        g,
2911                        layers,
2912                        cfg,
2913                        st,
2914                        &ids,
2915                        pos,
2916                        self.pool.as_deref(),
2917                        &mut lg,
2918                    );
2919                }
2920            }
2921            if end == input_ids.len() {
2922                self.graph_logits = Some(lg);
2923            }
2924            pos = end;
2925            hidden = vec![0.0; self.hidden_size];
2926        }
2927        // With dynamic routing, prefill sequentially so the φ hook fires
2928        // over the PROMPT — the router enters decode with a warm φ (the
2929        // fused-pair path skips the per-layer φ capture). o1 layers
2930        // collect their query trace in both the single and pair paths.
2931        let dyn_prefill = router.is_some();
2932        // Optional bounded calibration prefix for generation.  The normal
2933        // O(1) path seals after the full prompt; this explicit knob instead
2934        // runs only the requested prefix through exact attention, seals the
2935        // Nyström state, and streams the rest of the prompt through the same
2936        // O(1) step used by decode.  It keeps the O(1) layers' Q trace and
2937        // temporary full KV bounded by the prefix while leaving the default
2938        // full-prompt quality profile untouched.
2939        let o1_prefill_limit = o1_prefill
2940            .and_then(|requested| self.o1_effective_boundary(requested))
2941            .map(|boundary| boundary.min(input_ids.len()));
2942        let mut o1_sealed = false;
2943        if let Some(limit) = o1_prefill_limit {
2944            // Reuse the exact batched prefix machinery when available; it
2945            // records the same per-position Q trace as the full prefill.
2946            if self.can_prefill_batched() && limit > 2 {
2947                let chunk = prefill_chunk();
2948                let hs = self.hidden_size;
2949                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2950                    let end = (pos + chunk).min(limit);
2951                    let hb = self.prefill_batch(&input_ids[pos..end], pos);
2952                    hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2953                    pos = end;
2954                }
2955            } else {
2956                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2957                    hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
2958                    pos += 1;
2959                }
2960            }
2961            if pos >= limit {
2962                o1_sealed = match self.o1_seal_checked() {
2963                    Ok(sealed) => sealed,
2964                    Err(err) => {
2965                        self.finish_generation(&mut mtp, &mut router, true);
2966                        return Err(err);
2967                    }
2968                };
2969                tracing::info!(
2970                    "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
2971                    o1_prefill.unwrap_or(0),
2972                    self.o1_effective_boundary(o1_prefill.unwrap_or(0))
2973                        .unwrap_or(limit),
2974                    limit,
2975                    input_ids.len()
2976                );
2977            }
2978        }
2979        // q1 hybrids on Metal: the per-position GPU token graph beats
2980        // the CPU chunk-GEMM (whose wall is the sequential scalar GDN
2981        // recurrence), so prefill goes position-by-position through the
2982        // same graph as decode. Pure-attention models keep the batched
2983        // path — there the chunk-GEMM amortization wins.
2984        let graph_prefill = self.graph_prefill_preferred();
2985        // Native Metal, q4tp GDN hybrids: the prompt through the b-row
2986        // rows graph — projections as GEMMs over up to 512 positions, the
2987        // GDN recurrence in registers on the device, K/V rows appended by
2988        // the chunk — instead of one token-graph submit per position (the
2989        // 27B: 8 tok/s → GEMM-bound). The MTP warm-up rows come out of one
2990        // batched run of the block per chunk. Any refusal leaves the rest
2991        // of the prompt to the sequential paths below.
2992        #[cfg(target_os = "macos")]
2993        if task_mask.is_none()
2994            && !dyn_prefill
2995            && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
2996            && crate::gpu::enabled_here()
2997            && self.gdn_cfg.is_some()
2998            && self.g3n.is_none()
2999            && input_ids.len() > 8
3000            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3001            && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
3002        {
3003            let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
3004                .ok()
3005                .and_then(|v| v.parse().ok())
3006                .filter(|&v| (16..=512).contains(&v))
3007                .unwrap_or(256);
3008            let hs = self.hidden_size;
3009            let _tp = std::time::Instant::now();
3010            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3011                let end = (pos + chunk).min(input_ids.len());
3012                let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
3013                    MetalPrefillOutcome::Completed(hb) => hb,
3014                    MetalPrefillOutcome::Declined => break,
3015                    MetalPrefillOutcome::Failed => {
3016                        self.finish_generation(&mut mtp, &mut router, true);
3017                        return Err("ordinary Metal prefill failed after admission".into());
3018                    }
3019                };
3020                if let Some(m) = &mut mtp {
3021                    let n_pairs = if end < input_ids.len() {
3022                        end - pos
3023                    } else {
3024                        end - pos - 1
3025                    };
3026                    if n_pairs > 0 {
3027                        let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
3028                            .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
3029                            .collect();
3030                        if !self.mtp_warm_batch_metal(m, &pairs, pos) {
3031                            for (j, (h, t)) in pairs.iter().enumerate() {
3032                                let h = h.to_vec();
3033                                let _ = self.mtp_step(m, &h, *t, pos + j);
3034                            }
3035                        }
3036                    }
3037                }
3038                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3039                pos = end;
3040            }
3041            if std::env::var("CMF_PREFILL_PROF").is_ok() {
3042                eprintln!(
3043                    "metal-prefill: {} of {} tokens in {:.1} ms",
3044                    pos,
3045                    input_ids.len(),
3046                    _tp.elapsed().as_secs_f64() * 1e3
3047                );
3048            }
3049        }
3050        if task_mask.is_none()
3051            && !dyn_prefill
3052            && !graph_prefill
3053            && self.can_prefill_batched()
3054            && self.g3n.is_none()
3055            && o1_prefill.is_none()
3056            && input_ids.len() > 2
3057        {
3058            // Production prefill = the same chunked prefill-GEMM that
3059            // bench/PPL measure (roadmap §3 P0: generation used to warm
3060            // the prompt with the slower pair path — the published
3061            // prefill number didn't match real TTFT). MTP warm-up reads
3062            // each position's hidden straight from the chunk result.
3063            let chunk = prefill_chunk();
3064            let hs = self.hidden_size;
3065            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3066                let end = (pos + chunk).min(input_ids.len());
3067                let hb = self.prefill_batch(&input_ids[pos..end], pos);
3068                if let Some(m) = &mut mtp {
3069                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3070                        .ok()
3071                        .and_then(|v| v.parse().ok())
3072                        .unwrap_or(0);
3073                    for p in pos..end {
3074                        if p + 1 < input_ids.len() {
3075                            if probe >= 1 && p + 2 < input_ids.len() {
3076                                // Teacher-forced chain acceptance (see the
3077                                // tail loop's twin): the warm-up row stays,
3078                                // the chain's rows roll back.
3079                                let (d1, mut hx) = self.mtp_step_h(
3080                                    m,
3081                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
3082                                    input_ids[p + 1],
3083                                    p,
3084                                );
3085                                let mut ok = d1 == input_ids[p + 2];
3086                                Self::chain_probe_note(0, ok);
3087                                let mut d_prev = d1;
3088                                let mut extra = 0usize;
3089                                for j in 1..probe {
3090                                    if p + 2 + j >= input_ids.len() {
3091                                        break;
3092                                    }
3093                                    let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
3094                                    extra += 1;
3095                                    ok = ok && dj == input_ids[p + 2 + j];
3096                                    Self::chain_probe_note(j, ok);
3097                                    d_prev = dj;
3098                                    hx = hj;
3099                                }
3100                                m.kv.truncate_last(extra);
3101                            } else {
3102                                let _ = self.mtp_step(
3103                                    m,
3104                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
3105                                    input_ids[p + 1],
3106                                    p,
3107                                );
3108                            }
3109                        }
3110                    }
3111                }
3112                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3113                pos = end;
3114            }
3115        }
3116        let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
3117        if task_mask.is_none()
3118            && !dyn_prefill
3119            && !graph_prefill
3120            && !pair_off
3121            && self.pair_supported()
3122            && o1_prefill.is_none()
3123        {
3124            while pos + 1 < input_ids.len()
3125                && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3126            {
3127                let e1 = self.embed_single(input_ids[pos]);
3128                let e2 = self.embed_single(input_ids[pos + 1]);
3129                let (h1, h2) = self.forward_pair(&e1, &e2, pos);
3130                // Both prefill tokens are real → commit lane-2 states.
3131                self.commit_linear_scratch();
3132                if let Some(m) = &mut mtp {
3133                    let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
3134                    if pos + 2 < input_ids.len() {
3135                        let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3136                            .ok()
3137                            .and_then(|v| v.parse().ok())
3138                            .unwrap_or(0);
3139                        if probe >= 1 && pos + 3 < input_ids.len() {
3140                            // Same teacher-forced chain table as the tail
3141                            // loop below, fed from the pair path that owns
3142                            // most prefill positions.
3143                            let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
3144                            let mut ok = d1 == input_ids[pos + 3];
3145                            Self::chain_probe_note(0, ok);
3146                            let mut d_prev = d1;
3147                            let mut extra = 0usize;
3148                            for j in 1..probe {
3149                                if pos + 3 + j >= input_ids.len() {
3150                                    break;
3151                                }
3152                                let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
3153                                extra += 1;
3154                                ok = ok && dj == input_ids[pos + 3 + j];
3155                                Self::chain_probe_note(j, ok);
3156                                d_prev = dj;
3157                                hx = hj;
3158                            }
3159                            m.kv.truncate_last(extra);
3160                        } else {
3161                            let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
3162                        }
3163                    }
3164                }
3165                hidden = h2;
3166                pos += 2;
3167            }
3168        }
3169        // Batched GPU prefill for the wgpu decode graph (GDN hybrids): K prompt
3170        // positions per submit — projections/FFN as GEMMs (weight once per K),
3171        // attention/GDN looped inside — instead of one whole-graph submit per
3172        // position. Falls through to the per-position graph on any refusal.
3173        // Batched prefill is opt-in (CMF_BATCH_K>0). Default 0 = per-position
3174        // graph prefill. (Steady-state decode is provably identical either way —
3175        // token-graph submit and lm_head both unchanged — so this only trades
3176        // prefill wall.)
3177        // A bounded O(1) prefix is the one post-seal prompt interval: only
3178        // admit its batch when the device O(1) route is explicitly enabled and
3179        // every sealed layer exposes a portable view. The same batch size and
3180        // refusal behavior remain the ordinary controls/comparator.
3181        let o1_batch_ready = o1_sealed
3182            && o1_prefill.is_some()
3183            && mtp.is_none()
3184            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
3185            && (0..self.num_layers).all(|li| {
3186                let cache = &self.kv_cache.layers[self.phys_layer(li)];
3187                cache.o1.is_none() || cache.o1_views().is_some()
3188            });
3189        // The ordinary graph-prefill route can share each completed trunk
3190        // chunk with an attached MTP head.  Keep chain probing on its
3191        // established per-position path: the probe deliberately needs every
3192        // teacher-forced draft row and its rollback table.
3193        let mtp_batch_prefill = mtp.is_some()
3194            && graph_prefill
3195            && task_mask.is_none()
3196            && !dyn_prefill
3197            && !self.o1_active()
3198            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
3199        if batch_k > 0
3200            && (graph_prefill || o1_batch_ready)
3201            && task_mask.is_none()
3202            && (!self.o1_active() || o1_batch_ready)
3203            && (mtp.is_none() || mtp_batch_prefill)
3204            && !dyn_prefill
3205            && pos + 1 < input_ids.len()
3206        {
3207            let hs = self.hidden_size;
3208            let chunk = batch_k;
3209            while pos < input_ids.len() {
3210                let end = (pos + chunk).min(input_ids.len());
3211                let bk = end - pos;
3212                let mut hiddens = vec![0f32; bk * hs];
3213                for (j, &id) in input_ids[pos..end].iter().enumerate() {
3214                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3215                }
3216                let positions: Vec<usize> = (pos..end).collect();
3217                let t_chunk = std::time::Instant::now();
3218                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3219                let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
3220                if std::env::var("CMF_GRAPH_PROF").is_ok() {
3221                    let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3222                    eprintln!(
3223                        "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3224                        if o1_batch_ready {
3225                            "o1"
3226                        } else if mtp_batch_prefill {
3227                            "ordinary_mtp"
3228                        } else {
3229                            "ordinary"
3230                        },
3231                        bk as f64 / (ms / 1000.0)
3232                    );
3233                }
3234                {
3235                    use std::sync::atomic::{AtomicBool, Ordering};
3236                    static SAID: AtomicBool = AtomicBool::new(false);
3237                    if !SAID.swap(true, Ordering::Relaxed) {
3238                        if ok_b {
3239                            tracing::info!(
3240                                "batched prefill: ACTIVE mode={} (k={bk})",
3241                                if o1_batch_ready {
3242                                    "o1"
3243                                } else if mtp_batch_prefill {
3244                                    "ordinary_mtp"
3245                                } else {
3246                                    "ordinary"
3247                                }
3248                            );
3249                        } else {
3250                            tracing::warn!("batched prefill {:?} — per-position graph", outcome);
3251                        }
3252                    }
3253                }
3254                if ok_b {
3255                    if mtp_batch_prefill {
3256                        let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
3257                        if n_pairs > 0 {
3258                            // `hiddens` is owned by this chunk, so materialize
3259                            // row slices before borrowing the detached MTP
3260                            // module.  The last prompt row has no successor;
3261                            // the helper above is the single source of that
3262                            // boundary rule.
3263                            let rows: Vec<Vec<f32>> = (0..n_pairs)
3264                                .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
3265                                .collect();
3266                            let pairs: Vec<(&[f32], u32)> = rows
3267                                .iter()
3268                                .enumerate()
3269                                .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
3270                                .collect();
3271                            if std::env::var("CMF_GRAPH_PROF").is_ok() {
3272                                eprintln!(
3273                                    "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
3274                                    pos,
3275                                    n_pairs,
3276                                    pos + n_pairs - 1,
3277                                );
3278                            }
3279                            let warm_error = if let Some(m) = mtp.as_mut() {
3280                                self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
3281                            } else {
3282                                None
3283                            };
3284                            if let Some(err) = warm_error {
3285                                // The trunk batch was already admitted.  A
3286                                // failed MTP warm-up therefore clears both
3287                                // mirrors and exits; continuing would pair a
3288                                // current trunk state with a stale MTP cache.
3289                                self.finish_generation(&mut mtp, &mut router, true);
3290                                return Err(err.to_string());
3291                            }
3292                        }
3293                    }
3294                    hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
3295                    pos = end;
3296                } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
3297                    // A failed batch may have advanced a device recurrent
3298                    // state (ordinary GDN or sealed O(1)). A CPU fallback
3299                    // would then observe stale accumulators, so clear the
3300                    // request state and make the failure explicit.
3301                    self.finish_generation(&mut mtp, &mut router, true);
3302                    return Err(if o1_batch_ready {
3303                        "sealed O(1) batch graph failed after admission".to_string()
3304                    } else {
3305                        "ordinary recurrent batch graph failed after admission".to_string()
3306                    });
3307                } else {
3308                    break; // unsupported → per-position graph handles the rest
3309                }
3310            }
3311        }
3312        while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3313            self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
3314            hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
3315            if let Some(m) = &mut mtp {
3316                if pos + 1 < input_ids.len() {
3317                    // `CMF_MTP_CHAIN_PROBE=k`: teacher-forced acceptance of a
3318                    // CHAINED draft — iterate the head on its own hidden k
3319                    // deep and score every depth against the prompt's real
3320                    // continuation. The economics of a k-token speculative
3321                    // round stand or fall on this table.
3322                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3323                        .ok()
3324                        .and_then(|v| v.parse().ok())
3325                        .unwrap_or(0);
3326                    if probe >= 1 && pos + 2 < input_ids.len() {
3327                        let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
3328                        let mut ok = d1 == input_ids[pos + 2];
3329                        Self::chain_probe_note(0, ok);
3330                        let mut d_prev = d1;
3331                        let mut extra = 0usize;
3332                        for j in 1..probe {
3333                            if pos + 2 + j >= input_ids.len() {
3334                                break;
3335                            }
3336                            let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
3337                            extra += 1;
3338                            ok = ok && dj == input_ids[pos + 2 + j];
3339                            Self::chain_probe_note(j, ok);
3340                            d_prev = dj;
3341                            hx = hj;
3342                        }
3343                        // The chain's rows are speculation, not the prompt —
3344                        // keep only the warmup row the plain path would add.
3345                        m.kv.truncate_last(extra);
3346                    } else {
3347                        let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
3348                    }
3349                }
3350            }
3351            pos += 1;
3352        }
3353        if std::env::var("CMF_PREFILL_PROF").is_ok() {
3354            eprintln!(
3355                "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
3356                input_ids.len(),
3357                _tpf.elapsed().as_secs_f64() * 1000.0
3358            );
3359        }
3360        if self
3361            .graph_failed
3362            .swap(false, std::sync::atomic::Ordering::Relaxed)
3363        {
3364            // MTP is detached for speculative generation.  Restore the
3365            // module before returning the terminal graph error; otherwise a
3366            // failed request would silently remove the head from a pooled
3367            // pipeline and the next request would lose its configured route.
3368            self.finish_generation(&mut mtp, &mut router, true);
3369            return Err("GPU token graph failed during prefill".to_string());
3370        }
3371        // Cancelled mid-prefill: the cache holds a partial prompt —
3372        // drop the reuse history and return an empty generation.
3373        if self
3374            .cancel
3375            .swap(false, std::sync::atomic::Ordering::Relaxed)
3376        {
3377            // A cancelled prefill can already have advanced the device
3378            // mirror. Drop the whole partial sequence so a pooled pipeline
3379            // cannot carry that state into its next request.
3380            self.finish_generation(&mut mtp, &mut router, true);
3381            return Ok(GenerateResult {
3382                text: String::new(),
3383                token_ids: Vec::new(),
3384                prompt_tokens: input_ids.len(),
3385                tokens_generated: 0,
3386                finish_reason: "cancelled".to_string(),
3387                mtp_drafted: 0,
3388                mtp_accepted: 0,
3389                token_confidence: Vec::new(),
3390                traces: Vec::new(),
3391            });
3392        }
3393
3394        // Prompt absorbed → freeze the o1 layers' skeletons; from here
3395        // every decode step on those layers is O(W + m·dv + m²).
3396        if !o1_sealed {
3397            match self.o1_seal_checked() {
3398                Ok(_) => {}
3399                Err(err) => {
3400                    self.finish_generation(&mut mtp, &mut router, true);
3401                    return Err(err);
3402                }
3403            }
3404        }
3405
3406        // Commit one token: push, check EOS, stream. Returns false = stop.
3407        macro_rules! commit {
3408            ($id:expr) => {{
3409                all_ids.push($id);
3410                generated += 1;
3411                if self.tokenizer.is_eos($id) {
3412                    finish_reason = "stop".to_string();
3413                    false
3414                } else {
3415                    let token_text = self.tokenizer.decode_token($id);
3416                    let mut go = true;
3417                    if let Some(ref mut cb) = on_token {
3418                        if !cb(&token_text) {
3419                            finish_reason = "cancelled".to_string();
3420                            go = false;
3421                        }
3422                    }
3423                    go
3424                }
3425            }};
3426        }
3427
3428        // Speculation is decided by MEASUREMENT, not by an acceptance
3429        // model. A k=4 round costs ~3.8 plain tokens on the 5090 (draft
3430        // 6.6 + verify 66.6 + commit 4.8 ms against a 20.6 ms token), so it
3431        // pays only when the head lands ~2.8 of 4 — predictable text (code,
3432        // structured output) does, free prose often does not, and the
3433        // ratio at which the two cross depends on the card and the context
3434        // depth. So: four speculative rounds timed, then eight plain
3435        // tokens timed, and the faster arm runs until a re-check 256
3436        // tokens later (context growth moves the balance). The trial
3437        // costs at most a few tokens of the slower arm per 256.
3438        let mut spec_trial = SpecTrial::Spec {
3439            t0: std::time::Instant::now(),
3440            gen0: generated,
3441            rounds: 0,
3442        };
3443        let mut spec_mon = SpecMon::default();
3444        let mut spec_watchdog_off = false;
3445        // ── Decode ──
3446        let mut next_pos = input_ids.len();
3447        'decode: while generated < max_tokens {
3448            if self
3449                .graph_failed
3450                .swap(false, std::sync::atomic::Ordering::Relaxed)
3451            {
3452                // Keep the detached MTP module attached after a terminal
3453                // graph error so the pipeline can be reused for a fresh
3454                // sequence.  `clear_sequence_state` only clears mirrors and
3455                // host KV; it cannot recover a module dropped here.
3456                self.finish_generation(&mut mtp, &mut router, true);
3457                return Err("GPU token graph failed during decode".to_string());
3458            }
3459            if self
3460                .cancel
3461                .swap(false, std::sync::atomic::Ordering::Relaxed)
3462            {
3463                finish_reason = "cancelled".to_string();
3464                break 'decode;
3465            }
3466            // A rejected speculative draft already drew this position's
3467            // token from the residual distribution (graph_spec_step); it
3468            // is committed as-is — sampling again from the row's logits
3469            // would bias the stream toward the target's mode.
3470            let forced = self.spec_forced.take();
3471            let mut logits = match (forced, self.graph_logits.take()) {
3472                (Some(_), _) => Vec::new(),
3473                (None, Some(lg)) => lg,
3474                (None, None) => {
3475                    inference::rms_norm_into(
3476                        &hidden,
3477                        &self.weights.final_norm,
3478                        self.rms_eps,
3479                        self.norm_style,
3480                        &mut self.ws.n1,
3481                    );
3482                    self.lm_head_forward(&self.ws.n1)
3483                }
3484            };
3485            // CMF_LOGIT_DUMP=<path>: the first decode step's hidden + logits
3486            // as raw f32 (hidden first) — cross-backend numerics diffing.
3487            if generated
3488                == std::env::var("CMF_LOGIT_DUMP_STEP")
3489                    .ok()
3490                    .and_then(|v| v.parse().ok())
3491                    .unwrap_or(0)
3492            {
3493                if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
3494                    let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
3495                    for v in hidden.iter().chain(logits.iter()) {
3496                        bytes.extend_from_slice(&v.to_le_bytes());
3497                    }
3498                    if let Err(e) = std::fs::write(&path, &bytes) {
3499                        eprintln!("logit dump: failed to write {path}: {e}");
3500                        self.finish_generation(&mut mtp, &mut router, true);
3501                        return Err(format!("logit dump write failed: {e}"));
3502                    }
3503                }
3504            }
3505            let t_next = match forced {
3506                Some(c) => c,
3507                None => sampler::sample_with_scratch_pool(
3508                    &logits,
3509                    &self.sampler_config,
3510                    &all_ids,
3511                    &mut self.rng,
3512                    &mut self.sampler_scratch,
3513                    self.pool.as_deref(),
3514                ),
3515            };
3516            if self.confidence_on {
3517                confidence.push(if logits.is_empty() {
3518                    0.0
3519                } else {
3520                    sampler::top1_prob_pool(
3521                        self.pool.as_deref(),
3522                        &mut self.sampler_scratch,
3523                        &logits,
3524                        t_next,
3525                        calib_temp,
3526                    )
3527                });
3528            }
3529            if !logits.is_empty() {
3530                attention::recycle_buf(&mut logits);
3531            }
3532            if trace_on {
3533                // active_skill = the overlay in force while this token was
3534                // generated; recon/switched are filled after the post-emit
3535                // routing eval below (freshest coherence for this token).
3536                let skill = router.as_ref().and_then(|r| r.active_id());
3537                traces.push(TokenTrace {
3538                    t: generated,
3539                    token_id: t_next,
3540                    confidence: confidence.last().copied().unwrap_or(0.0),
3541                    active_skill: skill,
3542                    recon: None,
3543                    switched: false,
3544                });
3545            }
3546            if !commit!(t_next) {
3547                break 'decode;
3548            }
3549            if generated >= max_tokens {
3550                break 'decode;
3551            }
3552
3553            if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
3554                // Say it ONCE, loudly: past this point the model keeps
3555                // talking but has lost half its context, and on a GDN
3556                // hybrid the graph's device state goes stale on top. The
3557                // Qwen3.8 bring-up spent a day reading this cliff as
3558                // three different model bugs.
3559                static SAID: std::sync::Once = std::sync::Once::new();
3560                SAID.call_once(|| {
3561                    tracing::warn!(
3562                        "KV cache full at {} positions — evicting half; quality \
3563                         will degrade. Raise CMF_MAX_SEQ.",
3564                        self.kv_cache.max_seq_len,
3565                    );
3566                });
3567                let keep = (self.kv_cache.max_seq_len / 2).max(1);
3568                self.kv_cache.evict(keep);
3569            }
3570
3571            // Advance the speculation trial: plain-phase accounting and
3572            // the periodic re-check happen here, on every token.
3573            if graph_spec {
3574                match spec_trial {
3575                    SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
3576                        spec_mon.plain_ms =
3577                            t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
3578                        let keep = spec_mon.pays();
3579                        tracing::info!(
3580                            "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3581                            spec_mon.tokens,
3582                            spec_mon.round_ms,
3583                            spec_mon.plain_ms,
3584                            if keep { "speculating" } else { "plain" }
3585                        );
3586                        spec_mon.fails = 0;
3587                        spec_trial = SpecTrial::Decided {
3588                            spec: keep,
3589                            recheck_at: if keep { usize::MAX } else { generated + 128 },
3590                        };
3591                    }
3592                    SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
3593                        spec_mon.n = 0;
3594                        spec_trial = SpecTrial::Spec {
3595                            t0: std::time::Instant::now(),
3596                            gen0: generated,
3597                            rounds: 0,
3598                        };
3599                    }
3600                    _ => {}
3601                }
3602                spec_watchdog_off = matches!(
3603                    spec_trial,
3604                    SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
3605                );
3606            }
3607            match &mut mtp {
3608                // ── Graph speculation: chain-draft, batch-verify on device ──
3609                #[cfg(feature = "gpu")]
3610                Some(m)
3611                    if graph_spec
3612                        && !spec_watchdog_off
3613                        && generated + 1 < max_tokens
3614                        && next_pos > 0 =>
3615                {
3616                    let t_round = std::time::Instant::now();
3617                    if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
3618                        m,
3619                        &hidden,
3620                        t_next,
3621                        next_pos,
3622                        &mut drafted,
3623                        &mut accepted,
3624                        &mut all_ids,
3625                    ) {
3626                        next_pos = n_pos;
3627                        hidden = new_h;
3628                        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3629                            eprintln!(
3630                                "spec-round wall {:.1} ms → {} tokens",
3631                                t_round.elapsed().as_secs_f64() * 1e3,
3632                                extra.len() + 1
3633                            );
3634                        }
3635                        // One speculative round done: the monitor counts it
3636                        // (round 1 untimed — it pays the batch scratch and
3637                        // the draft mirror), and the trial advances.
3638                        spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
3639                        // the round's tokens land in `generated` below; the
3640                        // plain phase must start counting AFTER them
3641                        spec_trial = Self::spec_trial_round(
3642                            spec_trial,
3643                            &mut spec_mon,
3644                            generated + extra.len() + 1,
3645                        );
3646                        let mut stopped = false;
3647                        for &id in &extra {
3648                            if self.confidence_on {
3649                                confidence.push(0.0);
3650                            }
3651                            if !commit!(id) {
3652                                stopped = true;
3653                                break;
3654                            }
3655                        }
3656                        if stopped {
3657                            break 'decode;
3658                        }
3659                        continue 'decode;
3660                    }
3661                    if self
3662                        .graph_failed
3663                        .swap(false, std::sync::atomic::Ordering::Relaxed)
3664                    {
3665                        // `graph_spec_step` may have detached MTP while a
3666                        // warm-up was in flight.  Do not reinterpret its
3667                        // terminal device failure as a plain decode step;
3668                        // restore the head, clear both mirrors, and surface
3669                        // one explicit error to the caller.
3670                        self.finish_generation(&mut mtp, &mut router, true);
3671                        return Err("GPU MTP graph failed during speculative decode".to_string());
3672                    }
3673                    // Declined (batch graph refused): plain forward below —
3674                    // and a round that produced one token for the trial's
3675                    // ledger, so a graph that keeps refusing is measured out
3676                    // like a head that keeps missing (it was spinning
3677                    // forever on a file whose batch graph declines).
3678                    // A declined round is not a cheap one-token round — it
3679                    // is a verify that does not exist for this file (a
3680                    // healed q8_2f tail measured 760 drafts, 0 accepted, 33
3681                    // against 48.8 tok/s while the monitor called the draft
3682                    // alone "paying"). Count it as the losing streak in one.
3683                    spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
3684                    spec_mon.tokens = 0.0;
3685                    spec_mon.fails = 3;
3686                    spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
3687                    hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
3688                    next_pos += 1;
3689                    continue 'decode;
3690                }
3691                // ── Speculative: draft t+2, verify in a fused pair ──
3692                Some(m) if !graph_spec && generated + 1 < max_tokens => {
3693                    let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
3694                    drafted += 1;
3695                    let emb1 = self.embed_single(t_next);
3696                    let emb2 = self.embed_single(draft);
3697                    let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
3698
3699                    inference::rms_norm_into(
3700                        &h1,
3701                        &self.weights.final_norm,
3702                        self.rms_eps,
3703                        self.norm_style,
3704                        &mut self.ws.n1,
3705                    );
3706                    let mut logits1 = self.lm_head_forward(&self.ws.n1);
3707                    let t_after = sampler::sample_with_scratch_pool(
3708                        &logits1,
3709                        &self.sampler_config,
3710                        &all_ids,
3711                        &mut self.rng,
3712                        &mut self.sampler_scratch,
3713                        self.pool.as_deref(),
3714                    );
3715                    if self.confidence_on {
3716                        confidence.push(sampler::top1_prob_pool(
3717                            self.pool.as_deref(),
3718                            &mut self.sampler_scratch,
3719                            &logits1,
3720                            t_after,
3721                            calib_temp,
3722                        ));
3723                    }
3724                    attention::recycle_buf(&mut logits1);
3725                    if trace_on {
3726                        // Speculative decode is mutually exclusive with
3727                        // dynamic routing (router is None here) — no skill.
3728                        traces.push(TokenTrace {
3729                            t: generated,
3730                            token_id: t_after,
3731                            confidence: confidence.last().copied().unwrap_or(0.0),
3732                            active_skill: None,
3733                            recon: None,
3734                            switched: false,
3735                        });
3736                    }
3737                    let stop = !commit!(t_after);
3738
3739                    if t_after == draft {
3740                        accepted += 1;
3741                        self.commit_linear_scratch();
3742                        let _ = self.mtp_step(m, &h1, t_after, next_pos);
3743                        hidden = h2;
3744                        next_pos += 2;
3745                    } else {
3746                        // The draft lane is wrong: roll its KV entry back.
3747                        for layer in &mut self.kv_cache.layers {
3748                            layer.truncate_last(1);
3749                        }
3750                        if !stop {
3751                            let _ = self.mtp_step(m, &h1, t_after, next_pos);
3752                            hidden = self.forward_layers(
3753                                &self.embed_single(t_after),
3754                                next_pos + 1,
3755                                None,
3756                            );
3757                        }
3758                        next_pos += 2;
3759                    }
3760                    if stop {
3761                        break 'decode;
3762                    }
3763                }
3764                // ── Vanilla: forward the sampled token ──
3765                _ => {
3766                    // ── DeepSeek-V4 speculative decode (CMF_DSV4_SPEC=1):
3767                    // draft five on the card, verify batched, commit the
3768                    // accepted prefix. Greedy only; a rejected token's state
3769                    // is restored and replayed, so output equals the walk. ──
3770                    #[cfg(feature = "gpu")]
3771                    if Self::dsv4_spec_on() && self.dsv4.is_some() {
3772                        static SAID: std::sync::Once = std::sync::Once::new();
3773                        SAID.call_once(|| {
3774                            eprintln!(
3775                                "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
3776                                !self.dsv4_mtp.is_empty(),
3777                                task_mask.is_none(),
3778                                router.is_none(),
3779                                !trace_on,
3780                                self.sampler_config.temperature < 1e-6,
3781                                self.sampler_config.repetition_penalty == 1.0,
3782                            );
3783                        });
3784                    }
3785                    #[cfg(feature = "gpu")]
3786                    if Self::dsv4_spec_on()
3787                        && self.dsv4.is_some()
3788                        && !self.dsv4_mtp.is_empty()
3789                        && task_mask.is_none()
3790                        && router.is_none()
3791                        && !trace_on
3792                        && self.sampler_config.temperature < 1e-6
3793                        && self.sampler_config.repetition_penalty == 1.0
3794                        && generated + 1 < max_tokens
3795                        && all_ids.len() >= 2
3796                        && generated >= dsv4_spec_retry_at
3797                    {
3798                        let tip_token = all_ids[all_ids.len() - 2];
3799                        let drafted0 = drafted;
3800                        let round = self.dsv4_spec_step(
3801                            tip_token,
3802                            t_next,
3803                            next_pos,
3804                            max_tokens.saturating_sub(generated),
3805                            &mut drafted,
3806                            &mut accepted,
3807                        );
3808                        if drafted > drafted0 {
3809                            let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
3810                            if useful {
3811                                dsv4_spec_bad = 0;
3812                            } else {
3813                                dsv4_spec_bad += 1;
3814                                if dsv4_spec_bad >= 2 {
3815                                    dsv4_spec_bad = 0;
3816                                    dsv4_spec_retry_at = generated.saturating_add(32);
3817                                    tracing::info!(
3818                                        "dsv4: draft не окупился дважды — точный walk на 32 токена"
3819                                    );
3820                                }
3821                            }
3822                        }
3823                        if let Some((extra, n_pos)) = round {
3824                            next_pos = n_pos;
3825                            let mut stopped = false;
3826                            for &id in &extra {
3827                                if self.confidence_on {
3828                                    confidence.push(0.0);
3829                                }
3830                                if !commit!(id) {
3831                                    stopped = true;
3832                                    break;
3833                                }
3834                            }
3835                            if stopped {
3836                                break 'decode;
3837                            }
3838                            continue 'decode;
3839                        }
3840                    }
3841                    self.graph_want_logits = fuse_lm;
3842                    // Greedy burst (CMF_MULTISTEP, default 8, 1 = off): while
3843                    // nothing observes per-token state — pure argmax sampling,
3844                    // no router/trace/confidence/mask — decode k tokens per
3845                    // submit and commit them wholesale. The trailing normal
3846                    // forward leaves logits for the loop top, as always.
3847                    let mut t_fwd = t_next;
3848                    let pure_greedy = self.sampler_config.temperature < 1e-6
3849                        && self.sampler_config.repetition_penalty == 1.0
3850                        && self.sampler_config.suppress_tokens.is_empty();
3851                    // Off by default: at every k the burst measured at or
3852                    // below the plain path on this graph shape (k=1 loses
3853                    // the argmax dispatches vs a 1 MB readback, k>=8 loses
3854                    // inter-step drains vs the saved sync). Experimental.
3855                    let burst_k = std::env::var("CMF_MULTISTEP")
3856                        .ok()
3857                        .and_then(|v| v.parse::<usize>().ok())
3858                        .unwrap_or(0);
3859                    if pure_greedy
3860                        && burst_k >= 1
3861                        && fuse_lm
3862                        && task_mask.is_none()
3863                        && router.is_none()
3864                        && !trace_on
3865                        && !self.confidence_on
3866                    {
3867                        let mut stopped = false;
3868                        loop {
3869                            let room = max_tokens.saturating_sub(generated);
3870                            if room <= 2 {
3871                                break;
3872                            }
3873                            let k = burst_k.min(room - 1);
3874                            if k < 1 {
3875                                break;
3876                            }
3877                            let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
3878                                if self
3879                                    .graph_failed
3880                                    .swap(false, std::sync::atomic::Ordering::Relaxed)
3881                                {
3882                                    self.finish_generation(&mut mtp, &mut router, true);
3883                                    return Err(
3884                                        "GPU token graph failed during greedy burst".to_string()
3885                                    );
3886                                }
3887                                break;
3888                            };
3889                            next_pos += k;
3890                            for &id in &ids {
3891                                if !commit!(id) {
3892                                    stopped = true;
3893                                    break;
3894                                }
3895                            }
3896                            if stopped {
3897                                break;
3898                            }
3899                            t_fwd = *ids.last().unwrap();
3900                        }
3901                        if stopped {
3902                            break 'decode;
3903                        }
3904                    }
3905                    hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
3906                    next_pos += 1;
3907                    // Dynamic routing: the forward updated φ; ask the
3908                    // router whether to switch skills before the next token.
3909                    if let Some(r) = &mut router {
3910                        let phi = self.dyn_phi_ema.clone();
3911                        let decision = r.step(&phi, generated);
3912                        if let Some(new_active) = decision {
3913                            let _ = self.set_active_skill(new_active);
3914                        }
3915                        // Backfill this token's coherence + switch flag from
3916                        // the just-run eval (freshest measured values).
3917                        if trace_on {
3918                            if let Some(last) = traces.last_mut() {
3919                                let e = r.last_best_e();
3920                                last.recon = e.is_finite().then_some(e);
3921                                last.switched = decision.is_some();
3922                            }
3923                        }
3924                    }
3925                }
3926            }
3927        }
3928
3929        let cancelled = finish_reason == "cancelled";
3930        self.finish_generation(&mut mtp, &mut router, cancelled);
3931
3932        let output_ids = &all_ids[input_ids.len()..];
3933        // Forwarded = prompt + all generated but the LAST sampled token
3934        // (emitted without being fed back). Exact only without MTP —
3935        // reuse is gated off when MTP is active.
3936        let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3937        if cancelled {
3938            self.kv_history.clear();
3939        } else {
3940            self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3941        }
3942        confidence.truncate(output_ids.len()); // guard against any overshoot
3943        traces.truncate(output_ids.len());
3944        Ok(GenerateResult {
3945            text: self.tokenizer.decode(output_ids),
3946            token_ids: output_ids.to_vec(),
3947            prompt_tokens: input_ids.len(),
3948            tokens_generated: generated,
3949            finish_reason,
3950            mtp_drafted: drafted,
3951            mtp_accepted: accepted,
3952            token_confidence: confidence,
3953            traces,
3954        })
3955    }
3956
3957    /// One MTP step: feed `(hidden_p, token_{p+1})` into the draft head,
3958    /// advance its KV cache at position `p`, return the drafted token
3959    /// for position `p+2`.
3960    fn mtp_step(
3961        &mut self,
3962        m: &mut MtpModule,
3963        hidden: &[f32],
3964        next_token: u32,
3965        position: usize,
3966    ) -> u32 {
3967        self.mtp_step_h(m, hidden, next_token, position).0
3968    }
3969
3970    /// Tally for `CMF_MTP_CHAIN_PROBE`: per depth, how often the CHAIN is
3971    /// still an exact prefix of the real continuation. Printed every 128
3972    /// depth-0 samples so a killed run still shows its table.
3973    fn chain_probe_note(depth: usize, prefix_ok: bool) {
3974        use std::sync::Mutex;
3975        static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
3976        let mut t = T.lock().unwrap();
3977        if t.len() <= depth {
3978            t.resize(depth + 1, (0, 0));
3979        }
3980        t[depth].0 += 1;
3981        t[depth].1 += prefix_ok as u64;
3982        if depth == 0 && t[0].0 % 128 == 0 {
3983            let line: Vec<String> = t
3984                .iter()
3985                .enumerate()
3986                .map(|(d, (n, k))| {
3987                    format!(
3988                        "d{}={:.0}%({n})",
3989                        d + 1,
3990                        100.0 * *k as f64 / (*n).max(1) as f64
3991                    )
3992                })
3993                .collect();
3994            eprintln!("mtp-chain: {}", line.join(" "));
3995        }
3996    }
3997
3998    /// `mtp_step` that also hands back the block's own output hidden — the
3999    /// state a CHAINED draft feeds the next step, the way a multi-token
4000    /// speculative round iterates the head on itself.
4001    /// One MTP block step from (trunk hidden, token): the head's LOGITS
4002    /// and the block's own hidden for chaining. The draft is argmax of the
4003    /// logits on the greedy path and a draw from their post-chain
4004    /// distribution on the sampling path.
4005    fn mtp_step_hl(
4006        &mut self,
4007        m: &mut MtpModule,
4008        hidden: &[f32],
4009        next_token: u32,
4010        position: usize,
4011    ) -> (Vec<f32>, Vec<f32>) {
4012        // The graph arm: the MTP block as a one-layer token graph with the
4013        // head fused — device attention over the block's own KV mirror,
4014        // one submit for block + head, hidden and logits back together.
4015        // Decided once per generation (see `mtp_graph_mode`).
4016        #[cfg(target_os = "macos")]
4017        if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
4018            if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
4019                self.mtp_graph_mode = Some(true);
4020                return r;
4021            }
4022            if self.mtp_graph_mode == Some(true) {
4023                tracing::error!("mtp Metal graph failed after admission");
4024                self.clear_sequence_state();
4025                self.graph_failed
4026                    .store(true, std::sync::atomic::Ordering::Relaxed);
4027                self.cancel
4028                    .store(true, std::sync::atomic::Ordering::Relaxed);
4029                return (Vec::new(), Vec::new());
4030            }
4031            self.mtp_graph_mode = Some(false);
4032        }
4033        #[cfg(feature = "gpu")]
4034        if self.mtp_graph_mode != Some(false) {
4035            if !self.mtp_graph_ok(m) {
4036                if self.mtp_graph_mode == Some(true) {
4037                    // A mirror was already admitted, so a capability change
4038                    // cannot safely switch this request to the stale CPU
4039                    // cache.  Keep the same terminal contract as a failed
4040                    // token graph.
4041                    tracing::error!("mtp graph became unavailable after admission");
4042                    self.clear_sequence_state();
4043                    self.graph_failed
4044                        .store(true, std::sync::atomic::Ordering::Relaxed);
4045                    self.cancel
4046                        .store(true, std::sync::atomic::Ordering::Relaxed);
4047                    return (Vec::new(), Vec::new());
4048                }
4049                self.mtp_graph_mode = Some(false);
4050            } else {
4051                if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
4052                    self.mtp_graph_mode = Some(true);
4053                    return r;
4054                }
4055                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4056                    // A token graph can have admitted a persistent MTP/GDN
4057                    // mirror before its readback failed.  The CPU MTP cache
4058                    // is not a valid continuation in that state; leave the
4059                    // flag set so the generation caller returns through its
4060                    // terminal error path instead of silently switching
4061                    // arithmetic.
4062                    return (Vec::new(), Vec::new());
4063                }
4064                // `mtp_graph_ok` was true, so a None here means a refusal or
4065                // failure after graph admission.  Do not fall through to a
4066                // CPU cache whose rows may lag the device mirror.
4067                tracing::error!("mtp graph failed or declined after admission");
4068                self.clear_sequence_state();
4069                self.graph_failed
4070                    .store(true, std::sync::atomic::Ordering::Relaxed);
4071                self.cancel
4072                    .store(true, std::sync::atomic::Ordering::Relaxed);
4073                return (Vec::new(), Vec::new());
4074            }
4075        }
4076        // fc concat order is [enorm(embed); hnorm(hidden)] — EMBEDDING
4077        // FIRST. Verified by the oracle (converter/mtp_oracle.py):
4078        // [emb;hid] → 45.8% acceptance, [hid;emb] → 0.00%.
4079        let e = self.embed_single(next_token);
4080        let mut cat = vec![0.0f32; 2 * self.hidden_size];
4081        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4082        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4083        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4084        let mut x = vec![0.0f32; self.hidden_size];
4085        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4086
4087        // One standard transformer block over the MTP's own cache.
4088        let lw = &m.layer;
4089        inference::rms_norm_into(
4090            &x,
4091            &lw.input_norm,
4092            self.rms_eps,
4093            self.norm_style,
4094            &mut self.ws.n1,
4095        );
4096        let attn = match &lw.attn {
4097            // MLA models carry no MTP head; this path cannot see them.
4098            AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4099            AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4100            AttnKind::Full {
4101                wq,
4102                wk,
4103                wv,
4104                wo,
4105                q_norm,
4106                k_norm,
4107                output_gate,
4108                softplus_gate,
4109                bias,
4110            } => {
4111                let mut cfg = self.attn_cfg(position);
4112                cfg.q_norm = q_norm.as_deref();
4113                cfg.k_norm = k_norm.as_deref();
4114                cfg.output_gate = *output_gate;
4115                cfg.softplus_gate = softplus_gate
4116                    .as_ref()
4117                    .map(|(gate, per_head)| (gate, *per_head));
4118                cfg.bias = bias
4119                    .as_ref()
4120                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4121                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4122            }
4123            AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
4124                unreachable!("MTP block is full attention")
4125            }
4126        };
4127        for (i, &a) in attn.iter().enumerate() {
4128            x[i] += a;
4129        }
4130        inference::rms_norm_into(
4131            &x,
4132            &lw.post_norm,
4133            self.rms_eps,
4134            self.norm_style,
4135            &mut self.ws.p1,
4136        );
4137        let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
4138        for (i, &f) in ffn.iter().enumerate() {
4139            x[i] += f;
4140        }
4141
4142        inference::rms_norm_into(
4143            &x,
4144            &m.final_norm,
4145            self.rms_eps,
4146            self.norm_style,
4147            &mut self.ws.n1,
4148        );
4149        let lg = self.lm_head_forward(&self.ws.n1);
4150        (lg, x)
4151    }
4152
4153    /// `mtp_step_hl` reduced to the greedy draft: argmax of the head.
4154    fn mtp_step_h(
4155        &mut self,
4156        m: &mut MtpModule,
4157        hidden: &[f32],
4158        next_token: u32,
4159        position: usize,
4160    ) -> (u32, Vec<f32>) {
4161        let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
4162        let draft = sampler::argmax(&lg);
4163        attention::recycle_buf(&mut lg);
4164        (draft, x)
4165    }
4166
4167    /// One speculative round for the trial: rounds 1..5 of a `Spec` phase
4168    /// advance it (the monitor already averaged this round); after five,
4169    /// the plain phase runs (once — a known plain rate decides at once);
4170    /// a decided speculation keeps re-checking the rule every round and
4171    /// stops after four losing rounds in a row.
4172    fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
4173        match trial {
4174            SpecTrial::Spec { t0, gen0, rounds } => {
4175                let rounds = rounds + 1;
4176                if rounds >= 5 {
4177                    if mon.plain_ms > 0.0 {
4178                        let keep = mon.pays();
4179                        mon.fails = 0;
4180                        tracing::info!(
4181                            "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4182                            mon.tokens,
4183                            mon.round_ms,
4184                            mon.plain_ms,
4185                            if keep { "speculating" } else { "plain" }
4186                        );
4187                        SpecTrial::Decided {
4188                            spec: keep,
4189                            recheck_at: if keep { usize::MAX } else { generated + 128 },
4190                        }
4191                    } else {
4192                        SpecTrial::Plain {
4193                            t0: std::time::Instant::now(),
4194                            gen0: generated,
4195                        }
4196                    }
4197                } else {
4198                    SpecTrial::Spec { t0, gen0, rounds }
4199                }
4200            }
4201            SpecTrial::Decided { spec: true, .. } => {
4202                if mon.pays() {
4203                    mon.fails = 0;
4204                    trial
4205                } else {
4206                    mon.fails += 1;
4207                    if mon.fails >= 4 {
4208                        tracing::info!(
4209                            "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
4210                            mon.tokens,
4211                            mon.round_ms,
4212                            mon.plain_ms
4213                        );
4214                        SpecTrial::Decided {
4215                            spec: false,
4216                            recheck_at: generated + 128,
4217                        }
4218                    } else {
4219                        trial
4220                    }
4221                }
4222            }
4223            other => other,
4224        }
4225    }
4226
4227    /// The MTP block's device-mirror id: the trunk's id with a high bit,
4228    /// so the (kv_id, layer) mirror keys never collide.
4229    fn mtp_kv_id(&self) -> u64 {
4230        self.graph_kv_id | (1u64 << 40)
4231    }
4232
4233    /// The MTP block's mirror layer index: 0 — its own kv_id keeps it
4234    /// apart from the trunk, and the BATCH graph (the warm-up path) keys
4235    /// its mirrors at layer 0 with no base of its own, so the draft's
4236    /// token graph must key the same slot.
4237    const MTP_LAYER_BASE: usize = 0;
4238
4239    /// The wgpu MTP draft writes speculative rows straight into its device
4240    /// mirror while the CPU owner retains only the real prompt/decode anchor.
4241    /// After verification, move that mirror cursor back to the anchor before
4242    /// replaying accepted pairs.  The next graph append then sees the same
4243    /// contiguous position as the CPU/Metal path without uploading stale
4244    /// speculative rows.
4245    #[cfg(feature = "gpu")]
4246    fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
4247        self.mtp_graph_mode != Some(true)
4248            || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
4249    }
4250
4251    /// A speculative verify graph appends the full `k+1` trunk rows before
4252    /// the acceptance count is known.  GDN state already has a snapshot
4253    /// restore; Full-attention mirrors need the matching logical cursor
4254    /// rewind so the next graph call does not reject an ahead-of-position KV
4255    /// cache after a partial acceptance.
4256    #[cfg(feature = "gpu")]
4257    fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
4258        let mut ok = true;
4259        let mut expected = false;
4260        for li in 0..self.num_layers {
4261            if matches!(
4262                self.weights.layers[self.phys_layer(li)].attn,
4263                AttnKind::Full { .. }
4264            ) {
4265                expected = true;
4266                ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
4267            }
4268        }
4269        !expected || ok
4270    }
4271
4272    /// Count the recurrent layers participating in the trunk verify graph.
4273    /// Snapshot restore is all-or-nothing across that set; deriving the count
4274    /// from the model keeps the restore contract valid for looped models too.
4275    fn graph_gdn_layer_count(&self) -> usize {
4276        (0..self.num_layers)
4277            .filter(|&li| {
4278                matches!(
4279                    &self.weights.layers[self.phys_layer(li)].attn,
4280                    AttnKind::LinearGdn(_)
4281                )
4282            })
4283            .count()
4284    }
4285
4286    /// The block's input from (trunk hidden, token): eh_proj · [enorm(e);
4287    /// hnorm(h)] — the same arithmetic the per-op path starts with.
4288    fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
4289        let e = self.embed_single(next_token);
4290        let mut cat = vec![0.0f32; 2 * self.hidden_size];
4291        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4292        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4293        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4294        let mut x = vec![0.0f32; self.hidden_size];
4295        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4296        x
4297    }
4298
4299    /// Is the MTP block graphable at all (device up, full attention
4300    /// without softplus, dense FFN)? The plan itself is built per call.
4301    #[cfg(feature = "gpu")]
4302    fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
4303        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
4304            return false;
4305        }
4306        if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
4307            || !crate::gpu::enabled_here()
4308            || self.attn_softcap > 0.0
4309            || self.attention_heads_per_layer.is_some()
4310        {
4311            return false;
4312        }
4313        matches!(
4314            &m.layer.attn,
4315            AttnKind::Full {
4316                softplus_gate: None,
4317                ..
4318            }
4319        ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
4320    }
4321
4322    /// Full MTP token-graph eligibility, including the fused lm-head and all
4323    /// block projection weights.  Keep this distinct from the block-only
4324    /// check: prompt warm-up does not need the head, while a draft step does.
4325    #[cfg(feature = "gpu")]
4326    fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
4327        if !self.mtp_block_graph_ok(m) {
4328            return false;
4329        }
4330        let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
4331            return false;
4332        };
4333        let FfnKind::Dense(d) = &m.layer.ffn else {
4334            return false;
4335        };
4336        d.segs.is_empty()
4337            && wq.graph_weight().is_some()
4338            && wk.graph_weight().is_some()
4339            && wv.graph_weight().is_some()
4340            && wo.graph_weight().is_some()
4341            && d.gate_proj.graph_weight().is_some()
4342            && d.up_proj.graph_weight().is_some()
4343            && d.down_proj.graph_weight().is_some()
4344            && self.weights.lm_head.graph_weight().is_some()
4345    }
4346
4347    /// One MTP block step on the wgpu token graph: block + fused head in
4348    /// one submit, the block hidden and the logits read back together.
4349    /// None = the graph cannot take this block (softplus gate, non-dense
4350    /// FFN, unquantized head, no device) — the caller keeps the per-op
4351    /// path for the whole generation.
4352    #[cfg(feature = "gpu")]
4353    fn mtp_step_graph(
4354        &mut self,
4355        m: &mut MtpModule,
4356        hidden: &[f32],
4357        next_token: u32,
4358        position: usize,
4359    ) -> Option<(Vec<f32>, Vec<f32>)> {
4360        if !self.mtp_graph_ok(m) {
4361            return None;
4362        }
4363        let lw = &m.layer;
4364        let AttnKind::Full {
4365            wq,
4366            wk,
4367            wv,
4368            wo,
4369            q_norm,
4370            k_norm,
4371            output_gate,
4372            softplus_gate,
4373            bias,
4374        } = &lw.attn
4375        else {
4376            return None;
4377        };
4378        if softplus_gate.is_some() {
4379            return None;
4380        }
4381        let FfnKind::Dense(d) = &lw.ffn else {
4382            return None;
4383        };
4384        if !d.segs.is_empty() {
4385            return None; // tube layers run on the segmented path
4386        }
4387        // The block's input first: it borrows `self` mutably (embed scratch,
4388        // pool), the plan below borrows the weights immutably.
4389        let mut x = self.mtp_block_input(m, hidden, next_token);
4390        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4391            let (_, i, kind, rs) = t.graph_weight()?;
4392            Some(crate::gpu::GraphW {
4393                idx: i,
4394                kind,
4395                row_scale: rs,
4396                data: &[],
4397                prism: crate::gpu::GraphPrismOp::None,
4398                affine: false,
4399            })
4400        }
4401        let (model, _, _, _) = wq.graph_weight()?;
4402        let model = model.clone();
4403        let (lm_gw, lm_rows) = {
4404            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4405            (
4406                crate::gpu::GraphW {
4407                    idx: i,
4408                    kind,
4409                    row_scale: rs,
4410                    data: &[],
4411                    prism: crate::gpu::GraphPrismOp::None,
4412                    affine: false,
4413                },
4414                self.weights.lm_head.rows(),
4415            )
4416        };
4417        let layer = crate::gpu::GraphLayer {
4418            input_norm: &lw.input_norm,
4419            attn: crate::gpu::GraphAttn::Full {
4420                wq: gw(wq)?,
4421                wk: gw(wk)?,
4422                wv: gw(wv)?,
4423                wo: gw(wo)?,
4424                q_norm: q_norm.as_deref(),
4425                k_norm: k_norm.as_deref(),
4426                late_qk_norm: self.qk_norm_after_rope,
4427                bias: bias
4428                    .as_ref()
4429                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4430                output_gate: *output_gate,
4431                cpu_k: m.kv.k_heads(),
4432                cpu_v: m.kv.v_heads(),
4433            },
4434            post_norm: &lw.post_norm,
4435            ffn: crate::gpu::GraphFfn::Dense {
4436                gate: gw(&d.gate_proj)?,
4437                up: gw(&d.up_proj)?,
4438                down: gw(&d.down_proj)?,
4439            },
4440        };
4441        let nh = self.num_heads;
4442        let (nkv, hd, rd) = self.layer_geom(0);
4443        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4444        let mut logits = Vec::new();
4445        let ok = crate::gpu::forward_token_graph(
4446            &model,
4447            self.mtp_kv_id(),
4448            std::slice::from_ref(&layer),
4449            &[None],
4450            self.o1_epoch,
4451            &self.inv_freq,
4452            &mut x,
4453            nh,
4454            nkv,
4455            hd,
4456            self.attn_scale,
4457            rd,
4458            self.hidden_size,
4459            self.intermediate_size,
4460            position,
4461            self.kv_cache.max_seq_len,
4462            gemma,
4463            self.rms_eps as f32,
4464            Some((&lm_gw, lm_rows)),
4465            &m.final_norm,
4466            &mut logits,
4467            &[],
4468            1,
4469            None,
4470            None,
4471            None,
4472            Self::MTP_LAYER_BASE,
4473            true,
4474        );
4475        match ok {
4476            crate::gpu::TokenGraphOutcome::Completed => {}
4477            crate::gpu::TokenGraphOutcome::Declined => return None,
4478            crate::gpu::TokenGraphOutcome::Failed => {
4479                // The backend has already admitted persistent state.  Keep
4480                // this distinct from a capability refusal so the caller
4481                // cannot switch to the stale CPU MTP cache.
4482                self.clear_sequence_state();
4483                self.graph_failed
4484                    .store(true, std::sync::atomic::Ordering::Relaxed);
4485                self.cancel
4486                    .store(true, std::sync::atomic::Ordering::Relaxed);
4487                return None;
4488            }
4489        }
4490        logits.resize(self.vocab_size, 0.0);
4491        Some((logits, x))
4492    }
4493
4494    /// The warm-ups of one speculative round on the device: every accepted
4495    /// (hidden, token) pair as ONE batched graph run over the MTP block
4496    /// (no head) — its kv_append lands the pairs in the block's mirror.
4497    /// `pairs` are consecutive positions from `first_pos`.  The tri-state
4498    /// result is intentional: a refusal before admission may use the
4499    /// per-row/CPU route, while a failure after admission must terminate the
4500    /// sequence rather than fall through to a stale CPU cache.
4501    #[cfg(feature = "gpu")]
4502    fn mtp_warm_graph(
4503        &mut self,
4504        m: &mut MtpModule,
4505        pairs: &[(&[f32], u32)],
4506        first_pos: usize,
4507    ) -> crate::gpu::BatchGraphOutcome {
4508        if pairs.is_empty() {
4509            return crate::gpu::BatchGraphOutcome::Completed;
4510        }
4511        if !self.mtp_block_graph_ok(m) {
4512            return crate::gpu::BatchGraphOutcome::Declined;
4513        }
4514        let hs = self.hidden_size;
4515        // Block inputs for every pair (eh_proj on the per-op path, one
4516        // matvec each — the plan's own prologue).
4517        let mut hiddens = Vec::with_capacity(pairs.len() * hs);
4518        for (h, t) in pairs {
4519            hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
4520        }
4521        let lw = &m.layer;
4522        let AttnKind::Full {
4523            wq,
4524            wk,
4525            wv,
4526            wo,
4527            q_norm,
4528            k_norm,
4529            output_gate,
4530            bias,
4531            ..
4532        } = &lw.attn
4533        else {
4534            return crate::gpu::BatchGraphOutcome::Declined;
4535        };
4536        let FfnKind::Dense(d) = &lw.ffn else {
4537            return crate::gpu::BatchGraphOutcome::Declined;
4538        };
4539        if !d.segs.is_empty() {
4540            return crate::gpu::BatchGraphOutcome::Declined; // tube layers run on the segmented path
4541        }
4542        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4543            let (_, i, kind, rs) = t.graph_weight()?;
4544            Some(crate::gpu::GraphW {
4545                idx: i,
4546                kind,
4547                row_scale: rs,
4548                data: &[],
4549                prism: crate::gpu::GraphPrismOp::None,
4550                affine: false,
4551            })
4552        }
4553        let Some((model, _, _, _)) = wq.graph_weight() else {
4554            return crate::gpu::BatchGraphOutcome::Declined;
4555        };
4556        let model = model.clone();
4557        let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
4558            gw(wq),
4559            gw(wk),
4560            gw(wv),
4561            gw(wo),
4562            gw(&d.gate_proj),
4563            gw(&d.up_proj),
4564            gw(&d.down_proj),
4565        ) else {
4566            return crate::gpu::BatchGraphOutcome::Declined;
4567        };
4568        let layer = crate::gpu::GraphLayer {
4569            input_norm: &lw.input_norm,
4570            attn: crate::gpu::GraphAttn::Full {
4571                wq: gwq,
4572                wk: gwk,
4573                wv: gwv,
4574                wo: gwo,
4575                q_norm: q_norm.as_deref(),
4576                k_norm: k_norm.as_deref(),
4577                late_qk_norm: self.qk_norm_after_rope,
4578                bias: bias
4579                    .as_ref()
4580                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4581                output_gate: *output_gate,
4582                cpu_k: m.kv.k_heads(),
4583                cpu_v: m.kv.v_heads(),
4584            },
4585            post_norm: &lw.post_norm,
4586            ffn: crate::gpu::GraphFfn::Dense {
4587                gate: gg,
4588                up: gu,
4589                down: gd,
4590            },
4591        };
4592        let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
4593        let nh = self.num_heads;
4594        let (nkv, hd, rd) = self.layer_geom(0);
4595        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4596        crate::gpu::forward_batch_graph(
4597            &model,
4598            self.mtp_kv_id(),
4599            std::slice::from_ref(&layer),
4600            &self.inv_freq,
4601            &mut hiddens,
4602            nh,
4603            nkv,
4604            hd,
4605            rd,
4606            hs,
4607            self.intermediate_size,
4608            &positions,
4609            self.kv_cache.max_seq_len,
4610            gemma,
4611            self.rms_eps as f32,
4612            self.attn_scale,
4613            pairs.len(),
4614            &[],
4615            0,
4616            None,
4617        )
4618    }
4619
4620    /// Complete an MTP warm-up after the batched graph has refused.  A
4621    /// graphable block is retried one row at a time; once any device row has
4622    /// been admitted, a CPU fallback would observe a stale mirror, so every
4623    /// token-graph refusal is terminal.  If the block is not graphable and no
4624    /// mirror exists yet, warming on the CPU is safe and records the CPU mode
4625    /// for the rest of the generation.
4626    #[cfg(feature = "gpu")]
4627    fn mtp_warm_graph_fallback(
4628        &mut self,
4629        m: &mut MtpModule,
4630        pairs: &[(&[f32], u32)],
4631        first_pos: usize,
4632    ) -> bool {
4633        if pairs.is_empty() {
4634            return true;
4635        }
4636        let graphable = self.mtp_block_graph_ok(m);
4637        if !graphable {
4638            // A previously admitted mirror cannot be made coherent by
4639            // appending to the host cache.  The caller turns this into a
4640            // terminal generation error and clears both mirrors.
4641            if self.mtp_graph_mode == Some(true) {
4642                return false;
4643            }
4644            self.mtp_graph_mode = Some(false);
4645            for (j, (h, t)) in pairs.iter().enumerate() {
4646                self.mtp_warm(m, h, *t, first_pos + j);
4647            }
4648            return true;
4649        }
4650
4651        // The batch refusal is recoverable only through the same device
4652        // state.  Keep rows owned until each token graph has completed; a
4653        // None is treated as unsafe because the token-graph API deliberately
4654        // collapses its backend refusal/failure into that result.
4655        for (j, (h, t)) in pairs.iter().enumerate() {
4656            if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
4657                return false;
4658            }
4659        }
4660        self.mtp_graph_mode = Some(true);
4661        true
4662    }
4663
4664    /// Warm a contiguous set of MTP pairs using the existing graph seam, with
4665    /// an all-or-nothing error contract for callers that already admitted the
4666    /// trunk batch.  The non-GPU build keeps the same pair accounting while
4667    /// using the established CPU warm path.
4668    #[cfg(feature = "gpu")]
4669    fn mtp_warm_prefill_pairs(
4670        &mut self,
4671        m: &mut MtpModule,
4672        pairs: &[(&[f32], u32)],
4673        first_pos: usize,
4674    ) -> Result<(), &'static str> {
4675        // Keep unsupported token-graph heads on the established CPU MTP
4676        // route before admitting any block mirror.  Once a device mirror is
4677        // active, the same condition is terminal because CPU rows cannot
4678        // repair its state.
4679        if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
4680            if self.mtp_graph_mode == Some(true) {
4681                return Err("MTP token graph became unavailable after admission");
4682            }
4683            self.mtp_graph_mode = Some(false);
4684            for (j, (h, t)) in pairs.iter().enumerate() {
4685                self.mtp_warm(m, h, *t, first_pos + j);
4686            }
4687            return Ok(());
4688        }
4689        match self.mtp_warm_graph(m, pairs, first_pos) {
4690            crate::gpu::BatchGraphOutcome::Completed => {
4691                if !pairs.is_empty() {
4692                    self.mtp_graph_mode = Some(true);
4693                }
4694                Ok(())
4695            }
4696            crate::gpu::BatchGraphOutcome::Declined => {
4697                if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
4698                    Ok(())
4699                } else {
4700                    Err("MTP warm-up fallback failed after device admission")
4701                }
4702            }
4703            crate::gpu::BatchGraphOutcome::Failed => {
4704                Err("MTP warm batch graph failed after admission")
4705            }
4706        }
4707    }
4708
4709    #[cfg(not(feature = "gpu"))]
4710    fn mtp_warm_prefill_pairs(
4711        &mut self,
4712        m: &mut MtpModule,
4713        pairs: &[(&[f32], u32)],
4714        first_pos: usize,
4715    ) -> Result<(), &'static str> {
4716        for (j, (h, t)) in pairs.iter().enumerate() {
4717            self.mtp_warm(m, h, *t, first_pos + j);
4718        }
4719        Ok(())
4720    }
4721
4722    /// The MTP block alone — advance its KV with a (hidden, token) pair the
4723    /// verify just proved, without paying the head. What keeps the draft's
4724    /// attention context warm between speculative rounds.
4725    fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
4726        let e = self.embed_single(next_token);
4727        let mut cat = vec![0.0f32; 2 * self.hidden_size];
4728        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4729        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4730        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4731        let mut x = vec![0.0f32; self.hidden_size];
4732        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4733        inference::rms_norm_into(
4734            &x,
4735            &m.layer.input_norm,
4736            self.rms_eps,
4737            self.norm_style,
4738            &mut self.ws.n1,
4739        );
4740        let attn = match &m.layer.attn {
4741            AttnKind::Full {
4742                wq,
4743                wk,
4744                wv,
4745                wo,
4746                q_norm,
4747                k_norm,
4748                output_gate,
4749                softplus_gate,
4750                bias,
4751            } => {
4752                let mut cfg = self.attn_cfg(position);
4753                cfg.q_norm = q_norm.as_deref();
4754                cfg.k_norm = k_norm.as_deref();
4755                cfg.output_gate = *output_gate;
4756                cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
4757                cfg.bias = bias
4758                    .as_ref()
4759                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4760                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4761            }
4762            _ => return,
4763        };
4764        let _ = attn;
4765    }
4766
4767    /// Speculative decode ON the wgpu whole-token graph: draft k with the
4768    /// MTP head, verify all of them plus the tip in ONE batched graph
4769    /// submit whose tail folds the head, commit the accepted prefix and
4770    /// roll the GDN state back to the last real position. Greedy only —
4771    /// output equals the plain graph's token for token, the way the DSV4
4772    /// verify equals the walk.
4773    #[cfg(feature = "gpu")]
4774    #[allow(clippy::too_many_arguments)]
4775    fn graph_spec_step(
4776        &mut self,
4777        m: &mut MtpModule,
4778        hidden: &[f32],
4779        t_next: u32,
4780        next_pos: usize,
4781        drafted: &mut usize,
4782        accepted: &mut usize,
4783        // The committed stream (prompt + generated so far, `t_next`
4784        // included): the sampler chain's penalties read it, and the
4785        // sampling arm extends it with the drafts position by position.
4786        all_ids: &mut Vec<u32>,
4787    ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
4788        // 3 is the measured optimum on Qwen3.6-27B / RTX 5090 (medians
4789        // of three, greedy): 51.1 tok/s against a plain 49.4, where k=2
4790        // gives 46.1, k=4 50.0, k=5 47.4, k=6 45.2. Acceptance is 89-91%
4791        // throughout — what turns the curve over is the verify, which
4792        // costs ~7.4 ms per extra position, and the draft ~3 ms a step.
4793        // 4 since the draft moved onto the graph (Qwen3.8-27B / 5090:
4794        // k=3 51.2, k=4 51.8 with the per-op draft; the graph draft
4795        // halves the draft cost, so the extra draft is cheaper still).
4796        // 5 with the int8 verify (the default: measured 76.5 against
4797        // k=4's 72-74 and k=6's 74 on the 5090), 4 with the f32 one.
4798        #[cfg(target_os = "macos")]
4799        let metal_native = crate::gpu::q1_force();
4800        #[cfg(not(target_os = "macos"))]
4801        let metal_native = false;
4802        #[cfg(feature = "gpu")]
4803        let k_default = if metal_native {
4804            // the Metal verify's GEMM tile is 8 rows wide and flat in b:
4805            // seven drafts + the tip fill it for free
4806            7
4807        } else if crate::gpu_wgpu::verify_i8_on() {
4808            5
4809        } else {
4810            4
4811        };
4812        #[cfg(not(feature = "gpu"))]
4813        let k_default = 4;
4814        let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
4815            .ok()
4816            .and_then(|v| v.parse().ok())
4817            .filter(|&v| (1..=8).contains(&v))
4818            .unwrap_or(k_default);
4819        if next_pos == 0 {
4820            return None;
4821        }
4822        let t_round = std::time::Instant::now();
4823        // Submissions per phase — and they say where the round's money is.
4824        // Qwen3.6-27B on an RTX 5090, k=3:
4825        //
4826        //   draft   9.3 ms / 12 submissions   (four per MTP step)
4827        //   verify 52.8 ms /  1               (the batched graph)
4828        //   commit  5.4 ms /  6               (two per warm)
4829        //
4830        // The verify is already one submit. The draft's own work is 834 MB
4831        // a step — 0.8 ms at this card's measured 1056 GB/s — against 3.1
4832        // ms measured, so ~0.58 ms of every step is round trip, not
4833        // arithmetic, and the same holds for the warms. Eighteen round
4834        // trips a round at roughly half a millisecond each is ~11 ms of a
4835        // 68 ms round: fusing the MTP block into ONE submit the way the
4836        // trunk already is projects to ~64 tok/s against today's 50.9.
4837        // That is the largest measured item left on this path.
4838        let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
4839        let sub0 = subs();
4840        // Greedy without penalties verifies by argmax equality (bit-exact
4841        // against the plain path). Anything else is speculative SAMPLING:
4842        // each draft is a DRAW from the MTP head's post-chain distribution
4843        // q_j, kept for the accept test; the verify's rows give p_j.
4844        let cfg = self.sampler_config.clone();
4845        let penalized = !(cfg.repetition_penalty == 1.0
4846            && cfg.presence_penalty == 0.0
4847            && cfg.suppress_tokens.is_empty());
4848        // Three verify regimes: plain greedy (argmax of the raw rows),
4849        // greedy WITH penalties (argmax of the penalized rows — a single
4850        // pass each, no distributions), and sampling (draw / accept /
4851        // correct on post-chain distributions).
4852        let greedy_pen = cfg.temperature < 1e-6 && penalized;
4853        let sampling = cfg.temperature >= 1e-6;
4854        // Sampling with a top-k goes through the SPARSE chain: the dense
4855        // one builds nine 248k-float distributions a round (four drafts,
4856        // five verify rows) and measured 19-22 tok/s against a plain 40 —
4857        // the host, not the card. Sparse, the same nine cost tens of
4858        // microseconds each.
4859        let sparse = sampling && sampler::sparse_ok(&cfg);
4860        let base_len = all_ids.len();
4861        if sampling && !sparse && self.spec_q.len() < k_spec {
4862            self.spec_q.resize_with(k_spec, Vec::new);
4863        }
4864        if sparse && self.spec_qs.len() < k_spec {
4865            self.spec_qs.resize_with(k_spec, Vec::new);
4866        }
4867        // Draft the chain: first from the trunk's tip hidden, then the head
4868        // iterating on itself. Rows land in the MTP KV; the chain rows past
4869        // the first are speculation over speculative state and roll back
4870        // below, replaced by verified pairs.
4871        let mut drafts = Vec::with_capacity(k_spec);
4872        let mut hx = hidden.to_vec();
4873        // CMF_SPEC_DBG=1: draft 0 through BOTH MTP arms (graph and per-op)
4874        // from the same inputs — are the arms the difference, or the inputs?
4875        let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
4876        for j in 0..k_spec {
4877            let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
4878            let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
4879            if spec_dbg {
4880                let saved = self.mtp_graph_mode;
4881                self.mtp_graph_mode = Some(false);
4882                let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4883                self.mtp_graph_mode = saved;
4884                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4885                    return None;
4886                }
4887                m.kv.truncate_last(1);
4888                dbg_ref = Some(r);
4889            }
4890            let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4891            if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4892                return None;
4893            }
4894            if let Some((lg_cpu, h_cpu)) = dbg_ref {
4895                let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
4896                let dl = lg
4897                    .iter()
4898                    .zip(&lg_cpu)
4899                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4900                let dh = hj
4901                    .iter()
4902                    .zip(&h_cpu)
4903                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4904                eprintln!(
4905                    "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 {}",
4906                    next_pos - 1 + j,
4907                    sampler::argmax(&lg_cpu),
4908                    sampler::argmax(&lg),
4909                    n(&h_cpu),
4910                    n(&hj),
4911                    m.kv.seq_len
4912                );
4913            }
4914            let dj = if sparse {
4915                let mut q = std::mem::take(&mut self.spec_qs[j]);
4916                let ok = sampler::sparse_distribution_into(
4917                    &lg,
4918                    &cfg,
4919                    all_ids,
4920                    &mut self.sampler_scratch,
4921                    self.pool.as_deref(),
4922                    &mut q,
4923                );
4924                let d = if ok {
4925                    sampler::draw_sparse(&q, &mut self.rng)
4926                } else {
4927                    // everything filtered: the dense chain's greedy fallback
4928                    let t = sampler::argmax(&lg);
4929                    q.clear();
4930                    q.push((t, 1.0));
4931                    t
4932                };
4933                self.spec_qs[j] = q;
4934                all_ids.push(d);
4935                d
4936            } else if sampling {
4937                let mut q = std::mem::take(&mut self.spec_q[j]);
4938                sampler::distribution_into(
4939                    &lg,
4940                    &cfg,
4941                    all_ids,
4942                    &mut self.sampler_scratch,
4943                    self.pool.as_deref(),
4944                    &mut q,
4945                );
4946                let d = sampler::draw(&q, &mut self.rng);
4947                self.spec_q[j] = q;
4948                all_ids.push(d); // the next draft's penalties see this one
4949                d
4950            } else if greedy_pen {
4951                let d = sampler::argmax_penalized(
4952                    &lg,
4953                    &cfg,
4954                    all_ids,
4955                    &mut self.sampler_scratch,
4956                    self.pool.as_deref(),
4957                );
4958                all_ids.push(d);
4959                d
4960            } else {
4961                sampler::argmax(&lg)
4962            };
4963            attention::recycle_buf(&mut lg);
4964            drafts.push(dj);
4965            hx = hj;
4966        }
4967        all_ids.truncate(base_len);
4968        *drafted += k_spec;
4969        let t_draft = t_round.elapsed();
4970        let sub_draft = subs();
4971        // Verify batch: [t_next, d1 .. d_{k-1}] at next_pos.. — every row's
4972        // logits come back from the graph's own head.
4973        let b = k_spec + 1;
4974        let mut hiddens = vec![0.0f32; b * self.hidden_size];
4975        for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
4976            let e = self.embed_single(t);
4977            hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
4978        }
4979        let positions: Vec<usize> = (next_pos..next_pos + b).collect();
4980        let (lm_gw, lm_rows) = {
4981            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4982            (
4983                crate::gpu::GraphW {
4984                    idx: i,
4985                    kind,
4986                    row_scale: rs,
4987                    data: &[],
4988                    prism: crate::gpu::GraphPrismOp::None,
4989                    affine: false,
4990                },
4991                self.weights.lm_head.rows(),
4992            )
4993        };
4994        let mut logits = Vec::new();
4995        let final_norm = self.weights.final_norm.clone();
4996        #[cfg(target_os = "macos")]
4997        let verify_outcome = if metal_native {
4998            let lm = self.weights.lm_head.q1_parts()?;
4999            self.try_batch_graph_metal(
5000                &mut hiddens,
5001                &positions,
5002                b,
5003                Some((lm, &final_norm, &mut logits)),
5004            )
5005        } else {
5006            self.try_batch_graph_wgpu(
5007                &mut hiddens,
5008                &positions,
5009                b,
5010                Some(crate::gpu::SpecTail {
5011                    lm: lm_gw,
5012                    lm_rows,
5013                    final_norm: &final_norm,
5014                    logits_out: &mut logits,
5015                }),
5016            )
5017        };
5018        #[cfg(not(target_os = "macos"))]
5019        let verify_outcome = self.try_batch_graph_wgpu(
5020            &mut hiddens,
5021            &positions,
5022            b,
5023            Some(crate::gpu::SpecTail {
5024                lm: lm_gw,
5025                lm_rows,
5026                final_norm: &final_norm,
5027                logits_out: &mut logits,
5028            }),
5029        );
5030        match verify_outcome {
5031            crate::gpu::BatchGraphOutcome::Completed => {}
5032            crate::gpu::BatchGraphOutcome::Declined => {
5033                // The verifier refused before admission.  Its draft MTP
5034                // rows are still device-resident, so rewind the separate
5035                // mirror before the caller takes the exact one-token path.
5036                m.kv.truncate_last(k_spec);
5037                if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
5038                    self.clear_sequence_state();
5039                    self.graph_failed
5040                        .store(true, std::sync::atomic::Ordering::Relaxed);
5041                    self.cancel
5042                        .store(true, std::sync::atomic::Ordering::Relaxed);
5043                    tracing::error!("MTP graph mirror rewind failed after verify decline");
5044                }
5045                return None;
5046            }
5047            crate::gpu::BatchGraphOutcome::Failed => {
5048                // A failed batch may have advanced trunk/GDN state.  Clear
5049                // both mirrors and preserve the terminal outcome rather than
5050                // falling through to stale CPU state.
5051                self.clear_sequence_state();
5052                self.graph_failed
5053                    .store(true, std::sync::atomic::Ordering::Relaxed);
5054                self.cancel
5055                    .store(true, std::sync::atomic::Ordering::Relaxed);
5056                tracing::error!("MTP verify batch graph failed after admission");
5057                return None;
5058            }
5059        }
5060        // `CMF_METAL_VERIFY_CHECK=1`: run the same b tokens through the
5061        // plain per-token path and compare each row's argmax + logits with
5062        // the verify's — the bring-up oracle for the batched graph. The
5063        // plain forwards mutate the CPU state; it is snapshotted and put
5064        // back, and the K/V mirrors re-pointed, before the round goes on.
5065        #[cfg(target_os = "macos")]
5066        if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
5067            let snap: Vec<Vec<f32>> = self
5068                .kv_cache
5069                .layers
5070                .iter()
5071                .map(|l| l.linear_state.clone())
5072                .collect();
5073            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5074            let toks: Vec<u32> = std::iter::once(t_next)
5075                .chain(drafts.iter().copied())
5076                .collect();
5077            let want_save = self.graph_want_logits;
5078            self.graph_want_logits = false;
5079            for (i, &t) in toks.iter().enumerate() {
5080                let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5081                let _ = self.graph_logits.take();
5082                // CMF_SPEC_PLAIN_HIDDEN=1: the next round drafts from the
5083                // plain path's hidden instead of the verify's (an experiment
5084                // on the chain's sensitivity to the half-GEMM noise)
5085                if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
5086                    hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
5087                }
5088                let ref_lg = self.logits_from_hidden(&hi);
5089                let row = &logits[i * lm_rows..(i + 1) * lm_rows];
5090                let ra = sampler::argmax(&ref_lg);
5091                let va = sampler::argmax(row);
5092                let mut md = 0f32;
5093                let mut rms = 0f64;
5094                for j in 0..lm_rows.min(ref_lg.len()) {
5095                    let d = (ref_lg[j] - row[j]).abs();
5096                    md = md.max(d);
5097                    rms += (d as f64) * (d as f64);
5098                }
5099                let mut hd = 0f32;
5100                for j in 0..self.hidden_size {
5101                    hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
5102                }
5103                eprintln!(
5104                    "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
5105                    next_pos + i,
5106                    if ra == va { "OK" } else { "MISMATCH" },
5107                    (rms / lm_rows as f64).sqrt()
5108                );
5109            }
5110            self.graph_want_logits = want_save;
5111            // restore IN PLACE: the pending verify graph wraps these very
5112            // allocations (zero-copy) — replacing the Vec would strand it
5113            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5114                if l.linear_state.len() == st.len() {
5115                    l.linear_state.copy_from_slice(&st);
5116                } else {
5117                    l.linear_state = st;
5118                }
5119            }
5120            for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
5121                let extra = l.seq_len.saturating_sub(n0);
5122                if extra > 0 {
5123                    l.truncate_last(extra);
5124                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
5125                }
5126            }
5127        }
5128        let t_verify = t_round.elapsed();
5129        let sub_verify = subs();
5130        // Acceptance. Greedy: row i's argmax is the trunk's token after
5131        // input i. Sampling: accept draft i with min(1, p_i/q_i), and on
5132        // the first rejection draw the correction from max(0, p_i − q_i)
5133        // — that token is committed by the loop top as-is (spec_forced).
5134        let mut a = 0usize;
5135        let mut forced: Option<u32> = None;
5136        let ids: Vec<u32> = if sparse {
5137            let mut p = std::mem::take(&mut self.spec_ps);
5138            let mut res = std::mem::take(&mut self.spec_ress);
5139            while a < k_spec {
5140                let ok = sampler::sparse_distribution_into(
5141                    &logits[a * lm_rows..(a + 1) * lm_rows],
5142                    &cfg,
5143                    all_ids,
5144                    &mut self.sampler_scratch,
5145                    self.pool.as_deref(),
5146                    &mut p,
5147                );
5148                if !ok {
5149                    let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
5150                    p.clear();
5151                    p.push((t, 1.0));
5152                }
5153                match sampler::spec_accept_or_correct_sparse(
5154                    &p,
5155                    &self.spec_qs[a],
5156                    drafts[a],
5157                    &mut self.rng,
5158                    &mut res,
5159                ) {
5160                    None => {
5161                        all_ids.push(drafts[a]);
5162                        a += 1;
5163                    }
5164                    Some(c) => {
5165                        forced = Some(c);
5166                        break;
5167                    }
5168                }
5169            }
5170            all_ids.truncate(base_len);
5171            self.spec_ps = p;
5172            self.spec_ress = res;
5173            drafts.clone()
5174        } else if sampling {
5175            let mut p = std::mem::take(&mut self.spec_p);
5176            let mut res = std::mem::take(&mut self.spec_res);
5177            while a < k_spec {
5178                sampler::distribution_into(
5179                    &logits[a * lm_rows..(a + 1) * lm_rows],
5180                    &cfg,
5181                    all_ids,
5182                    &mut self.sampler_scratch,
5183                    self.pool.as_deref(),
5184                    &mut p,
5185                );
5186                match sampler::spec_accept_or_correct(
5187                    &p,
5188                    &self.spec_q[a],
5189                    drafts[a],
5190                    &mut self.rng,
5191                    &mut res,
5192                    self.pool.as_deref(),
5193                ) {
5194                    None => {
5195                        all_ids.push(drafts[a]);
5196                        a += 1;
5197                    }
5198                    Some(c) => {
5199                        forced = Some(c);
5200                        break;
5201                    }
5202                }
5203            }
5204            all_ids.truncate(base_len);
5205            self.spec_p = p;
5206            self.spec_res = res;
5207            // the accepted drafts ARE the verified tokens after inputs 0..a
5208            drafts.clone()
5209        } else if greedy_pen {
5210            // Row i's penalized argmax, penalties over the stream that
5211            // includes the accepted drafts before it — the plain loop's
5212            // exact arithmetic, one pass per row, no working copy.
5213            let mut ids: Vec<u32> = Vec::with_capacity(b);
5214            for i in 0..b {
5215                let t = sampler::argmax_penalized(
5216                    &logits[i * lm_rows..(i + 1) * lm_rows],
5217                    &cfg,
5218                    all_ids,
5219                    &mut self.sampler_scratch,
5220                    self.pool.as_deref(),
5221                );
5222                ids.push(t);
5223                if i < k_spec && t == drafts[i] {
5224                    all_ids.push(t);
5225                } else {
5226                    break;
5227                }
5228            }
5229            all_ids.truncate(base_len);
5230            while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
5231                a += 1;
5232            }
5233            // rows past the first mismatch were never scored; the loop
5234            // top re-samples the last verified row itself.
5235            ids
5236        } else {
5237            let ids: Vec<u32> = (0..b)
5238                .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
5239                .collect();
5240            while a < k_spec && ids[a] == drafts[a] {
5241                a += 1;
5242            }
5243            ids
5244        };
5245        if spec_dbg {
5246            eprintln!(
5247                "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
5248                drafts, ids
5249            );
5250        }
5251        // CMF_METAL_VERIFY_CHECK=2: the commit oracle — plain-forward the
5252        // a+1 accepted tokens from a snapshot, then diff the replayed GDN
5253        // states and the appended K/V rows against that.
5254        #[cfg(target_os = "macos")]
5255        let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
5256            && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
5257        {
5258            let snap: Vec<Vec<f32>> = self
5259                .kv_cache
5260                .layers
5261                .iter()
5262                .map(|l| l.linear_state.clone())
5263                .collect();
5264            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5265            let toks: Vec<u32> = std::iter::once(t_next)
5266                .chain(drafts.iter().copied())
5267                .collect();
5268            let want_save = self.graph_want_logits;
5269            self.graph_want_logits = false;
5270            for (i, &t) in toks.iter().take(a + 1).enumerate() {
5271                let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5272                let _ = self.graph_logits.take();
5273            }
5274            self.graph_want_logits = want_save;
5275            let plain_states: Vec<Vec<f32>> = self
5276                .kv_cache
5277                .layers
5278                .iter()
5279                .map(|l| l.linear_state.clone())
5280                .collect();
5281            let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5282            let mut rows = Vec::new();
5283            for (li, (l, n0)) in self
5284                .kv_cache
5285                .layers
5286                .iter_mut()
5287                .zip(attn_lens.iter())
5288                .enumerate()
5289            {
5290                let extra = l.seq_len.saturating_sub(*n0);
5291                if extra > 0 {
5292                    let mut kk = Vec::new();
5293                    let mut vv = Vec::new();
5294                    for g in 0..nkv {
5295                        kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5296                        vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5297                    }
5298                    rows.push((li, kk, vv));
5299                    l.truncate_last(extra);
5300                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
5301                }
5302            }
5303            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5304                if l.linear_state.len() == st.len() {
5305                    l.linear_state.copy_from_slice(&st);
5306                } else {
5307                    l.linear_state = st;
5308                }
5309            }
5310            Some((plain_states, rows))
5311        } else {
5312            None
5313        };
5314        // a fully-accepted round needs no restore: every input was real.
5315        #[cfg(target_os = "macos")]
5316        if metal_native {
5317            // the Metal verify never wrote its states: the commit replays the
5318            // accepted prefix into the CPU owners and appends the K/V rows
5319            if !self.metal_verify_commit(a) {
5320                self.clear_sequence_state();
5321                self.graph_failed
5322                    .store(true, std::sync::atomic::Ordering::Relaxed);
5323                self.cancel
5324                    .store(true, std::sync::atomic::Ordering::Relaxed);
5325                tracing::error!("Metal verify state/KV handoff failed after admission");
5326                return None;
5327            }
5328            if let Some((plain_states, rows)) = commit_ref {
5329                crate::gpu_metal::queue_fence();
5330                let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5331                let mut worst_s = 0f32;
5332                let mut worst_li = 0usize;
5333                for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
5334                    if l.linear_state.len() != ps.len() || ps.is_empty() {
5335                        continue;
5336                    }
5337                    let d = l
5338                        .linear_state
5339                        .iter()
5340                        .zip(ps)
5341                        .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5342                    let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
5343                    let rel = d / n.max(1e-6);
5344                    if rel > worst_s {
5345                        worst_s = rel;
5346                        worst_li = li;
5347                    }
5348                }
5349                let mut worst_k = 0f32;
5350                for (li, kk, vv) in &rows {
5351                    let l = &self.kv_cache.layers[*li];
5352                    let n0 = l.seq_len - (kk.len() / (nkv * hd));
5353                    let mut ck = Vec::new();
5354                    let mut cv = Vec::new();
5355                    for g in 0..nkv {
5356                        ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5357                        cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5358                    }
5359                    if ck.len() == kk.len() {
5360                        let dk = ck
5361                            .iter()
5362                            .zip(kk)
5363                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5364                        let dv = cv
5365                            .iter()
5366                            .zip(vv)
5367                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5368                        worst_k = worst_k.max(dk).max(dv);
5369                    } else {
5370                        eprintln!(
5371                            "commit-check L{li}: kv row count mismatch {} vs {}",
5372                            ck.len(),
5373                            kk.len()
5374                        );
5375                    }
5376                }
5377                eprintln!(
5378                    "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}"
5379                );
5380            }
5381        }
5382        if !metal_native && a + 1 < b {
5383            let expected_gdn_layers = self.graph_gdn_layer_count();
5384            if expected_gdn_layers > 0
5385                && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
5386            {
5387                self.clear_sequence_state();
5388                self.graph_failed
5389                    .store(true, std::sync::atomic::Ordering::Relaxed);
5390                self.cancel
5391                    .store(true, std::sync::atomic::Ordering::Relaxed);
5392                tracing::error!("GDN speculative restore failed after verify");
5393                return None;
5394            }
5395        }
5396        if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
5397            // The verify graph committed the full batch, but one of its
5398            // persistent Full-attention mirrors could not be re-pointed to
5399            // the accepted prefix.  Treat that as terminal state failure;
5400            // an exact CPU fallback would otherwise consume stale GDN/KV.
5401            self.clear_sequence_state();
5402            self.graph_failed
5403                .store(true, std::sync::atomic::Ordering::Relaxed);
5404            self.cancel
5405                .store(true, std::sync::atomic::Ordering::Relaxed);
5406            tracing::error!("trunk graph KV rewind failed after speculative verify");
5407            return None;
5408        }
5409        *accepted += a;
5410        // MTP cache: keep the first draft row (its inputs were real), drop
5411        // the chain's, then append the verified pairs the round produced.
5412        // Each of those is a whole MTP block on the per-op path and they
5413        // cost 5.8 ms of a 69 ms round at k=3 — a third of what the
5414        // round's own draft costs. PRICED, and they earn it: skipping
5415        // them (`CMF_SPEC_WARM=0`) drops acceptance from 89% to 81% at
5416        // k=3 and 85% to 74% at k=4, and the tok/s goes nowhere at k=3
5417        // (50.3 against 50.5) and backwards at k=4 (48.1 against 50.1).
5418        // The knob stays so the next person can re-price it after the
5419        // warms are batched instead of assuming either way.
5420        m.kv.truncate_last(k_spec.saturating_sub(1));
5421        #[cfg(target_os = "macos")]
5422        if metal_native && self.mtp_graph_mode == Some(true) {
5423            // the mirror rows below the cut are the CPU rows: re-point,
5424            // no re-upload
5425            crate::gpu_metal::kv_mirror_set_stored(
5426                self.mtp_kv_id(),
5427                Self::MTP_LAYER_BASE,
5428                m.kv.seq_len,
5429            );
5430        }
5431        if !metal_native
5432            && self.mtp_graph_mode == Some(true)
5433            && !self.rewind_mtp_graph_mirror(next_pos)
5434        {
5435            // The graph draft was admitted, so inability to move its cursor
5436            // back to the real anchor is a state failure, not a capability
5437            // refusal.  Do not warm or continue with a stale mirror.
5438            self.clear_sequence_state();
5439            self.graph_failed
5440                .store(true, std::sync::atomic::Ordering::Relaxed);
5441            self.cancel
5442                .store(true, std::sync::atomic::Ordering::Relaxed);
5443            tracing::error!("MTP graph mirror rewind failed after verify commit");
5444            return None;
5445        }
5446        let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
5447        if !warm_off && a > 0 {
5448            // Graph arm: all accepted pairs in ONE batched run over the
5449            // MTP block; the token graph one by one if the batch declines.
5450            let mut warmed = false;
5451            #[cfg(target_os = "macos")]
5452            if metal_native && self.mtp_graph_mode == Some(true) {
5453                // all accepted pairs in ONE b-row graph run over the MTP
5454                // block (its input projection folded in); one by one on
5455                // the token graph if that declines
5456                let pairs: Vec<(&[f32], u32)> = (0..a)
5457                    .map(|j| {
5458                        (
5459                            &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
5460                            ids[j],
5461                        )
5462                    })
5463                    .collect();
5464                warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
5465                if !warmed {
5466                    warmed = true;
5467                    for j in 0..a {
5468                        let row =
5469                            hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
5470                        if self
5471                            .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
5472                            .is_none()
5473                        {
5474                            warmed = false;
5475                            break;
5476                        }
5477                    }
5478                }
5479            }
5480            if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
5481                let rows: Vec<Vec<f32>> = (0..a)
5482                    .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
5483                    .collect();
5484                let pairs: Vec<(&[f32], u32)> = rows
5485                    .iter()
5486                    .zip(ids.iter())
5487                    .map(|(r, &t)| (r.as_slice(), t))
5488                    .collect();
5489                match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
5490                    Ok(()) => warmed = true,
5491                    Err(err) => {
5492                        // A warm-up failure after graph admission cannot
5493                        // fall back to `mtp_warm`: the detached CPU cache is
5494                        // not authoritative for the device mirror.  Mark it
5495                        // terminal so the generation caller clears state and
5496                        // returns instead of drafting from stale attention.
5497                        tracing::error!("{err}");
5498                        self.clear_sequence_state();
5499                        self.graph_failed
5500                            .store(true, std::sync::atomic::Ordering::Relaxed);
5501                        self.cancel
5502                            .store(true, std::sync::atomic::Ordering::Relaxed);
5503                        return None;
5504                    }
5505                }
5506            }
5507            if !warmed {
5508                for j in 0..a {
5509                    let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
5510                    let row = row.to_vec();
5511                    self.mtp_warm(m, &row, ids[j], next_pos + j);
5512                }
5513            }
5514        }
5515        // The sampler's contract: logits of the LAST verified position —
5516        // unless a rejected draft already drew the correction, in which
5517        // case the loop top commits that token and samples nothing.
5518        if let Some(c) = forced {
5519            self.spec_forced = Some(c);
5520            self.graph_logits = None;
5521        } else {
5522            let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
5523            row.resize(self.vocab_size, 0.0);
5524            if let Some(c) = self.final_softcap {
5525                for l in row.iter_mut() {
5526                    *l = c * (*l / c).tanh();
5527                }
5528            }
5529            self.graph_logits = Some(row);
5530        }
5531        let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
5532        // Three phases, not two. The round's wall clock was 4 ms longer
5533        // than draft+verify and the difference had nowhere to be seen:
5534        // the accepted prefix re-runs the MTP block once per token to
5535        // keep the draft head's attention cache warm, and the GDN state
5536        // rolls back on any rejection. Both live here, after the verify.
5537        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
5538            let end = subs();
5539            eprintln!(
5540                "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
5541                 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
5542                t_draft.as_secs_f64() * 1e3,
5543                sub_draft - sub0,
5544                (t_verify - t_draft).as_secs_f64() * 1e3,
5545                sub_verify - sub_draft,
5546                (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
5547                end - sub_verify,
5548            );
5549        }
5550        Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
5551    }
5552
5553    /// Micro-benchmark: two single-position forwards vs one fused pair
5554    /// from the current cache state (KV rewound after each probe).
5555    /// Returns (two_singles_ms, fused_pair_ms) per probe, or the (0, 0)
5556    /// sentinel when this model has no pair path to measure — the same
5557    /// answer the o1 arm gives, and the bench prints it the same way.
5558    /// (An architecture that loads its own layers leaves `weights.layers`
5559    /// empty; walking it here was an index panic, found by `bench` on
5560    /// deepseek_v4.)
5561    pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
5562        if !self.pair_supported() {
5563            return (0.0, 0.0);
5564        }
5565        // This is a host-side pair micro-benchmark. It truncates the host KV
5566        // after every probe, so letting the whole-token graph participate
5567        // would leave its device GDN/KV mirror ahead of the next probe and
5568        // poison the process-wide graph verdict before the real generation
5569        // benchmark starts. Keep the existing per-op/GPU arithmetic while
5570        // suppressing only the stateful token graph for this measurement.
5571        let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
5572        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5573        let emb1 = self.embed_single(1);
5574        let emb2 = self.embed_single(2);
5575        let pos = self.kv_cache.seq_len();
5576
5577        let t0 = std::time::Instant::now();
5578        for _ in 0..iters {
5579            let _ = self.forward_layers(&emb1, pos, None);
5580            let _ = self.forward_layers(&emb2, pos + 1, None);
5581            for l in &mut self.kv_cache.layers {
5582                l.truncate_last(2);
5583            }
5584        }
5585        let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5586
5587        let t1 = std::time::Instant::now();
5588        for _ in 0..iters {
5589            let _ = self.forward_pair(&emb1, &emb2, pos);
5590            for l in &mut self.kv_cache.layers {
5591                l.truncate_last(2);
5592            }
5593        }
5594        let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5595        match graph_env {
5596            Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
5597            None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
5598        }
5599        (singles_ms, pair_ms)
5600    }
5601
5602    /// Fused two-position forward: weight rows are streamed from memory
5603    /// once per layer for both positions. Full layers → fused GQA pair;
5604    /// linear layers → vmf_phase pair (lane 2 state is tentative in the
5605    /// per-layer scratch until the draft is accepted).
5606    /// Whether the fused two-position path covers every layer kind in
5607    /// this model. MLA and KDA run per position (their pair arms are
5608    /// unreachable); the seq prefill falls back to singles for them.
5609    fn pair_supported(&self) -> bool {
5610        // An EMPTY layer stack means the architecture loaded its own and
5611        // this path has nothing to walk. Checking that directly, rather
5612        // than naming each such architecture, is what makes the guard hold
5613        // for the next one: `any()` over no layers is false, so a
5614        // feature-by-feature test says "supported" for a model that has no
5615        // layers here at all.
5616        !self.weights.layers.is_empty()
5617            && self.g3n.is_none()
5618            && !self
5619                .weights
5620                .layers
5621                .iter()
5622                .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
5623    }
5624
5625    fn forward_pair(
5626        &mut self,
5627        emb1: &[f32],
5628        emb2: &[f32],
5629        position: usize,
5630    ) -> (Vec<f32>, Vec<f32>) {
5631        let mut h1 = emb1.to_vec();
5632        let mut h2 = emb2.to_vec();
5633        let (_nkv, _hd, hs, _rd, eps) = (
5634            self.num_kv_heads,
5635            self.head_dim,
5636            self.hidden_size,
5637            self.rotary_dim,
5638            self.rms_eps,
5639        );
5640        let pool = self.pool.clone();
5641
5642        for li in 0..self.num_layers {
5643            let lw = &self.weights.layers[self.phys_layer(li)];
5644            // Norms into pipeline scratch (4 allocs/layer on the MTP
5645            // decode hot path before this).
5646            inference::rms_norm_into(
5647                &h1,
5648                &lw.input_norm,
5649                self.rms_eps,
5650                self.norm_style,
5651                &mut self.ws.n1,
5652            );
5653            inference::rms_norm_into(
5654                &h2,
5655                &lw.input_norm,
5656                self.rms_eps,
5657                self.norm_style,
5658                &mut self.ws.n2,
5659            );
5660
5661            let (a1, a2) = match &lw.attn {
5662                AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5663                AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5664                AttnKind::Linear(w) => {
5665                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5666                    let layer = &mut self.kv_cache.layers[li];
5667                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5668                    vmf_phase_pair(
5669                        &self.ws.n1,
5670                        &self.ws.n2,
5671                        w,
5672                        &cfg,
5673                        state,
5674                        scratch,
5675                        self.pool.as_deref(),
5676                    )
5677                }
5678                AttnKind::LinearGdn(w) => {
5679                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5680                    let layer = &mut self.kv_cache.layers[li];
5681                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5682                    gdn_pair(
5683                        &self.ws.n1,
5684                        &self.ws.n2,
5685                        w,
5686                        &cfg,
5687                        state,
5688                        scratch,
5689                        self.pool.as_deref(),
5690                    )
5691                }
5692                AttnKind::ShortConv(w) => {
5693                    let cfg = self
5694                        .short_conv_cfg
5695                        .expect("short-conv layer without short_conv_cfg");
5696                    let layer = &mut self.kv_cache.layers[li];
5697                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5698                    short_conv_pair(
5699                        &self.ws.n1,
5700                        &self.ws.n2,
5701                        w,
5702                        &cfg,
5703                        state,
5704                        scratch,
5705                        self.pool.as_deref(),
5706                    )
5707                }
5708                AttnKind::Full {
5709                    wq,
5710                    wk,
5711                    wv,
5712                    wo,
5713                    q_norm,
5714                    k_norm,
5715                    output_gate,
5716                    softplus_gate,
5717                    bias,
5718                } => {
5719                    let inv_freq_l = self.layer_inv_freq(li);
5720                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5721                    let cfg = QwenAttnCfg {
5722                        num_heads: self.layer_num_heads(li),
5723                        num_kv_heads: nkv_l,
5724                        head_dim: hd_l,
5725                        hidden_size: hs,
5726                        position,
5727                        inv_freq: &inv_freq_l,
5728                        rotary_dim: rd_l,
5729                        scale: self.attn_scale,
5730                        softcap: self.attn_softcap,
5731                        window: self.layer_window(li),
5732                        v_norm: self.attn_v_norm,
5733                        qk_norm_after_rope: self.qk_norm_after_rope,
5734                        q_norm: q_norm.as_deref(),
5735                        k_norm: k_norm.as_deref(),
5736                        output_gate: *output_gate,
5737                        softplus_gate: softplus_gate
5738                            .as_ref()
5739                            .map(|(gate, per_head)| (gate, *per_head)),
5740                        rope_scale: self.layer_rope_scale(li),
5741                        bias: bias
5742                            .as_ref()
5743                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5744                        rms_eps: eps,
5745                        norm_style: self.norm_style,
5746                        pool: pool.as_deref(),
5747                    };
5748                    attention::qwen_attention_pair(
5749                        &self.ws.n1,
5750                        &self.ws.n2,
5751                        wq,
5752                        wk,
5753                        wv,
5754                        wo,
5755                        &mut self.kv_cache.layers[li],
5756                        &cfg,
5757                    )
5758                }
5759            };
5760            let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5761                Some(w) => (
5762                    inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
5763                    inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
5764                ),
5765                None => (a1, a2),
5766            };
5767            for i in 0..self.hidden_size {
5768                h1[i] += a1[i];
5769                h2[i] += a2[i];
5770            }
5771            let (mut a1, mut a2) = (a1, a2);
5772            attention::recycle_buf(&mut a1);
5773            attention::recycle_buf(&mut a2);
5774
5775            let lw = &self.weights.layers[self.phys_layer(li)];
5776            inference::rms_norm_into(
5777                &h1,
5778                &lw.post_norm,
5779                self.rms_eps,
5780                self.norm_style,
5781                &mut self.ws.p1,
5782            );
5783            inference::rms_norm_into(
5784                &h2,
5785                &lw.post_norm,
5786                self.rms_eps,
5787                self.norm_style,
5788                &mut self.ws.p2,
5789            );
5790            let (f1, f2) = match &lw.ffn {
5791                // Dual-branch layers need the raw residuals — run the
5792                // two positions through the same fn decode uses.
5793                FfnKind::DenseMoe(dm) => (
5794                    dense_moe_ffn(
5795                        dm,
5796                        &self.ws.p1,
5797                        &h1,
5798                        self.rms_eps,
5799                        self.norm_style,
5800                        self.pool.as_deref(),
5801                    ),
5802                    dense_moe_ffn(
5803                        dm,
5804                        &self.ws.p2,
5805                        &h2,
5806                        self.rms_eps,
5807                        self.norm_style,
5808                        self.pool.as_deref(),
5809                    ),
5810                ),
5811                _ => ffn_forward_pair(
5812                    &lw.ffn,
5813                    &self.ws.p1,
5814                    &self.ws.p2,
5815                    self.pool.as_deref(),
5816                    None,
5817                ),
5818            };
5819            let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5820                Some(w) => (
5821                    inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
5822                    inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
5823                ),
5824                None => (f1, f2),
5825            };
5826            for i in 0..self.hidden_size {
5827                h1[i] += f1[i];
5828                h2[i] += f2[i];
5829            }
5830            let (mut f1, mut f2) = (f1, f2);
5831            attention::recycle_buf(&mut f1);
5832            attention::recycle_buf(&mut f2);
5833            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5834                for i in 0..self.hidden_size {
5835                    h1[i] *= sc;
5836                    h2[i] *= sc;
5837                }
5838            }
5839            // Looped Transformer: apply final norm at the end of each loop iteration.
5840            if self.is_loop_end(li) && li + 1 < self.num_layers {
5841                h1 = inference::rms_norm(
5842                    &h1,
5843                    &self.weights.final_norm,
5844                    self.rms_eps,
5845                    self.norm_style,
5846                );
5847                h2 = inference::rms_norm(
5848                    &h2,
5849                    &self.weights.final_norm,
5850                    self.rms_eps,
5851                    self.norm_style,
5852                );
5853            }
5854        }
5855        // Real O(1) prefill pairs may also carry tentative lane-2 recurrent
5856        // state. Commit it before publishing the transition epoch so the
5857        // next serial/device row cannot observe a new attention epoch with an
5858        // old GDN state. Speculative pairs run only when O(1) is inactive and
5859        // retain their existing caller-controlled commit/rollback semantics.
5860        if self.o1_active() {
5861            self.commit_linear_scratch();
5862        }
5863        self.o1_progress();
5864        (h1, h2)
5865    }
5866
5867    /// Commit lane-2 linear states after an accepted draft.
5868    fn commit_linear_scratch(&mut self) {
5869        for layer in &mut self.kv_cache.layers {
5870            if !layer.linear_scratch.is_empty() {
5871                std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
5872                layer.linear_scratch.clear();
5873            }
5874        }
5875    }
5876
5877    /// Forward a full id sequence from a fresh cache and return the
5878    /// logits after the last position (golden-parity harness, bench).
5879    pub fn forward_ids(
5880        &mut self,
5881        ids: &[u32],
5882        task_mask: Option<&TaskMask>,
5883    ) -> Result<Vec<f32>, String> {
5884        if ids.is_empty() {
5885            return Err("empty id sequence".to_string());
5886        }
5887        self.clear_sequence_state();
5888        self.check_forward_graph("forward_ids setup", 0)?;
5889        if task_mask.is_none() {
5890            self.o1_begin();
5891        }
5892        let mut hidden = vec![0.0f32; self.hidden_size];
5893        let mut pos = 0usize;
5894        if let Some(b) = &mut self.dsv41 {
5895            let pool = self.pool.clone();
5896            let mut logits = Vec::new();
5897            crate::dsv41::forward_chunk(
5898                &b.0,
5899                &b.1,
5900                &b.2,
5901                &mut b.3,
5902                ids,
5903                0,
5904                pool.as_deref(),
5905                &mut logits,
5906            );
5907            if let Err(err) = self.o1_seal_checked() {
5908                self.clear_sequence_state();
5909                return Err(err);
5910            }
5911            return Ok(logits);
5912        }
5913        // Same routing predicate generation uses. Two reasons it must be
5914        // the same one: (1) a GDN hybrid's recurrent state is GPU-
5915        // resident, and a batched CPU prefill would build it on the host
5916        // only — decode then reads buffers the prefill never wrote;
5917        // (2) bench times THIS function and calls the result "prefill",
5918        // so a different path here reports a number production never
5919        // sees (W2 on 2×5090: 8.7 tok/s reported against 125 real).
5920        if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
5921            // prefill-GEMM in chunks; only the last position's hidden is
5922            // needed. (o1-compatible: the batch path attends per position
5923            // through qwen_attention, which carries the collection hook.)
5924            let chunk = prefill_chunk();
5925            let hs = self.hidden_size;
5926            while pos < ids.len() {
5927                let end = (pos + chunk).min(ids.len());
5928                let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
5929                self.check_forward_graph("forward_ids batched prefill", end - 1)?;
5930                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5931                pos = end;
5932            }
5933        }
5934        // Same guards as generation's prefill — INCLUDING the graph one.
5935        // The CPU pair walk was intercepting positions that the resident
5936        // token graph would have run itself: on a GDN hybrid over wgpu
5937        // that is 89 ms of host forward against 7 ms of device submit,
5938        // and it made prefill look 12× slower than it is (W2 on an RTX
5939        // 5090, ctx 512: 11.2 tok/s with the walk, 136.6 without).
5940        // CMF_PAIR=0 opts out; a model whose layers live outside
5941        // `weights.layers` has no pair walk to take.
5942        if task_mask.is_none()
5943            && !self.graph_prefill_preferred()
5944            && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
5945            && self.pair_supported()
5946        {
5947            while pos + 1 < ids.len() {
5948                let e1 = self.embed_single(ids[pos]);
5949                let e2 = self.embed_single(ids[pos + 1]);
5950                let (_, h2) = self.forward_pair(&e1, &e2, pos);
5951                self.check_forward_graph("forward_ids pair", pos + 1)?;
5952                self.commit_linear_scratch();
5953                hidden = h2;
5954                pos += 2;
5955            }
5956        }
5957        while pos < ids.len() {
5958            hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
5959            self.check_forward_graph("forward_ids", pos)?;
5960            pos += 1;
5961        }
5962        // Harness contract: after forward_ids the cache is decode-ready —
5963        // under o1 that means sealed (bench measures the seal as part of
5964        // prefill, honestly).
5965        if let Err(err) = self.o1_seal_checked() {
5966            self.clear_sequence_state();
5967            return Err(err);
5968        }
5969        let normed = inference::rms_norm(
5970            &hidden,
5971            &self.weights.final_norm,
5972            self.rms_eps,
5973            self.norm_style,
5974        );
5975        Ok(self.lm_head_forward(&normed))
5976    }
5977
5978    /// Run the V4.1 stack one token at a time and retain logits for every
5979    /// position. This is a diagnostic surface for comparing a converted
5980    /// checkpoint with a tokenwise reference implementation.
5981    #[doc(hidden)]
5982    pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
5983        #[cfg(target_os = "macos")]
5984        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
5985        if ids.is_empty() {
5986            return Err("empty id sequence".to_string());
5987        }
5988        self.clear_sequence_state();
5989        self.dsv41
5990            .as_ref()
5991            .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
5992        self.o1_begin();
5993        let rows = {
5994            let pool = self.pool.clone();
5995            let b = self
5996                .dsv41
5997                .as_mut()
5998                .expect("dsv41 checked above; state cannot change during forward");
5999            let mut rows = Vec::with_capacity(ids.len());
6000            for (position, &id) in ids.iter().enumerate() {
6001                let mut logits = Vec::new();
6002                crate::dsv41::forward_token(
6003                    &b.0,
6004                    &b.1,
6005                    &b.2,
6006                    &mut b.3,
6007                    id,
6008                    position,
6009                    pool.as_deref(),
6010                    &mut logits,
6011                );
6012                rows.push(logits);
6013            }
6014            rows
6015        };
6016        self.o1_seal();
6017        Ok(rows)
6018    }
6019
6020    /// Teacher-forced perplexity over a token sequence (phase-C gate:
6021    /// honest quant comparisons instead of prompt vibes).
6022    ///
6023    /// Attention is EXACT even on a model whose layers are flagged for
6024    /// the O(1) kernel — scoring the backbone is the default on purpose
6025    /// (it is the yardstick). `nll_ids_o1` scores the CONVERTED model.
6026    pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
6027        let (nll, cnt) = self.nll_ids_from(ids, 0)?;
6028        Ok((nll / cnt.max(1) as f64).exp())
6029    }
6030
6031    /// DTG-MA calibration pass (Patent 2): run `ids` through the model
6032    /// (CPU path, per position) and return each layer's per-neuron
6033    /// activation mass Σ|silu(gate)·up| — the statistic the task-guided
6034    /// FFN mask is derived from.
6035    pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
6036        self.clear_sequence_state();
6037        FFN_PROBE.with(|p| {
6038            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6039        });
6040        crate::gpu::cpu_scope(|| {
6041            for (pos, &id) in ids.iter().enumerate() {
6042                let emb = self.embed_single(id);
6043                let _ = self.forward_layers(&emb, pos, None);
6044            }
6045        });
6046        self.clear_sequence_state();
6047        FFN_PROBE
6048            .with(|p| p.borrow_mut().take())
6049            .unwrap_or_default()
6050    }
6051
6052    /// `probe_ffn_mass` over the BATCHED prefill: same accumulator, one
6053    /// sweep instead of one forward per token. What makes the statistic
6054    /// affordable on a 27B.
6055    pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
6056        if let Err(err) = self.nll_begin() {
6057            // A recorder can be left by a caller that was interrupted before
6058            // this request entered its scoring block.  Consume it even when
6059            // the preflight failure prevents initialization of a new one.
6060            let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
6061            self.nll_end();
6062            return Err(err);
6063        }
6064        FFN_PROBE.with(|p| {
6065            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6066        });
6067        let result: Result<(), String> = (|| {
6068            for chunk in ids.chunks(256) {
6069                if chunk.len() < 2 {
6070                    continue;
6071                }
6072                self.nll_ids_masked(chunk, 0, None)?;
6073            }
6074            Ok(())
6075        })();
6076        self.nll_end();
6077        let probe = FFN_PROBE
6078            .with(|p| p.borrow_mut().take())
6079            .unwrap_or_default();
6080        match result {
6081            Ok(()) => Ok(probe),
6082            Err(err) => {
6083                drop(probe);
6084                Err(err)
6085            }
6086        }
6087    }
6088
6089    /// Teacher-forced PPL with a task mask active (sparse execution) —
6090    /// the quality gate for a DTG-MA-masked skill. Sequential per
6091    /// position: the batched prefill path is dense-only.
6092    pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
6093        self.nll_begin()?;
6094        let result: Result<f64, String> = (|| {
6095            let mut nll = 0f64;
6096            let mut cnt = 0usize;
6097            let mut hidden = vec![0f32; self.hidden_size];
6098            for (pos, &id) in ids.iter().enumerate() {
6099                if pos > 0 {
6100                    inference::rms_norm_into(
6101                        &hidden,
6102                        &self.weights.final_norm,
6103                        self.rms_eps,
6104                        self.norm_style,
6105                        &mut self.ws.n1,
6106                    );
6107                    let mut logits = self.lm_head_forward(&self.ws.n1);
6108                    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
6109                    let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
6110                    let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
6111                    nll -= p.max(1e-300).ln();
6112                    cnt += 1;
6113                    attention::recycle_buf(&mut logits);
6114                }
6115                let emb = self.embed_single(id);
6116                hidden = self.forward_layers(&emb, pos, Some(mask));
6117                self.nll_check_graph("masked serial forward", pos)?;
6118                // Consume a possible graph logits side channel before the
6119                // next row.  Masked scoring normally disables that route,
6120                // but stale channel state must never survive a request.
6121                let _ = self.graph_logits.take();
6122            }
6123            Ok((nll / cnt.max(1) as f64).exp())
6124        })();
6125        self.nll_end();
6126        result
6127    }
6128
6129    /// Teacher-forced NLL sum + scored-token count over positions
6130    /// `start..len-1`, attention EXACT. Positions below `start` still
6131    /// run — they are the context — they are just not scored, so this
6132    /// pairs with `nll_ids_o1(ids, start)` over the very same tokens.
6133    ///
6134    /// Returning (nll, cnt) rather than a ppl is what lets a windowed
6135    /// caller combine windows before the exp, so every scored token
6136    /// weighs the same regardless of how the windows are cut.
6137    /// `nll_ids_from` with a task mask held active at every position.
6138    ///
6139    /// The batched prefill path does not thread masks, so this walks the
6140    /// per-position forward — slower, but it scores the file exactly the
6141    /// way `run --task` will serve it, which is the point of the gate
6142    /// that calls it. With `None` it defers to the fast path.
6143    /// Masked scoring rides the SAME batched sweep as unmasked scoring —
6144    /// the masked-inference fast path: `prefill_batch_masked` lands the
6145    /// per-visit FFN rows on the activations inside the fused arms. The
6146    /// per-position loop below remains only as the no-batch fallback.
6147    pub fn nll_ids_masked(
6148        &mut self,
6149        ids: &[u32],
6150        start: usize,
6151        task_mask: Option<&TaskMask>,
6152    ) -> Result<(f64, usize), String> {
6153        let task_mask = self.drop_open_mask(task_mask);
6154        self.nll_ids_inner(ids, start, task_mask)
6155    }
6156
6157    pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
6158        self.nll_ids_inner(ids, start, None)
6159    }
6160
6161    fn nll_ids_inner(
6162        &mut self,
6163        ids: &[u32],
6164        start: usize,
6165        task_mask: Option<&TaskMask>,
6166    ) -> Result<(f64, usize), String> {
6167        self.nll_begin()?;
6168        let result: Result<(f64, usize), String> = (|| {
6169            let mut nll = 0f64;
6170            let mut cnt = 0usize;
6171            // An unmasked quality run with the resident wgpu graph must score
6172            // the same stateful path used by generation.  The layer-major
6173            // GEMM prefill below is a valid CPU/GEMM oracle, but it seeds
6174            // neither the graph's device GDN state nor its device KV mirrors;
6175            // using it here would silently score a different execution.  Keep
6176            // masked scoring on the exact per-position path as before, and
6177            // let the serial arm below drive the graph-aware scorer.
6178            // Only native Metal has a fused graph lm_head contract.  Vulkan
6179            // and other graph backends may expose hidden state without the
6180            // optional logits side channel; preserve their established CPU
6181            // norm/head fallback instead of turning that valid route into a
6182            // hard missing-logits error.
6183            let (graph_quality, fused_head_quality) = nll_graph_policy(
6184                task_mask.is_none(),
6185                self.graph_prefill_preferred(),
6186                crate::gpu::q1_force(),
6187            );
6188            self.graph_head_required = fused_head_quality;
6189            self.graph_want_logits = fused_head_quality;
6190            #[cfg(target_os = "macos")]
6191            if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
6192                match self.nll_batch_metal(ids, start) {
6193                    MetalBatchNllOutcome::Completed(nll, count) => {
6194                        return Ok((nll, count));
6195                    }
6196                    MetalBatchNllOutcome::Declined => {}
6197                    MetalBatchNllOutcome::Failed(err) => return Err(err),
6198                }
6199            }
6200            if self.can_prefill_batched() && !graph_quality {
6201                // prefill-GEMM: layer-major position chunks, lm_head batched
6202                // (254MB lm_head read once per chunk, not per position).
6203                // The layer chunk is large (grouping positions by MoE experts
6204                // wins with size), lm_head in sub-blocks (logit buffer
6205                // 32×vocab ≈ 32MB instead of 128×).
6206                const CHUNK: usize = 128;
6207                const LM_SUB: usize = 32;
6208                let n = ids.len().saturating_sub(1);
6209                let hs = self.hidden_size;
6210                let rows = self.weights.lm_head.rows();
6211                let mut pos = 0usize;
6212                while pos < n {
6213                    let end = (pos + CHUNK).min(n);
6214                    let bsz = end - pos;
6215                    let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6216                    self.nll_check_graph("batched prefill", pos)?;
6217                    let mut k0 = 0usize;
6218                    while k0 < bsz {
6219                        let k1 = (k0 + LM_SUB).min(bsz);
6220                        let sb = k1 - k0;
6221                        // Sub-block entirely below the scored range: the KV
6222                        // it just built is all this pass needed from it.
6223                        if pos + k1 <= start {
6224                            k0 = k1;
6225                            continue;
6226                        }
6227                        let mut normed = vec![0.0f32; sb * hs];
6228                        for k in 0..sb {
6229                            let r = inference::rms_norm(
6230                                &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
6231                                &self.weights.final_norm,
6232                                self.rms_eps,
6233                                self.norm_style,
6234                            );
6235                            normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
6236                        }
6237                        let mut logits = vec![0.0f32; sb * rows];
6238                        self.weights
6239                            .lm_head
6240                            .matmat(&normed, sb, &mut logits, self.pool.as_deref());
6241                        for k in 0..sb {
6242                            if pos + k0 + k < start {
6243                                continue;
6244                            }
6245                            self.nll_check_graph("batched score row", pos + k0 + k)?;
6246                            let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
6247                            if let Some(mu) = self.logit_multiplier {
6248                                for v in lg.iter_mut() {
6249                                    *v *= mu;
6250                                }
6251                            }
6252                            // Gemma-class final-logit soft-capping: the
6253                            // decode paths apply it; scoring must too, or
6254                            // the uncapped softmax misprices every token.
6255                            if let Some(c) = self.final_softcap {
6256                                for v in lg.iter_mut() {
6257                                    *v = c * (*v / c).tanh();
6258                                }
6259                            }
6260                            // Cortiq Embryo hierarchical head: same correction
6261                            // the decode path applies (lm_head_forward).
6262                            if let Some(cm) = self.head_clusters.clone() {
6263                                self.hierarchical_head_logprobs(
6264                                    &normed[k * hs..(k + 1) * hs],
6265                                    &cm,
6266                                    lg,
6267                                );
6268                            }
6269                            let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
6270                            let target = ids[pos + k0 + k + 1] as usize;
6271                            let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6272                            let lse: f64 = lg
6273                                .iter()
6274                                .map(|&v| ((v - max) as f64).exp())
6275                                .sum::<f64>()
6276                                .ln()
6277                                + max as f64;
6278                            nll += lse - lg[target] as f64;
6279                            cnt += 1;
6280                            if std::env::var("CMF_PPL_TRACE").is_ok() {
6281                                let top = lg
6282                                    .iter()
6283                                    .enumerate()
6284                                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6285                                    .map(|(i, _)| i)
6286                                    .unwrap_or(0);
6287                                eprintln!(
6288                                    "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
6289                                    pos + k0 + k,
6290                                    target,
6291                                    lse - lg[target] as f64,
6292                                    top,
6293                                    lg[target],
6294                                    lg[top]
6295                                );
6296                            }
6297                        }
6298                        k0 = k1;
6299                    }
6300                    pos = end;
6301                }
6302                return Ok((nll, cnt));
6303            }
6304            for pos in 0..ids.len().saturating_sub(1) {
6305                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6306                self.nll_check_graph("serial forward", pos)?;
6307                // Architectures whose head lives inside their own stack return
6308                // the logits out of band and a zero hidden — DeepSeek-V4 folds
6309                // its hyper-connection copies between the last layer and the
6310                // norm, so it cannot hand back a vector this loop could use.
6311                // Scoring the zeros gave a perplexity of exactly the vocabulary
6312                // size, which is a uniform distribution reported as a
6313                // measurement. `generate` already reads this channel.
6314                let out_of_band = self.graph_logits.take();
6315                if self.graph_head_required && out_of_band.is_none() {
6316                    METAL_GRAPH_HEAD_MISS.fetch_add(
6317                        1,
6318                        std::sync::atomic::Ordering::Relaxed,
6319                    );
6320                    return Err(format!(
6321                        "fused Metal graph head did not complete at NLL position {pos}"
6322                    ));
6323                }
6324                if pos < start {
6325                    continue;
6326                }
6327                let logits = match out_of_band {
6328                    Some(lg) => lg,
6329                    None => {
6330                        let normed = inference::rms_norm(
6331                            &hidden,
6332                            &self.weights.final_norm,
6333                            self.rms_eps,
6334                            self.norm_style,
6335                        );
6336                        // lm_head_forward applies the final-logit softcap itself
6337                        // — capping again here double-squashed gemma-class
6338                        // logits (tanh∘tanh) and reported a flattered ppl.
6339                        self.lm_head_forward(&normed)
6340                    }
6341                };
6342                let target = ids[pos + 1] as usize;
6343                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6344                let lse: f64 = logits
6345                    .iter()
6346                    .map(|&v| ((v - max) as f64).exp())
6347                    .sum::<f64>()
6348                    .ln()
6349                    + max as f64;
6350                let tok_nll = lse - logits[target] as f64;
6351                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6352                    let top = logits
6353                        .iter()
6354                        .enumerate()
6355                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6356                        .map(|(i, _)| i)
6357                        .unwrap_or(0);
6358                    eprintln!(
6359                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6360                        logits[target], logits[top]
6361                    );
6362                }
6363                nll += tok_nll;
6364                cnt += 1;
6365            }
6366            Ok((nll, cnt))
6367        })();
6368        self.nll_end();
6369        result
6370    }
6371
6372    /// Score one post-layer hidden with the same final norm/head path used by
6373    /// decode. Keeping this in one helper is important for the production
6374    /// batch scorer: its rows stop before the final norm, just like the
6375    /// per-position O(1) path below.
6376    fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
6377        let normed = inference::rms_norm(
6378            hidden,
6379            &self.weights.final_norm,
6380            self.rms_eps,
6381            self.norm_style,
6382        );
6383        // lm_head_forward applies the final-logit softcap itself — capping
6384        // again here double-squashed gemma-class logits in earlier scorers.
6385        let mut logits = self.lm_head_forward(&normed);
6386        let target = target as usize;
6387        let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6388        let lse: f64 = logits
6389            .iter()
6390            .map(|&v| ((v - max) as f64).exp())
6391            .sum::<f64>()
6392            .ln()
6393            + max as f64;
6394        let tok_nll = lse - logits[target] as f64;
6395        if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6396            let top = logits
6397                .iter()
6398                .enumerate()
6399                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6400                .map(|(i, _)| i)
6401                .unwrap_or(0);
6402            eprintln!(
6403                "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6404                logits[target], logits[top]
6405            );
6406        }
6407        attention::recycle_buf(&mut logits);
6408        tok_nll
6409    }
6410
6411    /// Teacher-forced NLL of the CONVERTED model: the O(1) Nyström path
6412    /// is ACTIVE over the scored positions. Returns `Ok((nll sum, scored
6413    /// count))` over `prefill..len-1` and surfaces a post-mutation batch
6414    /// failure instead of returning a partial score.
6415    ///
6416    /// Runtime discipline, deliberately NOT the matrix probe's: the
6417    /// requested prefix plus any required deferred lead-in run the exact
6418    /// prompt pass — that pass is what freezes the landmarks and M — and
6419    /// every post-seal scored position goes through `NystromState::step()`,
6420    /// the same code decode runs.
6421    /// So the landmarks are PREFILL-frozen (what ships), not
6422    /// full-sequence oracles (what the published probe measured). When the
6423    /// requested prefix is shorter than the bounded transition, rows in the
6424    /// exact lead-in are still scored so the shifted target range is stable.
6425    ///
6426    /// Pair with `nll_ids_from(ids, prefill)` for the exact baseline
6427    /// over the identical token set — that ratio is the honest one.
6428    pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
6429        // This scorer consumes host hiddens, so never request the optional
6430        // token-graph lm_head side channel. `nll_begin` also consumes a
6431        // prior graph failure and clears only the cancel bit that failure
6432        // raised, leaving a caller-owned cancellation observable.
6433        self.nll_begin()?;
6434        let requested_prefix = (prefill > 0).then_some(prefill);
6435        self.o1_begin_with_prefix(requested_prefix);
6436        let n = ids.len().saturating_sub(1);
6437        let requested_start = prefill.min(n);
6438        // The exact prefix must reach the deferred boundary before a
6439        // collecting layer can convert. Rows between the requested start and
6440        // that boundary remain part of the public NLL range and are scored
6441        // from the same hidden pass below.
6442        let exact_end = if self.o1_active() {
6443            match requested_prefix {
6444                Some(requested) => self.o1_effective_boundary(requested),
6445                None => self
6446                    .o1_cfg
6447                    .as_ref()
6448                    .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
6449            }
6450            .unwrap_or(requested_start)
6451            .min(n)
6452        } else {
6453            requested_start
6454        };
6455        let mut nll = 0f64;
6456        let mut cnt = 0usize;
6457
6458        // Exact prompt pass over ids[..exact_end]: the seal consumes its
6459        // q/k/v. Rows at or after requested_start are scored here when the
6460        // bounded lead-in is longer than the caller's requested prefix.
6461        let mut pos = 0usize;
6462        if self.can_prefill_batched() {
6463            const CHUNK: usize = 128;
6464            while pos < exact_end {
6465                let end = (pos + CHUNK).min(exact_end);
6466                let hiddens = self.prefill_batch(&ids[pos..end], pos);
6467                if self
6468                    .graph_failed
6469                    .swap(false, std::sync::atomic::Ordering::Relaxed)
6470                {
6471                    self.cancel
6472                        .store(false, std::sync::atomic::Ordering::Relaxed);
6473                    self.nll_end();
6474                    return Err("GPU graph failed during O(1) NLL prefix".into());
6475                }
6476                for row in 0..end - pos {
6477                    let score_pos = pos + row;
6478                    if score_pos >= requested_start && score_pos < n {
6479                        nll += self.nll_from_hidden(
6480                            &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
6481                            ids[score_pos + 1],
6482                            score_pos,
6483                        );
6484                        cnt += 1;
6485                    }
6486                }
6487                pos = end;
6488            }
6489        } else {
6490            while pos < exact_end {
6491                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6492                if self
6493                    .graph_failed
6494                    .swap(false, std::sync::atomic::Ordering::Relaxed)
6495                {
6496                    self.cancel
6497                        .store(false, std::sync::atomic::Ordering::Relaxed);
6498                    self.nll_end();
6499                    return Err("GPU graph failed during O(1) NLL prefix".into());
6500                }
6501                if pos >= requested_start && pos < n {
6502                    nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6503                    cnt += 1;
6504                }
6505                pos += 1;
6506            }
6507        }
6508        self.o1_seal_checked().map_err(|err| {
6509            self.nll_end();
6510            err
6511        })?;
6512
6513        // Reuse the production whole-token batch graph for the post-seal
6514        // suffix when the caller explicitly enabled both routes. This is a
6515        // teacher-forced scorer, so every row is ids[pos] and its target is
6516        // ids[pos + 1]; no speculative tail or rollback state is involved.
6517        // A first Declined is safe to handle with the established serial O(1)
6518        // path. Once a chunk completes, however, the device recurrent state
6519        // owns the sequence and a later decline must be terminal rather than
6520        // falling back to stale CPU state.
6521        let batch_k = std::env::var("CMF_BATCH_K")
6522            .ok()
6523            .and_then(|v| v.parse::<usize>().ok())
6524            .unwrap_or(0);
6525        let batch_admitted = batch_k > 0
6526            && self.can_prefill_batched()
6527            && self.o1_active()
6528            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6529            && (0..self.num_layers).all(|li| {
6530                let cache = &self.kv_cache.layers[self.phys_layer(li)];
6531                cache.o1.is_none() || cache.o1_views().is_some()
6532            });
6533        if std::env::var("CMF_GRAPH_PROF").is_ok() {
6534            eprintln!(
6535                "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
6536                batch_admitted,
6537                batch_k,
6538                n.saturating_sub(exact_end),
6539            );
6540        }
6541        let mut batch_completed = false;
6542        if batch_admitted && exact_end < n {
6543            let hs = self.hidden_size;
6544            let mut batch_pos = exact_end;
6545            while batch_pos < n {
6546                let end = (batch_pos + batch_k).min(n);
6547                let bk = end - batch_pos;
6548                let mut hiddens = vec![0.0f32; bk * hs];
6549                for (row, &id) in ids[batch_pos..end].iter().enumerate() {
6550                    hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
6551                }
6552                let positions: Vec<usize> = (batch_pos..end).collect();
6553                let t_batch = std::time::Instant::now();
6554                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6555                if std::env::var("CMF_GRAPH_PROF").is_ok() {
6556                    let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
6557                    eprintln!(
6558                        "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6559                        batch_pos,
6560                        end.saturating_sub(1),
6561                        bk as f64 / (ms / 1000.0),
6562                    );
6563                }
6564                if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
6565                    self.nll_end();
6566                    return Err(err);
6567                }
6568                match outcome {
6569                    crate::gpu::BatchGraphOutcome::Completed => {
6570                        batch_completed = true;
6571                        for row in 0..bk {
6572                            nll += self.nll_from_hidden(
6573                                &hiddens[row * hs..(row + 1) * hs],
6574                                ids[batch_pos + row + 1],
6575                                batch_pos + row,
6576                            );
6577                            cnt += 1;
6578                        }
6579                        batch_pos = end;
6580                    }
6581                    crate::gpu::BatchGraphOutcome::Declined => {
6582                        if batch_completed {
6583                            self.nll_end();
6584                            return Err(format!(
6585                                "O(1) NLL batch declined after completed chunk at position {batch_pos}"
6586                            ));
6587                        }
6588                        break;
6589                    }
6590                    crate::gpu::BatchGraphOutcome::Failed => {
6591                        self.nll_end();
6592                        return Err(format!(
6593                            "O(1) NLL batch graph failed after admission at position {batch_pos}"
6594                        ));
6595                    }
6596                }
6597            }
6598            if batch_completed && cnt == n.saturating_sub(requested_start) {
6599                self.nll_end();
6600                return Ok((nll, cnt));
6601            }
6602        }
6603
6604        // Serial O(1) fallback/reference. It is intentionally retained when
6605        // batch admission declines before mutation; callers must label this
6606        // CMF_BATCH_K=0/per-position path separately from the production
6607        // whole-token batch route.
6608        for pos in exact_end..n {
6609            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6610            if self
6611                .graph_failed
6612                .swap(false, std::sync::atomic::Ordering::Relaxed)
6613            {
6614                self.cancel
6615                    .store(false, std::sync::atomic::Ordering::Relaxed);
6616                self.nll_end();
6617                return Err(format!(
6618                    "GPU graph failed during O(1) NLL serial scoring at position {pos}"
6619                ));
6620            }
6621            nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6622            cnt += 1;
6623        }
6624        self.nll_end();
6625        Ok((nll, cnt))
6626    }
6627
6628    /// Teacher-forced calibration data (B1): for each position, whether the
6629    /// argmax equals the actual next token, and the top-1 softmax prob
6630    /// (top-1 probability) under EACH temperature in `temps` — all from ONE forward
6631    /// pass (argmax/correctness are temperature-invariant; only p_max
6632    /// reshapes). Feeds `cortiq calibrate` (reliability/ECE + temperature
6633    /// fit): is the model's confidence a true property, or does it need a
6634    /// measured scaling?
6635    pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
6636        self.clear_sequence_state();
6637        let n = ids.len().saturating_sub(1);
6638        let mut correct = Vec::with_capacity(n);
6639        let mut pmax = Vec::with_capacity(n);
6640        for pos in 0..n {
6641            let emb = self.embed_single(ids[pos]);
6642            let hidden = self.forward_layers(&emb, pos, None);
6643            let normed = inference::rms_norm(
6644                &hidden,
6645                &self.weights.final_norm,
6646                self.rms_eps,
6647                self.norm_style,
6648            );
6649            // lm_head_forward applies the final-logit softcap itself —
6650            // capping again here double-squashed gemma-class logits
6651            // (tanh∘tanh) and reported a flattered ppl.
6652            let logits = self.lm_head_forward(&normed);
6653            let target = ids[pos + 1] as usize;
6654            let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
6655            for (i, &v) in logits.iter().enumerate() {
6656                if v > mval {
6657                    mval = v;
6658                    amax = i;
6659                }
6660            }
6661            correct.push(amax == target);
6662            let row: Vec<f32> = temps
6663                .iter()
6664                .map(|&t| {
6665                    let tt = t.max(1e-3);
6666                    let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
6667                    1.0 / s.max(1e-12) // numerator at the max is exp(0)=1
6668                })
6669                .collect();
6670            pmax.push(row);
6671        }
6672        self.clear_sequence_state();
6673        (correct, pmax)
6674    }
6675
6676    /// Teacher-forced PPL with the dynamic router driving per-window
6677    /// skill switches (VMF experiment №2 measurement). Sequential (φ
6678    /// must update per token), returns (ppl, switch_count). The router
6679    /// must be enabled (`enable_dynamic_routing`); else this equals
6680    /// plain `ppl_ids`. The active skill when scoring token t shapes the
6681    /// logits for t+1 — on-policy over the held-out text itself.
6682    pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
6683        if self.dyn_router.is_none() {
6684            return Ok((self.ppl_ids(ids)?, 0));
6685        }
6686        self.nll_begin()?;
6687        let saved_active = self.dyn_active;
6688        let mut router = self
6689            .dyn_router
6690            .take()
6691            .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
6692        router.reset();
6693        self.dyn_phi_seen = 0;
6694        let _ = self.set_active_skill(None);
6695
6696        let result: Result<(f64, usize), String> = (|| {
6697            let mut nll = 0f64;
6698            let mut cnt = 0usize;
6699            for pos in 0..ids.len().saturating_sub(1) {
6700                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6701                self.nll_check_graph("dynamic serial forward", pos)?;
6702                let out_of_band = self.graph_logits.take();
6703                let mut logits = match out_of_band {
6704                    Some(lg) => lg,
6705                    None => {
6706                        let normed = inference::rms_norm(
6707                            &hidden,
6708                            &self.weights.final_norm,
6709                            self.rms_eps,
6710                            self.norm_style,
6711                        );
6712                        // lm_head_forward applies the final-logit softcap itself —
6713                        // capping again here double-squashed gemma-class logits
6714                        // and reported a flattered ppl.
6715                        self.lm_head_forward(&normed)
6716                    }
6717                };
6718                let target = ids[pos + 1] as usize;
6719                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6720                let lse: f64 = logits
6721                    .iter()
6722                    .map(|&v| ((v - max) as f64).exp())
6723                    .sum::<f64>()
6724                    .ln()
6725                    + max as f64;
6726                let tok_nll = lse - logits[target] as f64;
6727                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6728                    let top = logits
6729                        .iter()
6730                        .enumerate()
6731                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6732                        .map(|(i, _)| i)
6733                        .unwrap_or(0);
6734                    eprintln!(
6735                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6736                        logits[target], logits[top]
6737                    );
6738                }
6739                nll += tok_nll;
6740                cnt += 1;
6741                attention::recycle_buf(&mut logits);
6742                // Route on the evolving phi (drives the NEXT token's skill).
6743                let phi = self.dyn_phi_ema.clone();
6744                if let Some(new_active) = router.step(&phi, pos) {
6745                    let _ = self.set_active_skill(new_active);
6746                }
6747            }
6748            Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
6749        })();
6750
6751        // Restore the detached router and the active overlay on both success
6752        // and failure. The scoring state is cleared independently below.
6753        let _ = self.set_active_skill(saved_active);
6754        self.dyn_router = Some(router);
6755        self.nll_end();
6756        result
6757    }
6758
6759    /// Routing probe φ (spec §9): mean-pooled hidden after `layer`.
6760    pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
6761        self.clear_sequence_state();
6762        let mut acc = vec![0f32; self.hidden_size];
6763        for (pos, &id) in ids.iter().enumerate() {
6764            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
6765            for (a, v) in acc.iter_mut().zip(&h) {
6766                *a += v;
6767            }
6768        }
6769        let n = ids.len().max(1) as f32;
6770        for a in acc.iter_mut() {
6771            *a /= n;
6772        }
6773        self.clear_sequence_state();
6774        acc
6775    }
6776
6777    /// Layer-major batched prefill (prefill-GEMM): full-attention —
6778    /// per-position with the existing operators (KV grows naturally,
6779    /// causality preserved), GDN projections / FFN / MoE — batched
6780    /// (a weight row is read from DRAM once per chunk, not per
6781    /// position). Returns the hidden of all positions [b × hidden].
6782    fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
6783        self.prefill_batch_masked(ids, start_pos, None)
6784    }
6785
6786    /// `prefill_batch` with a task mask honored on the dense-FFN panels
6787    /// (the masked-inference fast path: full fused compute, mask lands on
6788    /// the activations). The whole-chunk GPU graph is skipped for masked
6789    /// layers by the callers' arms; the per-GEMM device paths stay in
6790    /// play because the zeroing happens on the host between them.
6791    fn prefill_batch_masked(
6792        &mut self,
6793        ids: &[u32],
6794        start_pos: usize,
6795        task_mask: Option<&TaskMask>,
6796    ) -> Vec<f32> {
6797        self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
6798    }
6799
6800    /// The layer-major batched walk over a layer span [from..upto_excl):
6801    /// the whole prefill machinery (chunk graph, batched attends, GEMM
6802    /// panels) for a PARTIAL stack — the network split's prefill rides
6803    /// the same canon as the local one. Input is token ids (embeds
6804    /// itself, coordinator side) or ready boundary hiddens (worker side).
6805    fn prefill_batch_span(
6806        &mut self,
6807        input: PrefillIn<'_>,
6808        start_pos: usize,
6809        task_mask: Option<&TaskMask>,
6810        from: usize,
6811        upto_excl: usize,
6812    ) -> Vec<f32> {
6813        let hs = self.hidden_size;
6814        let b = match input {
6815            PrefillIn::Ids(ids) => ids.len(),
6816            PrefillIn::Hidden(hb) => hb.len() / hs,
6817        };
6818        let upto_excl = upto_excl.min(self.num_layers);
6819        // The CPU embed is deferred: when the chunk graph takes the run
6820        // from layer 0 it gathers the embeddings on the device instead.
6821        // A hidden input is ready by definition.
6822        let mut h: Vec<f32>;
6823        let mut h_ready;
6824        match input {
6825            PrefillIn::Ids(_) => {
6826                h = vec![0.0; b * hs];
6827                h_ready = false;
6828            }
6829            PrefillIn::Hidden(hb) => {
6830                h = hb.to_vec();
6831                h_ready = true;
6832            }
6833        }
6834        let fill_h = |h: &mut Vec<f32>, me: &Self| {
6835            if let PrefillIn::Ids(ids) = input {
6836                for (bi, &id) in ids.iter().enumerate() {
6837                    let e = me.embed_single(id);
6838                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
6839                }
6840                if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6841                    if let Ok(t) = tp.parse::<usize>() {
6842                        if t >= start_pos && t < start_pos + ids.len() {
6843                            let bi = t - start_pos;
6844                            let row = &h[bi * hs..(bi + 1) * hs];
6845                            let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6846                            eprintln!(
6847                                "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
6848                                ids[bi],
6849                                row[0],
6850                                row[1],
6851                                ids.len(),
6852                                &ids[..ids.len().min(8)]
6853                            );
6854                        }
6855                    }
6856                }
6857            }
6858        };
6859        let (_nkv, _hd, _rd, eps) = (
6860            self.num_kv_heads,
6861            self.head_dim,
6862            self.rotary_dim,
6863            self.rms_eps,
6864        );
6865        let pool = self.pool.clone();
6866        let norm_style = self.norm_style;
6867        let automatic_gpu_prefix = self.automatic_gpu_prefix();
6868
6869        #[cfg(target_os = "macos")]
6870        let mut chunk_skip_until = 0usize;
6871        for li in from..upto_excl {
6872            let _capacity_tail = automatic_gpu_prefix
6873                .filter(|&prefix| li >= prefix)
6874                .map(|_| crate::gpu::enter_cpu_scope());
6875            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU
6876            // GPU chunk graph (default-on under CMF_GPU=1): a run of
6877            // consecutive eligible layers for the whole chunk in ONE
6878            // Metal submission — norm, QKV, RoPE with fused mirror
6879            // append, causal attend, O, FFN, hidden device-resident
6880            // across the run. Any refusal falls through to the CPU path.
6881            #[cfg(target_os = "macos")]
6882            if task_mask.is_none() {
6883                if li < chunk_skip_until {
6884                    continue;
6885                }
6886                // Device-side embedding needs a q8_row embedding matrix;
6887                // with any other layout the CPU fills `h` first and the
6888                // graph starts from a ready hidden (refusing the whole
6889                // run over the embedding alone kept q4t models — the
6890                // whole Nanbeige/Bonsai class — on the CPU prefill).
6891                if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
6892                    fill_h(&mut h, self);
6893                    h_ready = true;
6894                }
6895                let ids_for_embed = match input {
6896                    PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
6897                    PrefillIn::Hidden(_) => None,
6898                };
6899                let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
6900                if end > li {
6901                    h_ready = true;
6902                    chunk_skip_until = end;
6903                    // Looped Transformer: the graph stopped at a loop
6904                    // boundary — apply final norm before the next iteration.
6905                    if self.is_loop_end(end - 1) && end < self.num_layers {
6906                        for bi in 0..b {
6907                            let normed = inference::rms_norm(
6908                                &h[bi * hs..(bi + 1) * hs],
6909                                &self.weights.final_norm,
6910                                eps,
6911                                norm_style,
6912                            );
6913                            h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6914                        }
6915                    }
6916                    continue;
6917                }
6918            }
6919            if !h_ready {
6920                fill_h(&mut h, self);
6921                h_ready = true;
6922            }
6923            let lw = &self.weights.layers[self.phys_layer(li)];
6924            // ── attention ──
6925            match &lw.attn {
6926                AttnKind::Kda(w) => {
6927                    // Projections batched, recurrence sequential.
6928                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
6929                    let mut normed = vec![0.0f32; b * hs];
6930                    for bi in 0..b {
6931                        inference::rms_norm_into(
6932                            &h[bi * hs..(bi + 1) * hs],
6933                            &lw.input_norm,
6934                            eps,
6935                            norm_style,
6936                            &mut normed[bi * hs..(bi + 1) * hs],
6937                        );
6938                    }
6939                    let attn = crate::linear_core::kda_forward_batch(
6940                        &normed,
6941                        b,
6942                        w,
6943                        &cfg,
6944                        &mut self.kv_cache.layers[li].linear_state,
6945                        pool.as_deref(),
6946                    );
6947                    for (dst, &a) in h.iter_mut().zip(&attn) {
6948                        *dst += a;
6949                    }
6950                }
6951                AttnKind::LinearGdn(w) => {
6952                    // Projections batched, recurrence sequential.
6953                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6954                    let mut normed = vec![0.0f32; b * hs];
6955                    for bi in 0..b {
6956                        let r = inference::rms_norm(
6957                            &h[bi * hs..(bi + 1) * hs],
6958                            &lw.input_norm,
6959                            eps,
6960                            norm_style,
6961                        );
6962                        normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6963                    }
6964                    let attn = crate::linear_core::gdn_forward_batch(
6965                        &normed,
6966                        b,
6967                        w,
6968                        &cfg,
6969                        &mut self.kv_cache.layers[li].linear_state,
6970                        pool.as_deref(),
6971                    );
6972                    for (dst, &a) in h.iter_mut().zip(&attn) {
6973                        *dst += a;
6974                    }
6975                }
6976                AttnKind::ShortConv(w) => {
6977                    // Projections batched over the chunk; the conv walks the
6978                    // contiguous positions in order (same ring as decode).
6979                    let cfg = self
6980                        .short_conv_cfg
6981                        .expect("short-conv layer without short_conv_cfg");
6982                    let mut normed = vec![0.0f32; b * hs];
6983                    for bi in 0..b {
6984                        inference::rms_norm_into(
6985                            &h[bi * hs..(bi + 1) * hs],
6986                            &lw.input_norm,
6987                            eps,
6988                            norm_style,
6989                            &mut normed[bi * hs..(bi + 1) * hs],
6990                        );
6991                    }
6992                    let attn = short_conv_forward_batch(
6993                        &normed,
6994                        b,
6995                        w,
6996                        &cfg,
6997                        &mut self.kv_cache.layers[li].linear_state,
6998                        pool.as_deref(),
6999                    );
7000                    for (dst, &a) in h.iter_mut().zip(&attn) {
7001                        *dst += a;
7002                    }
7003                }
7004                AttnKind::Mla(w) => {
7005                    // Per-position prefill (correctness first; latent
7006                    // batching is a later optimization).
7007                    let inv_freq_l = self.layer_inv_freq(li);
7008                    let rs = self.layer_rope_scale(li);
7009                    let mut normed = vec![0.0f32; hs];
7010                    for bi in 0..b {
7011                        inference::rms_norm_into(
7012                            &h[bi * hs..(bi + 1) * hs],
7013                            &lw.input_norm,
7014                            eps,
7015                            norm_style,
7016                            &mut normed,
7017                        );
7018                        let ao = mla_attention(
7019                            w,
7020                            &normed,
7021                            &mut self.kv_cache.layers[li],
7022                            start_pos + bi,
7023                            &inv_freq_l,
7024                            rs,
7025                            eps,
7026                            pool.as_deref(),
7027                        );
7028                        for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
7029                            *dst += a;
7030                        }
7031                    }
7032                }
7033                AttnKind::Full {
7034                    wq,
7035                    wk,
7036                    wv,
7037                    wo,
7038                    q_norm,
7039                    k_norm,
7040                    output_gate,
7041                    softplus_gate,
7042                    bias,
7043                } => {
7044                    // Chunk-GEMM QKV/O; per-position causal attention
7045                    // inside (roadmap §3 P0 — full-attention prefill no
7046                    // longer re-reads the projection weights b times).
7047                    let mut normed = vec![0.0f32; b * hs];
7048                    for bi in 0..b {
7049                        inference::rms_norm_into(
7050                            &h[bi * hs..(bi + 1) * hs],
7051                            &lw.input_norm,
7052                            eps,
7053                            norm_style,
7054                            &mut normed[bi * hs..(bi + 1) * hs],
7055                        );
7056                    }
7057                    let inv_freq_l = self.layer_inv_freq(li);
7058                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7059                    let cfg = QwenAttnCfg {
7060                        num_heads: self.layer_num_heads(li),
7061                        num_kv_heads: nkv_l,
7062                        head_dim: hd_l,
7063                        hidden_size: hs,
7064                        position: start_pos,
7065                        inv_freq: &inv_freq_l,
7066                        rotary_dim: rd_l,
7067                        scale: self.attn_scale,
7068                        softcap: self.attn_softcap,
7069                        window: self.layer_window(li),
7070                        v_norm: self.attn_v_norm,
7071                        qk_norm_after_rope: self.qk_norm_after_rope,
7072                        q_norm: q_norm.as_deref(),
7073                        k_norm: k_norm.as_deref(),
7074                        output_gate: *output_gate,
7075                        softplus_gate: softplus_gate
7076                            .as_ref()
7077                            .map(|(gate, per_head)| (gate, *per_head)),
7078                        rope_scale: self.layer_rope_scale(li),
7079                        bias: bias
7080                            .as_ref()
7081                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7082                        rms_eps: eps,
7083                        norm_style,
7084                        pool: pool.as_deref(),
7085                    };
7086                    let mut attn = attention::qwen_attention_batch(
7087                        &normed,
7088                        b,
7089                        wq,
7090                        wk,
7091                        wv,
7092                        wo,
7093                        &mut self.kv_cache.layers[li],
7094                        &cfg,
7095                    );
7096                    if let Some(w) = &lw.attn_out_norm {
7097                        for bi in 0..b {
7098                            inference::rms_norm_into(
7099                                &attn[bi * hs..(bi + 1) * hs],
7100                                w,
7101                                eps,
7102                                norm_style,
7103                                &mut normed[bi * hs..(bi + 1) * hs],
7104                            );
7105                        }
7106                        attn.copy_from_slice(&normed);
7107                    }
7108                    for (dst, &a) in h.iter_mut().zip(&attn) {
7109                        *dst += a;
7110                    }
7111                }
7112                AttnKind::Linear(w) => {
7113                    for bi in 0..b {
7114                        let normed = inference::rms_norm(
7115                            &h[bi * hs..(bi + 1) * hs],
7116                            &lw.input_norm,
7117                            eps,
7118                            norm_style,
7119                        );
7120                        vmf_phase_forward(
7121                            &normed,
7122                            w,
7123                            &self.vmf_cfg.expect("linear layer without vmf_cfg"),
7124                            &mut self.kv_cache.layers[li].linear_state,
7125                            pool.as_deref(),
7126                        )
7127                        .iter()
7128                        .enumerate()
7129                        .for_each(|(i, &a)| h[bi * hs + i] += a);
7130                    }
7131                }
7132            }
7133
7134            // ── FFN batched ──
7135            let lw = &self.weights.layers[self.phys_layer(li)];
7136            let mut post = vec![0.0f32; b * hs];
7137            for bi in 0..b {
7138                let r =
7139                    inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
7140                post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7141            }
7142            // A restrictive per-visit FFN row lands on the activations
7143            // inside the dense arm; an all-open row costs nothing.
7144            let mask_row = task_mask
7145                .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
7146                .and_then(|m| m.ffn_masks.get(li))
7147                .map(|v| v.as_slice());
7148            let mut ffn = match &lw.ffn {
7149                FfnKind::Dense(d) if !d.segs.is_empty() => {
7150                    tube_ffn(d, &post, b, pool.as_deref(), mask_row)
7151                }
7152                FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
7153                FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
7154                // Dual-branch layers run per position (the expert branch
7155                // reads the raw residual — nothing to batch yet).
7156                FfnKind::DenseMoe(dm) => {
7157                    let mut out = vec![0.0f32; b * hs];
7158                    for bi in 0..b {
7159                        let r = dense_moe_ffn(
7160                            dm,
7161                            &post[bi * hs..(bi + 1) * hs],
7162                            &h[bi * hs..(bi + 1) * hs],
7163                            eps,
7164                            norm_style,
7165                            pool.as_deref(),
7166                        );
7167                        out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7168                    }
7169                    out
7170                }
7171            };
7172            if let Some(w) = &lw.ffn_out_norm {
7173                for bi in 0..b {
7174                    inference::rms_norm_into(
7175                        &ffn[bi * hs..(bi + 1) * hs],
7176                        w,
7177                        eps,
7178                        norm_style,
7179                        &mut post[bi * hs..(bi + 1) * hs],
7180                    );
7181                }
7182                ffn.copy_from_slice(&post);
7183            }
7184            for (dst, &f) in h.iter_mut().zip(&ffn) {
7185                *dst += f;
7186            }
7187            if let Some(sc) = lw.layer_scale {
7188                for v in h.iter_mut() {
7189                    *v *= sc;
7190                }
7191            }
7192            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
7193                if let Ok(t) = tp.parse::<usize>() {
7194                    if t >= start_pos && t < start_pos + b {
7195                        let bi = t - start_pos;
7196                        let row = &h[bi * hs..(bi + 1) * hs];
7197                        let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
7198                        eprintln!(
7199                            "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
7200                            row[0], row[1]
7201                        );
7202                    }
7203                }
7204            }
7205            // CMF_DEBUG_LAYERS=1: per-layer hidden-state health of the
7206            // LAST prompt position — the knife for "which layer type
7207            // breaks first" on a new architecture.
7208            if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
7209                let row = &h[(b - 1) * hs..b * hs];
7210                let rms =
7211                    (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
7212                let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
7213                eprintln!(
7214                    "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
7215                    match &self.weights.layers[self.phys_layer(li)].attn {
7216                        AttnKind::LinearGdn(_) => "gdn",
7217                        AttnKind::Linear(_) => "vmf",
7218                        AttnKind::ShortConv(_) => "conv",
7219                        _ => "attn",
7220                    },
7221                    match &lw.ffn {
7222                        FfnKind::Moe(_) => "moe",
7223                        FfnKind::Dense(_) => "dense",
7224                        FfnKind::DenseMoe(_) => "dense+moe",
7225                    },
7226                );
7227            }
7228            // Looped Transformer: apply final norm at the end of each loop iteration.
7229            if self.is_loop_end(li) && li + 1 < self.num_layers {
7230                for bi in 0..b {
7231                    let normed = inference::rms_norm(
7232                        &h[bi * hs..(bi + 1) * hs],
7233                        &self.weights.final_norm,
7234                        eps,
7235                        norm_style,
7236                    );
7237                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
7238                }
7239            }
7240            if std::env::var("CMF_TRACE_H").is_ok() {
7241                let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
7242                let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
7243                eprintln!(
7244                    "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
7245                    lw.layer_scale
7246                );
7247            }
7248        }
7249        crate::gpu::set_layer(-1); // lm_head/final ops outside layer-split
7250        // A batched span owns a complete set of positions. Publish any
7251        // collecting→sealed transition only after every layer has finished;
7252        // callers that cross into serial/device work must see the new epoch
7253        // before this function returns.
7254        self.o1_progress();
7255        h
7256    }
7257
7258    /// Embed a single token.
7259    fn embed_single(&self, id: u32) -> Vec<f32> {
7260        let mut out = vec![0.0f32; self.hidden_size];
7261        if (id as usize) < self.weights.embed_tokens.rows() {
7262            self.weights.embed_tokens.row_f32(id as usize, &mut out);
7263        }
7264        if self.embed_multiplier != 1.0 {
7265            for v in out.iter_mut() {
7266                *v *= self.embed_multiplier;
7267            }
7268        }
7269        // DeepSeek-V4's hash layers route by TOKEN ID, so the id has to
7270        // reach the forward. It rides in slot 0 (the forward re-reads the
7271        // real embedding itself from the table).
7272        if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
7273            let mut v = vec![0.0f32; self.hidden_size.max(1)];
7274            v[0] = id as f32;
7275            return v;
7276        }
7277        // Gemma-3n: the per-layer-embedding half needs the token ID, so
7278        // it rides appended to the embedding; the g3n forward splits it.
7279        if let Some(b) = &self.g3n {
7280            return b.0.extend_embedding(id, &out, self.pool.as_deref());
7281        }
7282        out
7283    }
7284
7285    /// A run of consecutive prefill layers on the GPU for the whole
7286    /// chunk (default-on under CMF_GPU=1; CMF_GPU_CHUNK=0 disables).
7287    /// Eligibility per layer: q8_row weights, plain full attention
7288    /// (no output gate), F32 KV, no o1/masks/gemma extras. Returns the
7289    /// first layer index NOT processed (== `li0` when the run is empty).
7290    #[cfg(target_os = "macos")]
7291    fn chunk_run_gpu(
7292        &mut self,
7293        li0: usize,
7294        h: &mut [f32],
7295        b: usize,
7296        pos0: usize,
7297        embed_ids: Option<&[u32]>,
7298        cap: usize,
7299    ) -> usize {
7300        // (The old streaming attend needed a depth bound at ~1k; the
7301        // GEMM attention scales like the CPU path and lifted it.)
7302        // CMF_GPU_CHUNK=0 disables the graph.
7303        if !crate::gpu::enabled_here()
7304            || std::env::var("CMF_GPU_CHUNK")
7305                .map(|v| v == "0")
7306                .unwrap_or(false)
7307            || b < 32
7308            || self.swa.is_some()
7309            || self.global_attn.is_some()
7310            // Collection owns the exact Q trace and boundary conversion;
7311            // this chunk graph appends dense KV without feeding that trace.
7312            || self.o1_active()
7313            || self.attn_v_norm
7314            || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
7315        {
7316            return li0;
7317        }
7318        let Some(model) = self.model.clone() else {
7319            return li0;
7320        };
7321        let inv_freq = self.inv_freq.clone();
7322        let (nh, nkv, hd, hs) = (
7323            self.num_heads,
7324            self.num_kv_heads,
7325            self.head_dim,
7326            self.hidden_size,
7327        );
7328        // Collect the longest run of consecutive eligible layers.
7329        // Looped Transformer: stop at the loop boundary so the CPU can
7330        // apply loop_final_norm between iterations.
7331        let loop_end = if self.loop_final_norm {
7332            ((li0 / self.physical_layers) + 1) * self.physical_layers
7333        } else {
7334            self.num_layers
7335        };
7336        let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
7337        let mut stored_at: Vec<usize> = Vec::new();
7338        for li in li0..self.num_layers.min(loop_end).min(cap) {
7339            let lw = &self.weights.layers[self.phys_layer(li)];
7340            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
7341                break;
7342            }
7343            let AttnKind::Full {
7344                wq,
7345                wk,
7346                wv,
7347                wo,
7348                q_norm,
7349                k_norm,
7350                output_gate: false,
7351                softplus_gate: None,
7352                bias,
7353            } = &lw.attn
7354            else {
7355                break;
7356            };
7357            let FfnKind::Dense(d) = &lw.ffn else { break };
7358            if d.act != Act::Silu || !d.segs.is_empty() {
7359                break;
7360            }
7361            // q8_row (row_scale populated), or q4_tiled / q4tp (row_scale
7362            // empty — their scales are in the payload). Mixing across the
7363            // seven projections of one layer is fine; the encoder branches
7364            // per weight on the tensor's dtype. Anything else refuses.
7365            fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
7366                t.q8_row_parts()
7367                    .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7368                    .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7369            }
7370            let parts = (
7371                cw(wq),
7372                cw(wk),
7373                cw(wv),
7374                cw(wo),
7375                cw(&d.gate_proj),
7376                cw(&d.up_proj),
7377                cw(&d.down_proj),
7378            );
7379            let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
7380            else {
7381                break;
7382            };
7383            let layer = &self.kv_cache.layers[li];
7384            if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
7385                break;
7386            }
7387            stored_at.push(layer.head_len(0));
7388            layers.push(crate::gpu_metal::ChunkLayer {
7389                model: &model,
7390                kv_id: self.graph_kv_id,
7391                layer: li,
7392                wq: pq,
7393                wk: pk,
7394                wv: pv,
7395                wo: po,
7396                gate: pg,
7397                up: pu,
7398                down: pd,
7399                input_norm: &lw.input_norm,
7400                post_norm: &lw.post_norm,
7401                bias: bias
7402                    .as_ref()
7403                    .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
7404                q_norm: q_norm.as_deref(),
7405                k_norm: k_norm.as_deref(),
7406                inv_freq: &inv_freq,
7407                rd: self.rotary_dim,
7408                nh,
7409                nkv,
7410                hd,
7411                hs,
7412                inter: d.gate_proj.rows(),
7413                gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
7414                late_qk_norm: self.qk_norm_after_rope,
7415                eps: self.rms_eps as f32,
7416            });
7417        }
7418        if layers.is_empty() {
7419            return li0;
7420        }
7421        let row = nkv * hd;
7422        let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
7423            .iter()
7424            .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
7425            .collect();
7426        let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
7427        for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
7428            let li = layers[i].layer;
7429            let layer = &self.kv_cache.layers[li];
7430            io.push(crate::gpu_metal::ChunkIo {
7431                cpu_stored: stored_at[i],
7432                cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
7433                cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
7434                out_k: ok,
7435                out_v: ov,
7436                imp: oi,
7437            });
7438        }
7439        let n_run = layers.len();
7440        let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
7441        // Device-side embedding when the run starts the model and the
7442        // embedding matrix is q8_row-mapped.
7443        let ep = embed_ids.and_then(|ids| {
7444            self.weights
7445                .embed_tokens
7446                .q8_row_parts()
7447                .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
7448                    idx,
7449                    rows,
7450                    row_scale: rs,
7451                    ids,
7452                    mult: self.embed_multiplier,
7453                })
7454        });
7455        if embed_ids.is_some() && ep.is_none() {
7456            return li0;
7457        }
7458        if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
7459            return li0;
7460        }
7461        drop(io);
7462        drop(layers);
7463        // CPU caches stay the owners of record: append the chunk rows
7464        // and bank the importance masses per layer.
7465        for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
7466            let li = li0 + i;
7467            let layer = &mut self.kv_cache.layers[li];
7468            for bi in 0..b {
7469                layer.append(
7470                    &ok[bi * row..(bi + 1) * row],
7471                    &ov[bi * row..(bi + 1) * row],
7472                    &[],
7473                );
7474            }
7475            layer.accumulate_imp(oi);
7476        }
7477        last
7478    }
7479
7480    /// Is layer `li` a sliding-window (local-RoPE) layer? Gemma-3:
7481    /// every `pattern`-th layer is global, the rest are local.
7482    fn layer_is_local(&self, li: usize) -> bool {
7483        if let Some(layers) = &self.sliding_layers {
7484            return layers.get(li).copied().unwrap_or(false);
7485        }
7486        match self.swa {
7487            Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
7488            None => false,
7489        }
7490    }
7491
7492    /// The RoPE table for layer `li` (local layers may have their own;
7493    /// Gemma-4 global layers use the proportional padded table).
7494    fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
7495        if self.layer_is_local(li) {
7496            if let Some(f) = &self.inv_freq_local {
7497                return f.clone();
7498            }
7499        } else if let Some(f) = &self.inv_freq_global {
7500            return f.clone();
7501        }
7502        self.inv_freq.clone()
7503    }
7504
7505    /// The attend window for layer `li` (None = full context).
7506    fn layer_window(&self, li: usize) -> Option<usize> {
7507        self.swa
7508            .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
7509    }
7510
7511    fn layer_num_heads(&self, li: usize) -> usize {
7512        self.attention_heads_per_layer
7513            .as_ref()
7514            .and_then(|v| v.get(li).copied())
7515            .unwrap_or(self.num_heads)
7516    }
7517
7518    fn layer_rope_scale(&self, li: usize) -> f32 {
7519        if self.layer_is_local(li) {
7520            self.rope_scale_local
7521        } else {
7522            self.rope_scale
7523        }
7524    }
7525
7526    /// Attention geometry of layer `li`: (num_kv_heads, head_dim,
7527    /// rotary_dim). Gemma-4 global layers override all three.
7528    fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
7529        if !self.layer_is_local(li) {
7530            if let Some((ghd, gkv)) = self.global_attn {
7531                return (gkv, ghd, ghd);
7532            }
7533        }
7534        (
7535            self.num_kv_heads,
7536            self.head_dim,
7537            if self.layer_is_local(li) {
7538                self.rotary_dim_local.unwrap_or(self.rotary_dim)
7539            } else {
7540                self.rotary_dim
7541            },
7542        )
7543    }
7544
7545    /// Forward one position through all layers (hybrid dispatch).
7546    fn forward_layers(
7547        &mut self,
7548        hidden: &[f32],
7549        position: usize,
7550        task_mask: Option<&TaskMask>,
7551    ) -> Vec<f32> {
7552        let out = self.forward_layers_upto(hidden, position, task_mask, None);
7553        self.o1_progress();
7554        out
7555    }
7556
7557    // ── Network pipeline-split building blocks (coordinator/worker) ──
7558    // A remote worker owns layers [from ..= upto] and their KV; the
7559    // coordinator owns the rest plus embed / final norm / head. Attention
7560    // causality is per-layer, so a whole prompt's boundary hiddens ship
7561    // as one batch and decode ships one vector per token.
7562
7563    /// Embed one token id (embed multiplier applied).
7564    pub fn embed_id(&self, id: u32) -> Vec<f32> {
7565        self.embed_single(id)
7566    }
7567
7568    /// Refuse the archs/modes whose forward cannot be cut at a layer
7569    /// boundary. Loud by design: a split that silently changed the math
7570    /// would be a chimera.
7571    pub fn split_supported(&self) -> Result<(), String> {
7572        if self.dsv4.is_some() {
7573            return Err(
7574                "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
7575            );
7576        }
7577        if self.dsv41.is_some() {
7578            return Err(
7579                "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
7580                    .into(),
7581            );
7582        }
7583        if self.qwen4_exp.is_some() {
7584            return Err(
7585                "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
7586            );
7587        }
7588        if self.g3n.is_some() {
7589            return Err(
7590                "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
7591            );
7592        }
7593        Ok(())
7594    }
7595
7596    /// Forward `hidden` through layers [from ..= upto] at `position`,
7597    /// appending those layers' KV/state. Both split sides call this
7598    /// over their own range; a task mask applies to the span's own
7599    /// layers (each side masks what it runs).
7600    pub fn forward_span(
7601        &mut self,
7602        hidden: &[f32],
7603        position: usize,
7604        from: usize,
7605        upto: usize,
7606        task_mask: Option<&TaskMask>,
7607    ) -> Result<Vec<f32>, String> {
7608        self.split_supported()?;
7609        if from > upto || upto >= self.num_layers {
7610            return Err(format!(
7611                "forward_span: layer range {from}..={upto} outside 0..{}",
7612                self.num_layers
7613            ));
7614        }
7615        if hidden.len() != self.hidden_size {
7616            return Err(format!(
7617                "forward_span: hidden len {} ≠ hidden_size {}",
7618                hidden.len(),
7619                self.hidden_size
7620            ));
7621        }
7622        let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
7623        self.o1_progress();
7624        if self
7625            .graph_failed
7626            .swap(false, std::sync::atomic::Ordering::Relaxed)
7627        {
7628            self.cancel
7629                .store(false, std::sync::atomic::Ordering::Relaxed);
7630            self.clear_sequence_state();
7631            return Err("forward_span: deferred O(1) transition failed".into());
7632        }
7633        Ok(out)
7634    }
7635
7636    /// Final norm + lm_head over a boundary hidden (the final-logit
7637    /// softcap is applied by lm_head_forward itself).
7638    pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
7639        let normed = inference::rms_norm(
7640            hidden,
7641            &self.weights.final_norm,
7642            self.rms_eps,
7643            self.norm_style,
7644        );
7645        self.lm_head_forward(&normed)
7646    }
7647
7648    /// Sample the next token with this pipeline's sampler state.
7649    pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
7650        sampler::sample_with_scratch(
7651            logits,
7652            &self.sampler_config,
7653            past_tokens,
7654            &mut self.rng,
7655            &mut self.sampler_scratch,
7656        )
7657    }
7658
7659    /// Fresh sequence: clear KV, reuse history and device mirrors.
7660    pub fn reset_session(&mut self) {
7661        self.clear_sequence_state();
7662    }
7663
7664    /// Batched span prefill from token ids (coordinator side): embed +
7665    /// layers [0 ..= upto]; returns the boundary hiddens of ALL positions
7666    /// (ids.len() × hidden). Rides the same layer-major machinery as the
7667    /// local prefill; falls back to the per-position walk under
7668    /// CMF_PREFILL=seq.
7669    pub fn prefill_span_ids(
7670        &mut self,
7671        ids: &[u32],
7672        start_pos: usize,
7673        upto: usize,
7674        task_mask: Option<&TaskMask>,
7675    ) -> Result<Vec<f32>, String> {
7676        self.split_supported()?;
7677        if upto >= self.num_layers {
7678            return Err(format!(
7679                "prefill_span_ids: upto {upto} outside 0..{}",
7680                self.num_layers
7681            ));
7682        }
7683        // Same predicate as the whole-stack prefill: a span whose GDN
7684        // state lives on the device must walk positions through the
7685        // graph, not through the batched CPU span.
7686        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7687            let out =
7688                self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
7689            self.check_o1_progress_failure("prefill_span_ids")?;
7690            Ok(out)
7691        } else {
7692            let hs = self.hidden_size;
7693            let mut out = Vec::with_capacity(ids.len() * hs);
7694            for (i, &id) in ids.iter().enumerate() {
7695                let emb = self.embed_id(id);
7696                out.extend_from_slice(&self.forward_span(
7697                    &emb,
7698                    start_pos + i,
7699                    0,
7700                    upto,
7701                    task_mask,
7702                )?);
7703            }
7704            Ok(out)
7705        }
7706    }
7707
7708    /// Batched span prefill from boundary hiddens (worker side): layers
7709    /// [from ..= upto] for every position in the batch; returns the batch.
7710    pub fn prefill_span_hidden(
7711        &mut self,
7712        hidden: &[f32],
7713        start_pos: usize,
7714        from: usize,
7715        upto: usize,
7716        task_mask: Option<&TaskMask>,
7717    ) -> Result<Vec<f32>, String> {
7718        self.split_supported()?;
7719        let hs = self.hidden_size;
7720        if hidden.is_empty() || hidden.len() % hs != 0 {
7721            return Err(format!(
7722                "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
7723                hidden.len()
7724            ));
7725        }
7726        if from > upto || upto >= self.num_layers {
7727            return Err(format!(
7728                "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
7729                self.num_layers
7730            ));
7731        }
7732        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7733            let out = self.prefill_batch_span(
7734                PrefillIn::Hidden(hidden),
7735                start_pos,
7736                task_mask,
7737                from,
7738                upto + 1,
7739            );
7740            self.check_o1_progress_failure("prefill_span_hidden")?;
7741            Ok(out)
7742        } else {
7743            let b = hidden.len() / hs;
7744            let mut out = Vec::with_capacity(hidden.len());
7745            for i in 0..b {
7746                let h = self.forward_span(
7747                    &hidden[i * hs..(i + 1) * hs],
7748                    start_pos + i,
7749                    from,
7750                    upto,
7751                    task_mask,
7752                )?;
7753                out.extend_from_slice(&h);
7754            }
7755            Ok(out)
7756        }
7757    }
7758
7759    /// Build the whole-token wgpu graph for a pure-attention q1 model (every
7760    /// layer Full q1 + dense q1 FFN, no gate/bias). Returns the post-stack
7761    /// hidden (caller does final norm + lm_head), or None to fall back.
7762    fn try_token_graph_wgpu(
7763        &self,
7764        hidden: &[f32],
7765        position: usize,
7766        logits_out: &mut Vec<f32>,
7767        layers_run: &mut usize,
7768    ) -> Option<Result<Vec<f32>, ()>> {
7769        self.try_token_graph_wgpu_steps(
7770            hidden,
7771            position,
7772            logits_out,
7773            1,
7774            None,
7775            Some(layers_run),
7776            0,
7777            self.num_layers,
7778        )
7779    }
7780
7781    /// The span twin (network split): the graph covers [from..upto_excl)
7782    /// — one submit per SEGMENT per token. lm_head folds in only when
7783    /// the span reaches the last layer.
7784    fn try_token_graph_wgpu_span(
7785        &self,
7786        hidden: &[f32],
7787        position: usize,
7788        logits_out: &mut Vec<f32>,
7789        from: usize,
7790        upto_excl: usize,
7791        layers_run: &mut usize,
7792    ) -> Option<Result<Vec<f32>, ()>> {
7793        self.try_token_graph_wgpu_steps(
7794            hidden,
7795            position,
7796            logits_out,
7797            1,
7798            None,
7799            Some(layers_run),
7800            from,
7801            upto_excl,
7802        )
7803    }
7804
7805    /// Greedy burst: forward `t_next` and let the device pick + re-embed
7806    /// the next k−1 tokens — k frames, ONE submit, k ids back. The ZML
7807    /// trade, on wgpu. None ⇒ caller keeps the per-token path.
7808    fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
7809        if self.o1_active() || self.attn_softcap > 0.0 {
7810            return None;
7811        }
7812        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
7813        if !graph_on || crate::gpu::graph_unsupported() {
7814            // Same memo as the decode site: this path builds the very
7815            // same graph, so a model it cannot build for must not be
7816            // walked again here either. Missing this guard was worth
7817            // 2.5x on an Adreno — 0.361 tok/s against 0.905 — because
7818            // the burst retried per token what decode had already given
7819            // up on.
7820            return None;
7821        }
7822        let emb = self.embed_single(t_next);
7823        let mut lg = Vec::new();
7824        let mut ids = Vec::new();
7825        match self.try_token_graph_wgpu_steps(
7826            &emb,
7827            position,
7828            &mut lg,
7829            k,
7830            Some(&mut ids),
7831            None,
7832            0,
7833            self.num_layers,
7834        ) {
7835            Some(Ok(_)) => {}
7836            Some(Err(())) => {
7837                // Preserve the backend's post-admission failure through the
7838                // Option-based burst API.  The decode caller consumes this
7839                // flag and clears the sequence instead of falling through
7840                // to a stale CPU recurrent state.
7841                self.graph_failed
7842                    .store(true, std::sync::atomic::Ordering::Relaxed);
7843                return None;
7844            }
7845            None => return None,
7846        }
7847        (ids.len() == k).then_some(ids)
7848    }
7849
7850    /// Multi-step greedy: k whole frames in ONE submit, argmax and re-embed
7851    /// on the device. `ids_out` receives the k winner ids; the hidden/logits
7852    /// outputs are NOT produced in that mode.
7853    fn try_token_graph_wgpu_steps(
7854        &self,
7855        hidden: &[f32],
7856        position: usize,
7857        logits_out: &mut Vec<f32>,
7858        steps: usize,
7859        ids_out: Option<&mut Vec<u32>>,
7860        layers_run: Option<&mut usize>,
7861        from: usize,
7862        upto_excl: usize,
7863    ) -> Option<Result<Vec<f32>, ()>> {
7864        // O(1) Nyström decode runs off the sealed state, not the KV cache the
7865        // graph mirrors — never take the graph while o1 is active.
7866        let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
7867        if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
7868            // Softcapped scores have no graph kernel yet — CPU owns them.
7869            // o1 rides the graph only behind CMF_O1_GPU=1 while the port
7870            // proves itself; without it the CPU path owns o1 as before.
7871            return None;
7872        }
7873        // Per-layer sealed o1 state for the graph. During prefill the
7874        // state is still Collecting -> views are None -> the graph
7875        // refuses below and the CPU prefill records the q trace and
7876        // seals, exactly as the o1 design requires.
7877        let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
7878            .map(|li| {
7879                if !o1_gpu {
7880                    return None;
7881                }
7882                self.kv_cache.layers[self.phys_layer(li)].o1_views()
7883            })
7884            .collect();
7885        if self.o1_active() && o1_gpu {
7886            // Any o1 layer not sealed (or degenerate exact-only) keeps the
7887            // whole token on the CPU: half-graph forwards would desync.
7888            let want: usize = (from..upto_excl)
7889                .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
7890                .count();
7891            let have = o1_views.iter().filter(|v| v.is_some()).count();
7892            if want == 0 || have != want {
7893                // The silent twin of the gpu-side o1 gates, found the
7894                // same way: a 15x decode drop with an empty log. Views
7895                // stay None until the layer's state SEALS, so `have`
7896                // lagging `want` early in a run is the o1 design working
7897                // — but it must say so, or the next reader spends a
7898                // night proving the kernels innocent.
7899                // On CHANGE, not once: the first decline is the legal
7900                // unsealed prefill, and a once-print buries the state
7901                // that matters — what the count reads AFTER the seal.
7902                use std::sync::atomic::{AtomicUsize, Ordering};
7903                static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
7904                let code = have * 1000 + want;
7905                if LAST.swap(code, Ordering::Relaxed) != code {
7906                    tracing::warn!(
7907                        "o1 graph: {have} of {want} layers sealed — per-op until all seal"
7908                    );
7909                }
7910                return None;
7911            }
7912        }
7913        let nh = self.num_heads;
7914        let (nkv, hd, rd) = self.layer_geom(0);
7915        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7916        let mut layers = Vec::with_capacity(upto_excl - from);
7917        let mut model = None;
7918        let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
7919        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7920            if let Some((m, i, kind, rs)) = t
7921                .graph_weight()
7922                .or_else(|| t.graph_weight_descriptor())
7923            {
7924                let name = &m.tensors[i].name;
7925                let prism = if crate::prism::is_inverse_embedding(m, name) {
7926                    crate::gpu::GraphPrismOp::InverseEmbedding
7927                } else if crate::prism::is_forward_weight(m, name) {
7928                    crate::gpu::GraphPrismOp::Forward
7929                } else {
7930                    crate::gpu::GraphPrismOp::None
7931                };
7932                return Some(crate::gpu::GraphW {
7933                    idx: i,
7934                    kind,
7935                    row_scale: rs,
7936                    data: &[],
7937                    prism,
7938                    affine: crate::prism::is_affine_target(m, name),
7939                });
7940            }
7941            // Small unquantized projections (GDN in_proj_a/b) stay f32.
7942            match t.as_f32() {
7943                Some(d) => Some(crate::gpu::GraphW {
7944                    idx: 0,
7945                    kind: 4,
7946                    row_scale: &[],
7947                    data: d,
7948                    prism: crate::gpu::GraphPrismOp::None,
7949                    affine: false,
7950                }),
7951                None => {
7952                    if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
7953                        eprintln!("batch graph: weight has no graph/f32 representation");
7954                    }
7955                    None
7956                }
7957            }
7958        }
7959        for li in from..upto_excl {
7960            let lw = &self.weights.layers[self.phys_layer(li)];
7961            if dbg {
7962                let ak = match &lw.attn {
7963                    AttnKind::Mla(_) => "Mla".into(),
7964                    AttnKind::Full {
7965                        output_gate, bias, ..
7966                    } => format!("Full gate={output_gate} bias={}", bias.is_some()),
7967                    AttnKind::LinearGdn(_) => "LinearGdn".into(),
7968                    AttnKind::Kda(_) => "Kda".into(),
7969                    AttnKind::Linear(_) => "Linear".into(),
7970                    AttnKind::ShortConv(_) => "ShortConv".into(),
7971                };
7972                let fk = match &lw.ffn {
7973                    FfnKind::Dense(_) => "Dense",
7974                    FfnKind::Moe(_) => "Moe",
7975                    FfnKind::DenseMoe(_) => "DenseMoe",
7976                };
7977                eprintln!("graph L{li}: attn={ak} ffn={fk}");
7978            }
7979            let gffn = match &lw.ffn {
7980                FfnKind::DenseMoe(_) => return None, // dual branch: CPU path
7981                // A tube layer is several matrices, not one — the
7982                // whole-layer graph has no shape for it yet.
7983                FfnKind::Dense(d) if !d.segs.is_empty() => return None,
7984                FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7985                    gate: gw(&d.gate_proj)?,
7986                    up: gw(&d.up_proj)?,
7987                    down: gw(&d.down_proj)?,
7988                },
7989                FfnKind::Moe(m) => {
7990                    // Adaptive τ and expert masks keep the CPU path, where
7991                    // they are implemented. Sigmoid routing with a selection
7992                    // bias (LFM2-MoE / DeepSeek noaux_tc), a routed scale ≠ 1
7993                    // and an UNGATED shared expert (HunYuan hy_v3: ×2.826 on
7994                    // the routed mix, the shared expert at weight 1) are all
7995                    // graphed — before, every such token fell to the per-op
7996                    // path whole (145 submits/token on Hy-MT2-30B-A3B).
7997                    if m.route_tau.is_some() || m.mask.is_some() {
7998                        return None;
7999                    }
8000                    let shared = m.shared.as_ref();
8001                    let has_shared = shared.is_some();
8002                    let shared_gated = matches!(shared, Some((_, Some(_))));
8003                    let sgate = match shared {
8004                        Some((_, Some(sg))) => gw(sg)?,
8005                        // No gate (hy_v3) or no shared expert at all: the
8006                        // router weight stands in so the plumbing stays
8007                        // total; the select kernels pin weight 1 or skip.
8008                        _ => gw(&m.router)?,
8009                    };
8010                    let router = gw(&m.router)?;
8011                    // The resident MoE kernels do not yet carry the
8012                    // descriptor-aware transform through router/shared-gate
8013                    // selection.  Refuse the complete layer instead of
8014                    // scoring with an untransformed Prism plane (the dense
8015                    // path has an explicit FWHT boundary below).
8016                    if router.prism != crate::gpu::GraphPrismOp::None
8017                        || sgate.prism != crate::gpu::GraphPrismOp::None
8018                        || router.affine
8019                        || sgate.affine
8020                    {
8021                        tracing::warn!(
8022                            "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
8023                        );
8024                        return None;
8025                    }
8026                    let inter = m.experts.first()?.gate_proj.rows();
8027                    let mut experts = Vec::with_capacity(m.experts.len() + 1);
8028                    // q4t or q4tp, but not both in one layer — the kernels
8029                    // are picked per layer, not per expert.
8030                    let mut q4tp: Option<bool> = None;
8031                    // The mixed 2-bit profile: q2tp gate/up over a q4tp
8032                    // down. Uniform across the layer, like `q4tp` itself.
8033                    let mut gu_q2: Option<bool> = None;
8034                    for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
8035                        if !matches!(e.act, Act::Silu)
8036                            || e.gate_proj.rows() != inter
8037                            || e.up_proj.rows() != inter
8038                        {
8039                            return None;
8040                        }
8041                        // Expert tensors are packed into one resident buffer
8042                        // and the MoE kernels have no transform slot per
8043                        // expert.  Keep the CPU/per-op owner for Prism or
8044                        // affine experts rather than silently using raw bytes.
8045                        for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
8046                            let Some((em, ei, _, _)) = expert_weight
8047                                .graph_weight()
8048                                .or_else(|| expert_weight.graph_weight_descriptor())
8049                            else {
8050                                return None;
8051                            };
8052                            let name = &em.tensors[ei].name;
8053                            if crate::prism::is_forward_weight(em, name)
8054                                || crate::prism::is_inverse_embedding(em, name)
8055                                || crate::prism::is_affine_target(em, name)
8056                            {
8057                                tracing::warn!(
8058                                    "resident MoE declined: expert Prism/affine transform is not implemented"
8059                                );
8060                                return None;
8061                            }
8062                        }
8063                        let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8064                            Some((mm, gi)) => (
8065                                mm,
8066                                gi,
8067                                e.up_proj.mapped_q4t()?.1,
8068                                e.down_proj.mapped_q4t()?.1,
8069                                false,
8070                                false,
8071                            ),
8072                            None => match e.gate_proj.mapped_q2tp() {
8073                                Some((mm, gi)) => (
8074                                    mm,
8075                                    gi,
8076                                    e.up_proj.mapped_q2tp()?.1,
8077                                    e.down_proj.mapped_q4tp()?.1,
8078                                    true,
8079                                    true,
8080                                ),
8081                                None => {
8082                                    let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8083                                    (
8084                                        mm,
8085                                        gi,
8086                                        e.up_proj.mapped_q4tp()?.1,
8087                                        e.down_proj.mapped_q4tp()?.1,
8088                                        true,
8089                                        false,
8090                                    )
8091                                }
8092                            },
8093                        };
8094                        if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
8095                        {
8096                            // The shared expert rides in the same packed
8097                            // buffer as the routed ones, so a layer that
8098                            // mixes layouts cannot be indexed by one stride.
8099                            // Say so: the symptom is a whole model quietly
8100                            // running its MoE on the CPU.
8101                            tracing::warn!(
8102                                "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."
8103                            );
8104                            return None;
8105                        }
8106                        model.get_or_insert_with(|| mm.clone());
8107                        experts.push((gi, ui, di));
8108                    }
8109                    crate::gpu::GraphFfn::Moe {
8110                        router,
8111                        shared_gate: sgate,
8112                        experts,
8113                        n_exp: m.experts.len(),
8114                        // CMF_TOPK_PROBE: timing probe only — output is WRONG.
8115                        // Fewer experts shrink the MoE arithmetic while the
8116                        // dispatch count stays identical, which is the only
8117                        // clean way to tell a launch-bound decode from a
8118                        // compute-bound one.
8119                        top_k: std::env::var("CMF_TOPK_PROBE")
8120                            .ok()
8121                            .and_then(|v| v.parse::<usize>().ok())
8122                            .filter(|k| *k > 0 && *k <= m.top_k)
8123                            .unwrap_or(m.top_k),
8124                        inter,
8125                        norm_topk: m.norm_topk_prob,
8126                        q4tp: q4tp?,
8127                        gu_q2: gu_q2.unwrap_or(false),
8128                        sigmoid: m.router_sigmoid,
8129                        bias: m.expert_bias.as_deref(),
8130                        has_shared,
8131                        shared_gated,
8132                        route_scale: m.routed_scaling,
8133                    }
8134                }
8135            };
8136            let attn = match &lw.attn {
8137                AttnKind::Full {
8138                    wq,
8139                    wk,
8140                    wv,
8141                    wo,
8142                    q_norm,
8143                    k_norm,
8144                    output_gate,
8145                    softplus_gate,
8146                    bias,
8147                } => {
8148                    if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
8149                        return None;
8150                    }
8151                    let (m, _, _, _) = wq
8152                        .graph_weight()
8153                        .or_else(|| wq.graph_weight_descriptor())?;
8154                    model = Some(m.clone());
8155                    crate::gpu::GraphAttn::Full {
8156                        wq: gw(wq)?,
8157                        wk: gw(wk)?,
8158                        wv: gw(wv)?,
8159                        wo: gw(wo)?,
8160                        q_norm: q_norm.as_deref(),
8161                        k_norm: k_norm.as_deref(),
8162                        late_qk_norm: self.qk_norm_after_rope,
8163                        bias: bias
8164                            .as_ref()
8165                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8166                        output_gate: *output_gate,
8167                        cpu_k: self.kv_cache.layers[li].k_heads(),
8168                        cpu_v: self.kv_cache.layers[li].v_heads(),
8169                    }
8170                }
8171                AttnKind::LinearGdn(w) => {
8172                    let cfg = self.gdn_cfg?;
8173                    let (m, _, _, _) = w
8174                        .in_proj_qkv
8175                        .graph_weight()
8176                        .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
8177                    model = Some(m.clone());
8178                    crate::gpu::GraphAttn::Gdn {
8179                        qkv: gw(&w.in_proj_qkv)?,
8180                        z: gw(&w.in_proj_z)?,
8181                        a: gw(&w.in_proj_a)?,
8182                        b: gw(&w.in_proj_b)?,
8183                        out: gw(&w.out_proj)?,
8184                        conv1d: &w.conv1d,
8185                        a_log: &w.a_log,
8186                        dt_bias: &w.dt_bias,
8187                        norm: &w.norm,
8188                        nv: cfg.num_v_heads,
8189                        nk: cfg.num_k_heads,
8190                        dk: cfg.key_head_dim,
8191                        dv: cfg.value_head_dim,
8192                        kk: cfg.conv_kernel,
8193                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8194                    }
8195                }
8196                AttnKind::ShortConv(w) => {
8197                    let cfg = self.short_conv_cfg?;
8198                    let (m, _, _, _) = w
8199                        .in_proj
8200                        .graph_weight()
8201                        .or_else(|| w.in_proj.graph_weight_descriptor())?;
8202                    model = Some(m.clone());
8203                    crate::gpu::GraphAttn::ShortConv {
8204                        inp: gw(&w.in_proj)?,
8205                        out: gw(&w.out_proj)?,
8206                        taps: &w.conv,
8207                        kernel: cfg.kernel,
8208                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8209                    }
8210                }
8211                _ => return None,
8212            };
8213            layers.push(crate::gpu::GraphLayer {
8214                input_norm: &lw.input_norm,
8215                attn,
8216                post_norm: &lw.post_norm,
8217                ffn: gffn,
8218            });
8219        }
8220        let model = model?;
8221        // Fold final-norm + lm_head into the graph when this call wants logits
8222        // and the lm_head is a graphable (quantized) weight — the graph then
8223        // reads back logits (into logits_out) instead of the hidden, dropping
8224        // the separate CPU/GPU lm_head op + its sync. Never the f32 fallback:
8225        // an unquantized lm_head is vocab·hidden and must not be uploaded.
8226        let lm_gw = if upto_excl == self.num_layers
8227            && self.graph_want_logits
8228            && std::env::var("CMF_GPU_LMHEAD")
8229                .map(|v| v != "0")
8230                .unwrap_or(true)
8231        {
8232            self.weights
8233                .lm_head
8234                .graph_weight()
8235                .or_else(|| self.weights.lm_head.graph_weight_descriptor())
8236                .map(|(m, i, kind, rs)| {
8237                let name = &m.tensors[i].name;
8238                let prism = if crate::prism::is_inverse_embedding(m, name) {
8239                    crate::gpu::GraphPrismOp::InverseEmbedding
8240                } else if crate::prism::is_forward_weight(m, name) {
8241                    crate::gpu::GraphPrismOp::Forward
8242                } else {
8243                    crate::gpu::GraphPrismOp::None
8244                };
8245                (
8246                    crate::gpu::GraphW {
8247                        idx: i,
8248                        kind,
8249                        row_scale: rs,
8250                        data: &[],
8251                        prism,
8252                        affine: crate::prism::is_affine_target(m, name),
8253                    },
8254                    self.weights.lm_head.rows(),
8255                )
8256            })
8257        } else {
8258            None
8259        };
8260        let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
8261        // Multi-step re-embeds the winner on the device.
8262        let emb_gw = if steps > 1 {
8263            self.weights
8264                .embed_tokens
8265                .graph_weight()
8266                .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
8267                .map(|(m, i, kind, rs)| {
8268                    let name = &m.tensors[i].name;
8269                    let prism = if crate::prism::is_inverse_embedding(m, name) {
8270                        crate::gpu::GraphPrismOp::InverseEmbedding
8271                    } else if crate::prism::is_forward_weight(m, name) {
8272                        crate::gpu::GraphPrismOp::Forward
8273                    } else {
8274                        crate::gpu::GraphPrismOp::None
8275                    };
8276                    (
8277                        crate::gpu::GraphW {
8278                            idx: i,
8279                            kind,
8280                            row_scale: rs,
8281                            data: &[],
8282                            prism,
8283                            affine: crate::prism::is_affine_target(m, name),
8284                        },
8285                        self.weights.embed_tokens.rows(),
8286                        self.embed_multiplier,
8287                    )
8288                })
8289        } else {
8290            None
8291        };
8292
8293        // Loop boundaries: virtual layer indices after which final_norm is
8294        // applied (mid-stack only; the GLOBAL last layer's norm folds into
8295        // lm_head). Span-relative — the executor compares its enumerate
8296        // index. A span ending mid-stack keeps its boundary norm even when
8297        // it is the span's own last layer.
8298        let loop_norm_at: Vec<usize> = if self.loop_final_norm {
8299            (from..upto_excl.min(self.num_layers - 1))
8300                .filter(|&li| (li + 1) % self.physical_layers == 0)
8301                .map(|li| li - from)
8302                .collect()
8303        } else {
8304            Vec::new()
8305        };
8306        let mut h = hidden.to_vec();
8307        // The normal decode path only needs the fused lm-head logits.  A
8308        // CMF_LOGIT_DUMP diagnostic, however, promises a prompt-boundary
8309        // post-stack hidden alongside those logits; request the existing
8310        // second readback only for that explicit probe instead of dumping
8311        // the input copy left in `h` by a folded-head graph.
8312        let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
8313        let outcome = crate::gpu::forward_token_graph(
8314            &model,
8315            self.graph_kv_id,
8316            &layers,
8317            &o1_views,
8318            self.o1_epoch,
8319            &self.inv_freq,
8320            &mut h,
8321            nh,
8322            nkv,
8323            hd,
8324            self.attn_scale,
8325            rd,
8326            self.hidden_size,
8327            self.intermediate_size,
8328            position,
8329            self.kv_cache.max_seq_len,
8330            gemma,
8331            self.rms_eps as f32,
8332            lm,
8333            &self.weights.final_norm,
8334            logits_out,
8335            &loop_norm_at,
8336            steps,
8337            emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
8338            ids_out,
8339            layers_run,
8340            from,
8341            dump_hidden,
8342        );
8343        match outcome {
8344            crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
8345            crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
8346            crate::gpu::TokenGraphOutcome::Declined => None,
8347        }
8348    }
8349
8350    /// Batched prefill: k contiguous prompt positions through the whole wgpu
8351    /// graph in ONE submit (projections/FFN as GEMMs). `hiddens` is [k·hidden]
8352    /// in/out (embeddings in, layer output out); KV mirror / GDN state advance.
8353    /// false ⇒ unsupported → caller keeps the per-position graph.
8354    /// The b-row Metal graph plan for the whole model: every layer as a
8355    /// GDN run or a full-attention item, all-or-nothing (a layer outside the
8356    /// graph's contract → None, the caller runs plain). Shared by the
8357    /// speculative verify and the batched prefill.
8358    #[cfg(target_os = "macos")]
8359    #[allow(clippy::type_complexity)]
8360    fn metal_rows_plan(
8361        &self,
8362    ) -> Option<(
8363        Vec<MetalRowsItem<'_>>,
8364        std::sync::Arc<cortiq_core::CmfModel>,
8365        Option<crate::gpu_metal::GdnGpuCfg>,
8366    )> {
8367        use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
8368        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
8369        if !graph_force
8370            || !crate::gpu::enabled_here()
8371            || std::env::var("CMF_GPU_BLOCK")
8372                .map(|v| v == "0")
8373                .unwrap_or(false)
8374            || self.attn_softcap > 0.0
8375            || self.o1_active()
8376            || self.swa.is_some()
8377            || self.global_attn.is_some()
8378            || self.attention_heads_per_layer.is_some()
8379            || self.attn_v_norm
8380            || self.loop_final_norm
8381        {
8382            return None;
8383        }
8384        let attend_contract = self.head_dim % 4 == 0
8385            && self.head_dim <= 256
8386            && self.rotary_dim >= 2
8387            && self.rotary_dim <= self.head_dim
8388            && (self.rotary_dim / 2) % 32 == 0
8389            && self.num_kv_heads > 0
8390            && self.num_heads % self.num_kv_heads == 0;
8391        if !attend_contract {
8392            return None;
8393        }
8394        let mut plan: Vec<MetalRowsItem> = Vec::new();
8395        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
8396        for li in 0..self.num_layers {
8397            let lw = &self.weights.layers[self.phys_layer(li)];
8398            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
8399                return None;
8400            }
8401            let ffn = match &lw.ffn {
8402                FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
8403                    let (Some(g), Some(u), Some(dn)) = (
8404                        d.gate_proj.metal_graph_parts(),
8405                        d.up_proj.metal_graph_parts(),
8406                        d.down_proj.metal_graph_parts(),
8407                    ) else {
8408                        return None;
8409                    };
8410                    MetalFfn::Dense {
8411                        gate: g,
8412                        up: u,
8413                        down: dn,
8414                    }
8415                }
8416                _ => return None,
8417            };
8418            match &lw.attn {
8419                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
8420                    let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
8421                        w.in_proj_qkv.metal_graph_parts(),
8422                        w.in_proj_z.metal_graph_parts(),
8423                        w.in_proj_a.f32_parts(),
8424                        w.in_proj_b.f32_parts(),
8425                        w.out_proj.metal_graph_parts(),
8426                    ) else {
8427                        return None;
8428                    };
8429                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
8430                        model_ref.get_or_insert_with(|| model.clone());
8431                    }
8432                    let gl = GdnGpuLayer {
8433                        attn_norm: &lw.input_norm,
8434                        post_norm: &lw.post_norm,
8435                        qkv,
8436                        z,
8437                        a,
8438                        b: bb,
8439                        out,
8440                        ffn,
8441                        conv1d: &w.conv1d,
8442                        a_log: &w.a_log,
8443                        dt_bias: &w.dt_bias,
8444                        gnorm: &w.norm,
8445                    };
8446                    match plan.last_mut() {
8447                        Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
8448                        _ => plan.push(MetalRowsItem::Gdn {
8449                            run: vec![gl],
8450                            first: li,
8451                        }),
8452                    }
8453                }
8454                AttnKind::Full {
8455                    wq,
8456                    wk,
8457                    wv,
8458                    wo,
8459                    q_norm,
8460                    k_norm,
8461                    output_gate,
8462                    softplus_gate: None,
8463                    bias: None,
8464                } => {
8465                    let (Some(pq), Some(pk), Some(pv), Some(po)) =
8466                        (
8467                            wq.metal_graph_parts(),
8468                            wk.metal_graph_parts(),
8469                            wv.metal_graph_parts(),
8470                            wo.metal_graph_parts(),
8471                        )
8472                    else {
8473                        return None;
8474                    };
8475                    if let QTensor::Mapped { model, .. } = wq {
8476                        model_ref.get_or_insert_with(|| model.clone());
8477                    }
8478                    let cache = &self.kv_cache.layers[li];
8479                    if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
8480                        return None;
8481                    }
8482                    plan.push(MetalRowsItem::Attn {
8483                        l: AttnGpuLayer {
8484                            attn_norm: &lw.input_norm,
8485                            post_norm: &lw.post_norm,
8486                            wq: pq,
8487                            wk: pk,
8488                            wv: pv,
8489                            wo: po,
8490                            ffn,
8491                        },
8492                        li,
8493                        q_norm: q_norm.as_deref(),
8494                        k_norm: k_norm.as_deref(),
8495                        output_gate: *output_gate,
8496                    });
8497                }
8498                _ => return None,
8499            }
8500        }
8501        let model = model_ref?;
8502        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
8503            nv: cfg.num_v_heads,
8504            nk: cfg.num_k_heads,
8505            dk: cfg.key_head_dim,
8506            dv: cfg.value_head_dim,
8507            kk: cfg.conv_kernel,
8508            hidden: self.hidden_size,
8509            inter: self.intermediate_size,
8510            c_dim: cfg.conv_dim(),
8511            eps: cfg.rms_eps as f32,
8512            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8513        });
8514        Some((plan, model, gcfg))
8515    }
8516
8517    /// `AttnDeviceParams` for a plan item over the CPU cache as it stands.
8518    #[cfg(target_os = "macos")]
8519    #[allow(clippy::too_many_arguments)]
8520    fn metal_attn_params<'a>(
8521        li: usize,
8522        cache: &'a crate::kv_cache::LayerKvCache,
8523        q_norm: Option<&'a [f32]>,
8524        k_norm: Option<&'a [f32]>,
8525        output_gate: bool,
8526        inv_freq: &'a [f32],
8527        geom: (usize, usize, usize, usize),
8528        pos0: usize,
8529        kv_id: u64,
8530        scale: f32,
8531        eps: f32,
8532        gemma: bool,
8533        late_qk_norm: bool,
8534    ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
8535        let (nh, nkv, hd, rd) = geom;
8536        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8537        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8538        let cpu_stored = cpu_k[0].len() / hd;
8539        (
8540            crate::gpu_metal::AttnDeviceParams {
8541                kv_id,
8542                layer: li,
8543                nh,
8544                nkv,
8545                hd,
8546                rd,
8547                position: pos0,
8548                scale,
8549                eps,
8550                gemma,
8551                late_qk_norm,
8552                output_gate,
8553                q_norm,
8554                k_norm,
8555                inv_freq,
8556                cpu_k,
8557                cpu_v,
8558                cpu_stored,
8559                o1: None,
8560            },
8561            cpu_stored,
8562        )
8563    }
8564
8565    /// Run the rows plan over `hiddens` (b rows at `pos0..`): validate,
8566    /// encode every item, optionally the head, sync. Returns the graph
8567    /// (for the commit / state finish) plus the GDN layer indices and the
8568    /// attention layers with the row count they were encoded against.
8569    #[cfg(target_os = "macos")]
8570    #[allow(clippy::type_complexity)]
8571    fn metal_rows_run(
8572        &mut self,
8573        hiddens: &mut [f32],
8574        pos0: usize,
8575        b: usize,
8576        prefill: bool,
8577        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8578    ) -> MetalRowsRun {
8579        use crate::gpu_metal::{GraphDims, VerifyGraph};
8580        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
8581        for l in &mut self.kv_cache.layers {
8582            if l.linear_state.len() != want && want > 0 {
8583                l.linear_state = vec![0f32; want];
8584            }
8585        }
8586        let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
8587            return MetalRowsRun::Declined;
8588        };
8589        let dims = GraphDims {
8590            hidden: self.hidden_size,
8591            eps: self.rms_eps as f32,
8592            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8593        };
8594        let Some(mut graph) = (if prefill {
8595            VerifyGraph::new_prefill(&model, dims, hiddens, b)
8596        } else {
8597            VerifyGraph::new(&model, dims, hiddens, b)
8598        }) else {
8599            return MetalRowsRun::Declined;
8600        };
8601        let geom = (
8602            self.num_heads,
8603            self.num_kv_heads,
8604            self.head_dim,
8605            self.rotary_dim,
8606        );
8607        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8608        let eps = self.rms_eps as f32;
8609        let kv_id = self.graph_kv_id;
8610        let inv_freq = self.inv_freq.clone();
8611        for item in &plan {
8612            let ok = match item {
8613                MetalRowsItem::Gdn { run, .. } => gcfg
8614                    .as_ref()
8615                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
8616                    .unwrap_or(false),
8617                MetalRowsItem::Attn {
8618                    l,
8619                    li,
8620                    q_norm,
8621                    k_norm,
8622                    output_gate,
8623                } => {
8624                    let (p, _) = Self::metal_attn_params(
8625                        *li,
8626                        &self.kv_cache.layers[*li],
8627                        *q_norm,
8628                        *k_norm,
8629                        *output_gate,
8630                        &inv_freq,
8631                        geom,
8632                        pos0,
8633                        kv_id,
8634                        self.attn_scale,
8635                        eps,
8636                        gemma,
8637                        self.qk_norm_after_rope,
8638                    );
8639                    graph.attn_ok(l, &p)
8640                }
8641            };
8642            if !ok {
8643                use std::sync::atomic::{AtomicBool, Ordering};
8644                static SAID: AtomicBool = AtomicBool::new(false);
8645                if !SAID.swap(true, Ordering::Relaxed) {
8646                    tracing::warn!("metal rows graph: a layer failed preflight — declining");
8647                }
8648                return MetalRowsRun::Declined;
8649            }
8650        }
8651        let lm = match &spec {
8652            Some((lm, _, _)) => {
8653                if !graph.lm_head_ok(*lm) {
8654                    return MetalRowsRun::Declined;
8655                }
8656                Some(*lm)
8657            }
8658            None => None,
8659        };
8660        let mut gdn_layers = Vec::new();
8661        let mut attn_layers = Vec::new();
8662        for item in &plan {
8663            match item {
8664                MetalRowsItem::Gdn { run, first } => {
8665                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
8666                        .iter()
8667                        .map(|l| l.linear_state.as_slice())
8668                        .collect();
8669                    if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
8670                        return MetalRowsRun::Declined;
8671                    }
8672                    gdn_layers.extend(*first..*first + run.len());
8673                }
8674                MetalRowsItem::Attn {
8675                    l,
8676                    li,
8677                    q_norm,
8678                    k_norm,
8679                    output_gate,
8680                } => {
8681                    let (p, cpu_stored) = Self::metal_attn_params(
8682                        *li,
8683                        &self.kv_cache.layers[*li],
8684                        *q_norm,
8685                        *k_norm,
8686                        *output_gate,
8687                        &inv_freq,
8688                        geom,
8689                        pos0,
8690                        kv_id,
8691                        self.attn_scale,
8692                        eps,
8693                        gemma,
8694                        self.qk_norm_after_rope,
8695                    );
8696                    if !graph.encode_attn_b(l, &p) {
8697                        return MetalRowsRun::Declined;
8698                    }
8699                    attn_layers.push((*li, cpu_stored));
8700                }
8701            }
8702        }
8703        if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
8704            if !graph.encode_lm_head_b(final_norm, lm) {
8705                return MetalRowsRun::Declined;
8706            }
8707        }
8708        if !graph.sync() {
8709            return MetalRowsRun::Failed;
8710        }
8711        if let Some((lm, _, logits)) = spec {
8712            logits.resize(b * lm.1, 0.0);
8713            if !graph.read_logits(logits) {
8714                return MetalRowsRun::Failed;
8715            }
8716        }
8717        if !graph.read_hidden(hiddens) {
8718            return MetalRowsRun::Failed;
8719        }
8720        MetalRowsRun::Completed(MetalVerifyPending {
8721            graph,
8722            gdn_layers,
8723            attn_layers,
8724        })
8725    }
8726
8727    /// Native-Metal twin of `try_batch_graph_wgpu`: the b rows through the
8728    /// whole model on the `VerifyGraph` (one submit), the head folded in
8729    /// when `spec` asks; `hiddens` come back as the last layer's output
8730    /// rows, `spec.2` as `[b][lm_rows]` logits. The graph is parked in
8731    /// `metal_verify` for `metal_verify_commit`.
8732    #[cfg(target_os = "macos")]
8733    fn try_batch_graph_metal(
8734        &mut self,
8735        hiddens: &mut [f32],
8736        positions: &[usize],
8737        b: usize,
8738        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8739    ) -> crate::gpu::BatchGraphOutcome {
8740        let _t0 = std::time::Instant::now();
8741        if positions.len() != b
8742            || positions.windows(2).any(|w| w[1] != w[0] + 1)
8743            || hiddens.len() != b * self.hidden_size
8744        {
8745            return crate::gpu::BatchGraphOutcome::Declined;
8746        }
8747        let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec) {
8748            MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
8749            MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
8750            MetalRowsRun::Completed(pending) => pending,
8751        };
8752        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8753            eprintln!(
8754                "metal-verify: {:.1} ms | b={b}",
8755                _t0.elapsed().as_secs_f64() * 1e3
8756            );
8757        }
8758        self.metal_verify = Some(pending);
8759        crate::gpu::BatchGraphOutcome::Completed
8760    }
8761
8762    /// Batched prefill on the Metal rows graph: `ids` (≤ 512) at
8763    /// `start_pos..`, states written in place, K/V rows appended to the
8764    /// CPU caches; optional final norm/head logits are returned in `spec`.
8765    /// Declined means no command buffer was admitted; Failed is terminal.
8766    #[cfg(target_os = "macos")]
8767    fn prefill_rows_metal(
8768        &mut self,
8769        ids: &[u32],
8770        start_pos: usize,
8771        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8772    ) -> MetalPrefillOutcome {
8773        let b = ids.len();
8774        if b == 0 || b > 512 {
8775            return MetalPrefillOutcome::Declined;
8776        }
8777        METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8778        let with_head = spec.is_some();
8779        let hs = self.hidden_size;
8780        let mut hiddens = vec![0f32; b * hs];
8781        for (j, &id) in ids.iter().enumerate() {
8782            let e = self.embed_single(id);
8783            hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
8784        }
8785        let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec) {
8786            MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
8787            MetalRowsRun::Failed => {
8788                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8789                return MetalPrefillOutcome::Failed;
8790            }
8791            MetalRowsRun::Completed(pending) => pending,
8792        };
8793        // states are final: copy them to the owners
8794        let idxs = pending.gdn_layers.clone();
8795        let mut outs: Vec<&mut [f32]> = self
8796            .kv_cache
8797            .layers
8798            .iter_mut()
8799            .enumerate()
8800            .filter(|(i, _)| idxs.binary_search(i).is_ok())
8801            .map(|(_, l)| l.linear_state.as_mut_slice())
8802            .collect();
8803        if !pending.graph.finish_states(&mut outs) {
8804            METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8805            return MetalPrefillOutcome::Failed;
8806        }
8807        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8808        // Read every layer before mutating any CPU cache.  A missing mirror
8809        // row is a terminal graph failure, not a reason to append a partial
8810        // prefix and replay the remainder serially.
8811        let mut rows = Vec::with_capacity(pending.attn_layers.len());
8812        for (li, cpu_stored) in &pending.attn_layers {
8813            let mut kbuf = vec![0f32; b * nkv * hd];
8814            let mut vbuf = vec![0f32; b * nkv * hd];
8815            if !crate::gpu_metal::kv_mirror_read_rows(
8816                self.graph_kv_id,
8817                *li,
8818                nkv,
8819                hd,
8820                *cpu_stored,
8821                b,
8822                &mut kbuf,
8823                &mut vbuf,
8824            ) {
8825                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8826                return MetalPrefillOutcome::Failed;
8827            }
8828            rows.push((*li, *cpu_stored, kbuf, vbuf));
8829        }
8830        for (li, cpu_stored, kbuf, vbuf) in rows {
8831            let cache = &mut self.kv_cache.layers[li];
8832            for r in 0..b {
8833                cache.append(
8834                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8835                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8836                    &[],
8837                );
8838            }
8839            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
8840        }
8841        METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8842        if with_head {
8843            METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8844        }
8845        MetalPrefillOutcome::Completed(hiddens)
8846    }
8847
8848    #[cfg(target_os = "macos")]
8849    fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
8850        self.prefill_rows_metal(ids, start_pos, None)
8851    }
8852
8853    /// Exact teacher-forced NLL through the ordinary Metal rows graph.  This
8854    /// is intentionally separate from the serial TokenGraph scorer: every
8855    /// chunk owns a real b-row graph/head completion and the recurrent/KV
8856    /// handoff is committed before the next chunk begins.
8857    #[cfg(target_os = "macos")]
8858    fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
8859        if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
8860            return MetalBatchNllOutcome::Declined;
8861        }
8862        let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
8863            return MetalBatchNllOutcome::Declined;
8864        };
8865        let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
8866            .ok()
8867            .and_then(|v| v.parse::<usize>().ok())
8868            .filter(|&v| (1..=512).contains(&v))
8869            .unwrap_or(32);
8870        let final_norm = self.weights.final_norm.clone();
8871        let mut nll = 0.0f64;
8872        let mut count = 0usize;
8873        let mut pos = 0usize;
8874        let mut completed = 0usize;
8875        while pos < ids.len() {
8876            let end = (pos + chunk).min(ids.len());
8877            let mut logits = Vec::new();
8878            let outcome = self.prefill_rows_metal(
8879                &ids[pos..end],
8880                pos,
8881                Some((lm, &final_norm, &mut logits)),
8882            );
8883            match outcome {
8884                MetalPrefillOutcome::Declined => {
8885                    return if completed == 0 {
8886                        MetalBatchNllOutcome::Declined
8887                    } else {
8888                        MetalBatchNllOutcome::Failed(format!(
8889                            "ordinary Metal NLL batch declined after {completed} chunks"
8890                        ))
8891                    };
8892                }
8893                MetalPrefillOutcome::Failed => {
8894                    return MetalBatchNllOutcome::Failed(
8895                        "ordinary Metal NLL batch failed after admission".to_string(),
8896                    );
8897                }
8898                MetalPrefillOutcome::Completed(_) => {}
8899            }
8900            completed += 1;
8901            let vocab = self.vocab_size.min(lm.1);
8902            if logits.len() != (end - pos) * lm.1 || vocab == 0 {
8903                return MetalBatchNllOutcome::Failed(
8904                    "ordinary Metal NLL head returned an invalid shape".to_string(),
8905                );
8906            }
8907            for row in 0..(end - pos) {
8908                let absolute = pos + row;
8909                if absolute < start || absolute + 1 >= ids.len() {
8910                    continue;
8911                }
8912                let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
8913                if let Some(mu) = self.logit_multiplier {
8914                    for v in lg.iter_mut() {
8915                        *v *= mu;
8916                    }
8917                }
8918                if let Some(c) = self.final_softcap {
8919                    for v in lg.iter_mut() {
8920                        *v = c * (*v / c).tanh();
8921                    }
8922                }
8923                let target = ids[absolute + 1] as usize;
8924                if target >= vocab {
8925                    return MetalBatchNllOutcome::Failed(format!(
8926                        "target token {target} exceeds Metal head rows {vocab}"
8927                    ));
8928                }
8929                let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8930                let lse: f64 = lg
8931                    .iter()
8932                    .map(|&v| ((v - max) as f64).exp())
8933                    .sum::<f64>()
8934                    .ln()
8935                    + max as f64;
8936                nll += lse - lg[target] as f64;
8937                count += 1;
8938            }
8939            pos = end;
8940        }
8941        MetalBatchNllOutcome::Completed(nll, count)
8942    }
8943
8944    /// Commit a Metal verify round: replay the GDN recurrences over the
8945    /// `a + 1` accepted positions into the CPU states, append the accepted
8946    /// K/V rows from the mirrors to the CPU caches, re-point the mirrors.
8947    #[cfg(target_os = "macos")]
8948    fn metal_verify_commit(&mut self, a: usize) -> bool {
8949        let Some(mut pending) = self.metal_verify.take() else {
8950            return false;
8951        };
8952        let n = a + 1;
8953        // encode order == ascending layer order (the plan walks 0..layers)
8954        let idxs = pending.gdn_layers.clone();
8955        let mut outs: Vec<&mut [f32]> = self
8956            .kv_cache
8957            .layers
8958            .iter_mut()
8959            .enumerate()
8960            .filter(|(i, _)| idxs.binary_search(i).is_ok())
8961            .map(|(_, l)| l.linear_state.as_mut_slice())
8962            .collect();
8963        if !pending.graph.commit(n, &mut outs) {
8964            return false;
8965        }
8966        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8967        // Read every layer before mutating any CPU cache.  Missing rows are
8968        // terminal after the replay has executed; never append a partial KV
8969        // prefix and continue on a serial path.
8970        let mut rows = Vec::with_capacity(pending.attn_layers.len());
8971        for (li, cpu_stored) in &pending.attn_layers {
8972            let mut kbuf = vec![0f32; n * nkv * hd];
8973            let mut vbuf = vec![0f32; n * nkv * hd];
8974            if !crate::gpu_metal::kv_mirror_read_rows(
8975                self.graph_kv_id,
8976                *li,
8977                nkv,
8978                hd,
8979                *cpu_stored,
8980                n,
8981                &mut kbuf,
8982                &mut vbuf,
8983            ) {
8984                return false;
8985            }
8986            rows.push((*li, *cpu_stored, kbuf, vbuf));
8987        }
8988        for (li, cpu_stored, kbuf, vbuf) in rows {
8989            let cache = &mut self.kv_cache.layers[li];
8990            for r in 0..n {
8991                cache.append(
8992                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8993                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8994                    &[],
8995                );
8996            }
8997            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
8998        }
8999        true
9000    }
9001
9002    /// The round's warm-ups as ONE b-row graph run over the MTP block on
9003    /// Metal: `pairs` = (trunk hidden, next token) at consecutive positions
9004    /// from `first_pos`; the block's input projection is folded in, the
9005    /// appended K/V rows are pulled into the CPU MTP cache. False = the
9006    /// graph declined (nothing appended).
9007    #[cfg(target_os = "macos")]
9008    fn mtp_warm_batch_metal(
9009        &mut self,
9010        m: &mut MtpModule,
9011        pairs: &[(&[f32], u32)],
9012        first_pos: usize,
9013    ) -> bool {
9014        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
9015        let b = pairs.len();
9016        if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
9017            return false;
9018        }
9019        let AttnKind::Full {
9020            wq,
9021            wk,
9022            wv,
9023            wo,
9024            q_norm,
9025            k_norm,
9026            output_gate,
9027            softplus_gate: None,
9028            bias: None,
9029        } = &m.layer.attn
9030        else {
9031            return false;
9032        };
9033        let FfnKind::Dense(d) = &m.layer.ffn else {
9034            return false;
9035        };
9036        if !d.segs.is_empty() {
9037            return false;
9038        }
9039        let (Some(pq), Some(pk), Some(pv), Some(po)) =
9040            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
9041        else {
9042            return false;
9043        };
9044        let (Some(g), Some(u), Some(dn)) = (
9045            d.gate_proj.q1_parts(),
9046            d.up_proj.q1_parts(),
9047            d.down_proj.q1_parts(),
9048        ) else {
9049            return false;
9050        };
9051        let Some(eh) = m.eh_proj.q1_parts() else {
9052            return false;
9053        };
9054        let QTensor::Mapped { model, .. } = wq else {
9055            return false;
9056        };
9057        let model = model.clone();
9058        let hs = self.hidden_size;
9059        // [enorm(embed(tok)); hnorm(hidden)] rows
9060        let mut cat = vec![0f32; b * 2 * hs];
9061        for (j, (h, tok)) in pairs.iter().enumerate() {
9062            let e = self.embed_single(*tok);
9063            let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
9064            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
9065            inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
9066        }
9067        let dims = GraphDims {
9068            hidden: hs,
9069            eps: self.rms_eps as f32,
9070            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9071        };
9072        let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
9073            return false;
9074        };
9075        let l = AttnGpuLayer {
9076            attn_norm: &m.layer.input_norm,
9077            post_norm: &m.layer.post_norm,
9078            wq: pq,
9079            wk: pk,
9080            wv: pv,
9081            wo: po,
9082            ffn: MetalFfn::Dense {
9083                gate: g,
9084                up: u,
9085                down: dn,
9086            },
9087        };
9088        let (nh, nkv, hd, rd) = (
9089            self.num_heads,
9090            self.num_kv_heads,
9091            self.head_dim,
9092            self.rotary_dim,
9093        );
9094        let inv_freq = self.inv_freq.clone();
9095        let cpu_stored;
9096        {
9097            let cache = &m.kv;
9098            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9099            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9100            cpu_stored = cpu_k[0].len() / hd;
9101            if cpu_stored != first_pos {
9102                return false;
9103            }
9104            let p = AttnDeviceParams {
9105                kv_id: self.mtp_kv_id(),
9106                layer: Self::MTP_LAYER_BASE,
9107                nh,
9108                nkv,
9109                hd,
9110                rd,
9111                position: first_pos,
9112                scale: self.attn_scale,
9113                eps: self.rms_eps as f32,
9114                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9115                late_qk_norm: self.qk_norm_after_rope,
9116                output_gate: *output_gate,
9117                q_norm: q_norm.as_deref(),
9118                k_norm: k_norm.as_deref(),
9119                inv_freq: &inv_freq,
9120                cpu_k,
9121                cpu_v,
9122                cpu_stored,
9123                o1: None,
9124            };
9125            if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
9126                return false;
9127            }
9128        }
9129        if !graph.sync() {
9130            return false;
9131        }
9132        let mut kbuf = vec![0f32; b * nkv * hd];
9133        let mut vbuf = vec![0f32; b * nkv * hd];
9134        if !crate::gpu_metal::kv_mirror_read_rows(
9135            self.mtp_kv_id(),
9136            Self::MTP_LAYER_BASE,
9137            nkv,
9138            hd,
9139            cpu_stored,
9140            b,
9141            &mut kbuf,
9142            &mut vbuf,
9143        ) {
9144            return false;
9145        }
9146        for r in 0..b {
9147            m.kv.append(
9148                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9149                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9150                &[],
9151            );
9152        }
9153        crate::gpu_metal::kv_mirror_set_stored(
9154            self.mtp_kv_id(),
9155            Self::MTP_LAYER_BASE,
9156            cpu_stored + b,
9157        );
9158        true
9159    }
9160
9161    /// Draft-head shortlist size: `CMF_DRAFT_VOCAB` rows (default 65536,
9162    /// capped at the head; 0 = full head).
9163    fn draft_vocab_rows(head_rows: usize) -> usize {
9164        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9165        let n = *N.get_or_init(|| {
9166            std::env::var("CMF_DRAFT_VOCAB")
9167                .ok()
9168                .and_then(|v| v.parse().ok())
9169                .unwrap_or(65536)
9170        });
9171        if n == 0 { head_rows } else { n.min(head_rows) }
9172    }
9173
9174    /// One MTP block step on the native Metal token graph: block input on
9175    /// the host, the attention layer + FFN device-resident over the MTP
9176    /// mirror, the head folded in when `want_logits`. The appended K/V row
9177    /// is pulled into the CPU MTP cache (owner of record) after the sync.
9178    #[cfg(target_os = "macos")]
9179    fn mtp_step_metal(
9180        &mut self,
9181        m: &mut MtpModule,
9182        hidden: &[f32],
9183        next_token: u32,
9184        position: usize,
9185        want_logits: bool,
9186    ) -> Option<(Vec<f32>, Vec<f32>)> {
9187        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
9188        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9189            || !crate::gpu::q1_force()
9190            || !crate::gpu::enabled_here()
9191            || self.attn_softcap > 0.0
9192            || self.attention_heads_per_layer.is_some()
9193            || m.kv.mode != crate::kv_cache::KvMode::F32
9194            || m.kv.o1.is_some()
9195        {
9196            return None;
9197        }
9198        let AttnKind::Full {
9199            wq,
9200            wk,
9201            wv,
9202            wo,
9203            q_norm,
9204            k_norm,
9205            output_gate,
9206            softplus_gate: None,
9207            bias: None,
9208        } = &m.layer.attn
9209        else {
9210            return None;
9211        };
9212        let FfnKind::Dense(d) = &m.layer.ffn else {
9213            return None;
9214        };
9215        if d.act != Act::Silu || !d.segs.is_empty() {
9216            return None;
9217        }
9218        let (pq, pk, pv, po) = (
9219            wq.q1_parts()?,
9220            wk.q1_parts()?,
9221            wv.q1_parts()?,
9222            wo.q1_parts()?,
9223        );
9224        let (g, u, dn) = (
9225            d.gate_proj.q1_parts()?,
9226            d.up_proj.q1_parts()?,
9227            d.down_proj.q1_parts()?,
9228        );
9229        let QTensor::Mapped { model, .. } = wq else {
9230            return None;
9231        };
9232        let model = model.clone();
9233        let lm = if want_logits {
9234            Some(self.weights.lm_head.q1_parts()?)
9235        } else {
9236            None
9237        };
9238        let dims = GraphDims {
9239            hidden: self.hidden_size,
9240            eps: self.rms_eps as f32,
9241            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9242        };
9243        // The block input `eh_proj · [enorm(e); hnorm(h)]` rides in the
9244        // graph (one submit a step); the host per-op matvec if it cannot.
9245        let hs = self.hidden_size;
9246        let mut x = vec![0f32; hs];
9247        let mut graph = TokenGraph::new(&model, dims, &x)?;
9248        let mut folded = false;
9249        if let Some(eh) = m.eh_proj.q1_parts() {
9250            let e = self.embed_single(next_token);
9251            let mut cat = vec![0.0f32; 2 * hs];
9252            let (cat_e, cat_h) = cat.split_at_mut(hs);
9253            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9254            inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9255            folded = graph.encode_input_proj(eh, &cat);
9256        }
9257        if !folded {
9258            x = self.mtp_block_input(m, hidden, next_token);
9259            graph = TokenGraph::new(&model, dims, &x)?;
9260        }
9261        let l = AttnGpuLayer {
9262            attn_norm: &m.layer.input_norm,
9263            post_norm: &m.layer.post_norm,
9264            wq: pq,
9265            wk: pk,
9266            wv: pv,
9267            wo: po,
9268            ffn: MetalFfn::Dense {
9269                gate: g,
9270                up: u,
9271                down: dn,
9272            },
9273        };
9274        let (nh, nkv, hd, rd) = (
9275            self.num_heads,
9276            self.num_kv_heads,
9277            self.head_dim,
9278            self.rotary_dim,
9279        );
9280        let inv_freq = self.inv_freq.clone();
9281        {
9282            let cache = &m.kv;
9283            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9284            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9285            let cpu_stored = cpu_k[0].len() / hd;
9286            let p = AttnDeviceParams {
9287                kv_id: self.mtp_kv_id(),
9288                layer: Self::MTP_LAYER_BASE,
9289                nh,
9290                nkv,
9291                hd,
9292                rd,
9293                position,
9294                scale: self.attn_scale,
9295                eps: self.rms_eps as f32,
9296                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9297                late_qk_norm: self.qk_norm_after_rope,
9298                output_gate: *output_gate,
9299                q_norm: q_norm.as_deref(),
9300                k_norm: k_norm.as_deref(),
9301                inv_freq: &inv_freq,
9302                cpu_k,
9303                cpu_v,
9304                cpu_stored,
9305                o1: None,
9306            };
9307            if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
9308                return None;
9309            }
9310        }
9311        // The draft's head over a vocabulary SHORTLIST (the first
9312        // CMF_DRAFT_VOCAB rows — BPE ids run roughly by merge rank, so the
9313        // low ids carry the mass): the verify keeps the full head, so a true
9314        // token past the cut is only a rejected draft, never a wrong token.
9315        // 662 MB a step on Qwen3.8 becomes 170 MB at 65536.
9316        let draft_rows = if let Some(lm) = lm {
9317            Self::draft_vocab_rows(lm.1)
9318        } else {
9319            0
9320        };
9321        if let Some(lm) = lm {
9322            if !graph.lm_head_ok(lm) {
9323                return None;
9324            }
9325            if draft_rows < lm.1 {
9326                if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
9327                    return None;
9328                }
9329            } else {
9330                graph.encode_lm_head(&m.final_norm, lm);
9331            }
9332        }
9333        if graph.sync_checked().is_err() {
9334            return None;
9335        }
9336        let mut logits = Vec::new();
9337        if let Some(lm) = lm {
9338            let n_read = draft_rows.min(lm.1).min(self.vocab_size);
9339            logits = attention::take_buf(n_read);
9340            graph.read_logits(&mut logits);
9341            // ids past the shortlist: never drafted (−∞ in every chain)
9342            logits.resize(self.vocab_size, f32::NEG_INFINITY);
9343        }
9344        graph.finish(&mut x);
9345        let mut krow = attention::take_buf(nkv * hd);
9346        let mut vrow = attention::take_buf(nkv * hd);
9347        if crate::gpu_metal::kv_mirror_read_last(
9348            self.mtp_kv_id(),
9349            Self::MTP_LAYER_BASE,
9350            nkv,
9351            hd,
9352            &mut krow,
9353            &mut vrow,
9354        ) {
9355            m.kv.append(&krow, &vrow, &[]);
9356        }
9357        attention::recycle_buf(&mut krow);
9358        attention::recycle_buf(&mut vrow);
9359        Some((logits, x))
9360    }
9361
9362    fn try_batch_graph_wgpu(
9363        &self,
9364        hiddens: &mut [f32],
9365        positions: &[usize],
9366        k: usize,
9367        spec: Option<crate::gpu::SpecTail<'_>>,
9368    ) -> crate::gpu::BatchGraphOutcome {
9369        let _tb = std::time::Instant::now();
9370        let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
9371        if self.attn_softcap > 0.0 {
9372            return crate::gpu::BatchGraphOutcome::Declined; // capped scores: no graph kernel — CPU path
9373        }
9374        let nh = self.num_heads;
9375        let (nkv, hd, rd) = self.layer_geom(0);
9376        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9377        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9378            if let Some((m, i, kind, rs)) = t
9379                .graph_weight()
9380                .or_else(|| t.graph_weight_descriptor())
9381            {
9382                let name = &m.tensors[i].name;
9383                let prism = if crate::prism::is_inverse_embedding(m, name) {
9384                    crate::gpu::GraphPrismOp::InverseEmbedding
9385                } else if crate::prism::is_forward_weight(m, name) {
9386                    crate::gpu::GraphPrismOp::Forward
9387                } else {
9388                    crate::gpu::GraphPrismOp::None
9389                };
9390                return Some(crate::gpu::GraphW {
9391                    idx: i,
9392                    kind,
9393                    row_scale: rs,
9394                    data: &[],
9395                    prism,
9396                    affine: crate::prism::is_affine_target(m, name),
9397                });
9398            }
9399            if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
9400                eprintln!(
9401                    "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
9402                    t.rows(),
9403                    t.cols()
9404                );
9405            }
9406            t.as_f32().map(|d| crate::gpu::GraphW {
9407                idx: 0,
9408                kind: 4,
9409                row_scale: &[],
9410                data: d,
9411                prism: crate::gpu::GraphPrismOp::None,
9412                affine: false,
9413            })
9414        }
9415        let built: Option<(
9416            Vec<crate::gpu::GraphLayer<'_>>,
9417            std::sync::Arc<cortiq_core::CmfModel>,
9418        )> = (|| {
9419            let mut layers = Vec::with_capacity(self.num_layers);
9420            let mut model = None;
9421            for li in 0..self.num_layers {
9422                let lw = &self.weights.layers[self.phys_layer(li)];
9423                // MoE routes per token, so its experts are encoded token by
9424                // token inside the batched submit while attention and the
9425                // projections stay GEMMs. Refusing MoE here is what left
9426                // prefill running one position at a time: 33 tok/s against
9427                // 54 on decode, i.e. reading the prompt was slower than
9428                // writing the answer.
9429                let gffn = match &lw.ffn {
9430                    FfnKind::Dense(d) if !d.segs.is_empty() => {
9431                        if batch_debug {
9432                            eprintln!("batch graph: dense segmented FFN at layer {li}");
9433                        }
9434                        return None;
9435                    }
9436                    FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
9437                        gate: gw(&d.gate_proj)?,
9438                        up: gw(&d.up_proj)?,
9439                        down: gw(&d.down_proj)?,
9440                    },
9441                    FfnKind::Moe(m) => {
9442                        // Adaptive τ and expert masks stay on the CPU path.
9443                        // Sigmoid scores, the selection bias, a routed scale
9444                        // ≠ 1 and an ungated shared expert (hy_v3) ride the
9445                        // same flags word as the token graph — before, this
9446                        // refusal sent every Hy-MT2-30B prompt to the chunked
9447                        // fallback (8 tok/s of ingest against 53 of decode).
9448                        if m.route_tau.is_some() || m.mask.is_some() {
9449                            return None;
9450                        }
9451                        // The batch MoE kernels need the shared slot (k+1
9452                        // rows); gated or not is a flag on the select kernel.
9453                        let (se, sg) = m.shared.as_ref()?;
9454                        let shared_gated = sg.is_some();
9455                        let sgate = match sg {
9456                            Some(sg) => gw(sg)?,
9457                            // Ungated: the router plane stands in so the
9458                            // plumbing stays total; the kernel pins weight 1.
9459                            None => gw(&m.router)?,
9460                        };
9461                        let router = gw(&m.router)?;
9462                        // The batch MoE kernels still consume raw per-token
9463                        // rows and do not carry the descriptor-aware Prism
9464                        // transform/affine bit for router or shared-gate
9465                        // planes.  Refuse rather than route an untransformed
9466                        // source activation.
9467                        if router.prism != crate::gpu::GraphPrismOp::None
9468                            || router.affine
9469                            || sgate.prism != crate::gpu::GraphPrismOp::None
9470                            || sgate.affine
9471                        {
9472                            return None;
9473                        }
9474                        let inter = m.experts.first()?.gate_proj.rows();
9475                        let mut experts = Vec::with_capacity(m.experts.len() + 1);
9476                        let mut q4tp: Option<bool> = None;
9477                        let mut gu_q2: Option<bool> = None;
9478                        for e in m.experts.iter().chain(std::iter::once(se)) {
9479                            if !matches!(e.act, Act::Silu)
9480                                || e.gate_proj.rows() != inter
9481                                || e.up_proj.rows() != inter
9482                            {
9483                                return None;
9484                            }
9485                            // Same ladder as the token graph: q4t → q2tp
9486                            // (mixed profile: 2-bit gate/up over a q4tp
9487                            // down) → q4tp. Uniform across the layer.
9488                            let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
9489                                Some((mm, gi)) => (
9490                                    mm,
9491                                    gi,
9492                                    e.up_proj.mapped_q4t()?.1,
9493                                    e.down_proj.mapped_q4t()?.1,
9494                                    false,
9495                                    false,
9496                                ),
9497                                None => match e.gate_proj.mapped_q2tp() {
9498                                    Some((mm, gi)) => (
9499                                        mm,
9500                                        gi,
9501                                        e.up_proj.mapped_q2tp()?.1,
9502                                        e.down_proj.mapped_q4tp()?.1,
9503                                        true,
9504                                        true,
9505                                    ),
9506                                    None => {
9507                                        let (mm, gi) = e.gate_proj.mapped_q4tp()?;
9508                                        (
9509                                            mm,
9510                                            gi,
9511                                            e.up_proj.mapped_q4tp()?.1,
9512                                            e.down_proj.mapped_q4tp()?.1,
9513                                            true,
9514                                            false,
9515                                        )
9516                                    }
9517                                },
9518                            };
9519                            if *q4tp.get_or_insert(is_p) != is_p
9520                                || *gu_q2.get_or_insert(is_q2) != is_q2
9521                            {
9522                                return None;
9523                            }
9524                            if [gi, ui, di].into_iter().any(|idx| {
9525                                mm.tensors
9526                                    .get(idx)
9527                                    .is_some_and(|t| {
9528                                        crate::prism::is_forward_weight(mm, &t.name)
9529                                            || crate::prism::is_affine_target(mm, &t.name)
9530                                    })
9531                            }) {
9532                                return None;
9533                            }
9534                            model.get_or_insert_with(|| mm.clone());
9535                            experts.push((gi, ui, di));
9536                        }
9537                        crate::gpu::GraphFfn::Moe {
9538                            router,
9539                            shared_gate: sgate,
9540                            experts,
9541                            n_exp: m.experts.len(),
9542                            top_k: m.top_k,
9543                            inter,
9544                            norm_topk: m.norm_topk_prob,
9545                            q4tp: q4tp?,
9546                            gu_q2: gu_q2.unwrap_or(false),
9547                            sigmoid: m.router_sigmoid,
9548                            bias: m.expert_bias.as_deref(),
9549                            has_shared: true,
9550                            shared_gated,
9551                            route_scale: m.routed_scaling,
9552                        }
9553                    }
9554                    _ => return None,
9555                };
9556                let attn = match &lw.attn {
9557                    AttnKind::Full {
9558                        wq,
9559                        wk,
9560                        wv,
9561                        wo,
9562                        q_norm,
9563                        k_norm,
9564                        output_gate,
9565                        softplus_gate,
9566                        bias,
9567                    } => {
9568                        if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
9569                            if batch_debug {
9570                                eprintln!(
9571                                    "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
9572                                    softplus_gate.is_some(),
9573                                    self.attention_heads_per_layer.is_some()
9574                                );
9575                            }
9576                            return None;
9577                        }
9578                        let (m, _, _, _) = wq
9579                            .graph_weight()
9580                            .or_else(|| wq.graph_weight_descriptor())?;
9581                        model = Some(m.clone());
9582                        crate::gpu::GraphAttn::Full {
9583                            wq: gw(wq)?,
9584                            wk: gw(wk)?,
9585                            wv: gw(wv)?,
9586                            wo: gw(wo)?,
9587                            q_norm: q_norm.as_deref(),
9588                            k_norm: k_norm.as_deref(),
9589                            late_qk_norm: self.qk_norm_after_rope,
9590                            bias: bias
9591                                .as_ref()
9592                                .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9593                            output_gate: *output_gate,
9594                            cpu_k: self.kv_cache.layers[li].k_heads(),
9595                            cpu_v: self.kv_cache.layers[li].v_heads(),
9596                        }
9597                    }
9598                    AttnKind::LinearGdn(w) => {
9599                        let Some(cfg) = self.gdn_cfg else {
9600                            if batch_debug {
9601                                eprintln!("batch graph: no GDN config at layer {li}");
9602                            }
9603                            return None;
9604                        };
9605                        let (m, _, _, _) = w
9606                            .in_proj_qkv
9607                            .graph_weight()
9608                            .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
9609                        model = Some(m.clone());
9610                        crate::gpu::GraphAttn::Gdn {
9611                            qkv: gw(&w.in_proj_qkv)?,
9612                            z: gw(&w.in_proj_z)?,
9613                            a: gw(&w.in_proj_a)?,
9614                            b: gw(&w.in_proj_b)?,
9615                            out: gw(&w.out_proj)?,
9616                            conv1d: &w.conv1d,
9617                            a_log: &w.a_log,
9618                            dt_bias: &w.dt_bias,
9619                            norm: &w.norm,
9620                            nv: cfg.num_v_heads,
9621                            nk: cfg.num_k_heads,
9622                            dk: cfg.key_head_dim,
9623                            dv: cfg.value_head_dim,
9624                            kk: cfg.conv_kernel,
9625                            cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
9626                        }
9627                    }
9628                    _ => return None,
9629                };
9630                layers.push(crate::gpu::GraphLayer {
9631                    input_norm: &lw.input_norm,
9632                    attn,
9633                    post_norm: &lw.post_norm,
9634                    ffn: gffn,
9635                });
9636            }
9637            Some((layers, model?))
9638        })();
9639        let Some((layers, model)) = built else {
9640            {
9641                use std::sync::atomic::{AtomicBool, Ordering};
9642                static SAID: AtomicBool = AtomicBool::new(false);
9643                if !SAID.swap(true, Ordering::Relaxed) {
9644                    tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
9645                }
9646            }
9647            return crate::gpu::BatchGraphOutcome::Declined;
9648        };
9649        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
9650            eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
9651        }
9652        crate::gpu::forward_batch_graph(
9653            &model,
9654            self.graph_kv_id,
9655            &layers,
9656            &self.inv_freq,
9657            hiddens,
9658            nh,
9659            nkv,
9660            hd,
9661            rd,
9662            self.hidden_size,
9663            self.intermediate_size,
9664            positions,
9665            self.kv_cache.max_seq_len,
9666            gemma,
9667            self.rms_eps as f32,
9668            self.attn_scale,
9669            k,
9670            &(0..self.num_layers)
9671                .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
9672                .collect::<Vec<_>>(),
9673            self.o1_epoch,
9674            spec,
9675        )
9676    }
9677
9678    /// Same, stopping after layer `upto` inclusive (routing probe φ).
9679    /// `CMF_DSV4_DRAFT_PROBE=1` — grade the draft against what the trunk goes on
9680    /// to produce. Off by default; it runs a whole draft per decoded token.
9681    fn draft_probe() -> bool {
9682        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9683        *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
9684    }
9685
9686    /// `CMF_DSV4_DRAFT_PROBE=1`: measure how much of the draft the trunk
9687    /// would have agreed with, WITHOUT verifying or rolling anything back.
9688    ///
9689    /// The number this produces decides the whole speculation design — at
9690    /// acceptance a, a block of B positions yields 1 + a + a² + ... tokens
9691    /// per trunk pass — so it is worth measuring before any of the machinery
9692    /// that would exploit it exists. Each draft is parked with the position
9693    /// it was made at, and graded as the real tokens arrive.
9694    /// `CMF_DSV4_SPEC=1` — the DeepSeek-V4 speculative decode: draft five
9695    /// on the card, verify them in one batched trunk pass, commit the
9696    /// accepted prefix, roll the rest back.
9697    #[cfg(feature = "gpu")]
9698    fn dsv4_spec_on() -> bool {
9699        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9700        *ON.get_or_init(|| {
9701            // Test-only runtime gate: model loading still performs the same
9702            // reservation and trunk packing, which gives rollback parity a
9703            // topology-identical non-speculative control arm.
9704            if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
9705                return v != "0";
9706            }
9707            // An explicit value is a diagnostic force/escape hatch.  With no
9708            // knob, speculation is eligible only when model loading reserved
9709            // its bounded pack.  On small q4tp cards the geometric reserve
9710            // gate deliberately leaves this at zero: trying to build DSpark
9711            // after the exact trunk filled VRAM is both slower and a device
9712            // OOM (measured on A40).
9713            std::env::var("CMF_DSV4_SPEC")
9714                .map(|v| v != "0")
9715                .unwrap_or_else(|_| {
9716                    crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
9717                })
9718        })
9719    }
9720
9721    /// One speculative round at the decode tip. `t_next` is the token the
9722    /// sampler just committed for `next_pos`. Returns the EXTRA accepted
9723    /// tokens (possibly none) and the new position, with `graph_logits`
9724    /// left holding the last accepted position's logits — exactly what the
9725    /// loop top expects. `None` means "speculate not this round": nothing
9726    /// was committed, the caller forwards normally.
9727    #[cfg(feature = "gpu")]
9728    fn dsv4_spec_step(
9729        &mut self,
9730        tip_token: u32,
9731        t_next: u32,
9732        next_pos: usize,
9733        max_extra: usize,
9734        drafted: &mut usize,
9735        accepted_ctr: &mut usize,
9736    ) -> Option<(Vec<u32>, usize)> {
9737        let t_all = std::time::Instant::now();
9738        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9739            thread_local! {
9740                static LAST: std::cell::Cell<Option<std::time::Instant>> =
9741                    const { std::cell::Cell::new(None) };
9742            }
9743            LAST.with(|l| {
9744                if let Some(prev) = l.get() {
9745                    eprintln!(
9746                        "между раундами {:.1} мс",
9747                        prev.elapsed().as_secs_f64() * 1e3
9748                    );
9749                }
9750                l.set(Some(std::time::Instant::now()));
9751            });
9752        }
9753        if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9754            eprintln!("spec_step: вход pos={next_pos}");
9755        }
9756        let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
9757        let cfg = self.dsv4.as_ref().map(|b| b.2)?;
9758        // The draft state and its capture, armed exactly as the probe does.
9759        if self.dspark.is_none() {
9760            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9761            if t.is_empty() {
9762                return None;
9763            }
9764            crate::dsv4::dspark_arm(&t, cfg.dim);
9765            self.dspark = Some(crate::dsv4::DsparkState::new(
9766                self.dsv4_mtp.len(),
9767                &cfg,
9768                t.len(),
9769            ));
9770        }
9771        let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9772        let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
9773        if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9774            eprintln!("spec_step: пак не построился (targets {targets:?})");
9775        }
9776        let pack = pack?;
9777        let block = crate::dsv4::dspark_block();
9778        let b_box = self.dsv4.as_mut()?;
9779        let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
9780        let ds = self.dspark.as_mut()?;
9781        // The tip's captures: either this token ran on a normal path that
9782        // filled the thread-local, or the previous spec round left them.
9783        let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
9784        if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
9785            if dbg {
9786                eprintln!("spec_step: нет захвата");
9787            }
9788            return None;
9789        }
9790        ds.have_hidden = true;
9791        let tip_pos = next_pos.checked_sub(1)?;
9792        let draft_started = std::time::Instant::now();
9793        let mut conf = Vec::new();
9794        let props = crate::dsv4::dspark_draft_gpu(
9795            g,
9796            &self.dsv4_mtp,
9797            &cfg,
9798            ds,
9799            pack,
9800            st.kv_id,
9801            tip_token,
9802            tip_pos,
9803            self.pool.as_deref(),
9804            &mut conf,
9805        );
9806        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9807        *drafted += block;
9808        if props.is_empty() || props[0] != t_next {
9809            if dbg {
9810                eprintln!(
9811                    "spec_step: черновик {} (props0={:?} t_next={t_next})",
9812                    if props.is_empty() {
9813                        "пуст"
9814                    } else {
9815                        "мимо"
9816                    },
9817                    props.first()
9818                );
9819            }
9820            return None;
9821        }
9822        // `fed[0]` is `t_next`, which the outer loop has already committed;
9823        // only `fed[1..]` become additional output tokens. Cap the verify
9824        // transaction itself to the caller's remaining output budget instead
9825        // of merely truncating the returned vector: otherwise the KV/state
9826        // would advance past `max_tokens` and a 64-token request could return
9827        // 66 tokens (and poison a reused session with two invisible steps).
9828        let mut k_verify = crate::dsv4::dspark_verify_k()
9829            .min(props.len())
9830            .min(max_extra.saturating_add(1));
9831        // Adaptive depth: positions the draft itself doubts are paid for on
9832        // every verify and delivered almost never (natural-text survival
9833        // [.67 .50 .29 .08 .04]). `CMF_DSPARK_CONF_MIN=p` trims the fed
9834        // prefix at the first proposal whose confidence drops below p; on
9835        // predictable text the confidences stay high and nothing changes.
9836        let conf_min = {
9837            static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
9838            *M.get_or_init(|| {
9839                std::env::var("CMF_DSPARK_CONF_MIN")
9840                    .ok()
9841                    .and_then(|v| v.parse().ok())
9842                    .unwrap_or(0.0)
9843            })
9844        };
9845        if conf_min > 0.0 && conf.len() >= props.len() {
9846            let mut keep = 1usize;
9847            while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
9848                keep += 1;
9849            }
9850            k_verify = k_verify.min(keep.max(2));
9851        }
9852        if k_verify < 2 {
9853            return None;
9854        }
9855        let mut fed = Vec::with_capacity(k_verify);
9856        fed.push(t_next);
9857        fed.extend_from_slice(&props[1..k_verify]);
9858        let mut argmax = Vec::new();
9859        let mut logits_all = Vec::new();
9860        let mut walked = Vec::new();
9861        let txn = crate::dsv4::dsv4_verify_chunk(
9862            g,
9863            layers,
9864            &cfg,
9865            st,
9866            &fed,
9867            next_pos,
9868            &self.inv_freq,
9869            self.pool.as_deref(),
9870            &targets,
9871            &mut argmax,
9872            &mut logits_all,
9873            &mut walked,
9874        );
9875        if txn.is_none() && dbg {
9876            eprintln!("spec_step: verify отказал");
9877        }
9878        let txn = txn?;
9879        let spec_gpu_end = txn.gpu_end;
9880        let b = fed.len();
9881        let mut accepted = 1usize;
9882        while accepted < b && fed[accepted] == argmax[accepted - 1] {
9883            accepted += 1;
9884        }
9885        // `CMF_DSV4_SPEC_FORCE_REJECT=1` — accept nothing beyond the known
9886        // token, every round: the pure rollback exerciser. The output must
9887        // stay byte-identical to the plain walk; anything else is a
9888        // transaction bug, isolated from the acceptance logic.
9889        if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
9890            accepted = 1;
9891        }
9892        if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
9893            eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
9894        }
9895        let t_fin = std::time::Instant::now();
9896        if !crate::dsv4::dsv4_spec_finish(
9897            g,
9898            layers,
9899            &cfg,
9900            st,
9901            txn,
9902            accepted,
9903            &fed,
9904            &self.inv_freq,
9905            self.pool.as_deref(),
9906        ) {
9907            tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
9908            return None;
9909        }
9910        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9911            eprintln!(
9912                "finish(k={accepted}): {:.1} мс",
9913                t_fin.elapsed().as_secs_f64() * 1e3
9914            );
9915        }
9916        *accepted_ctr += accepted - 1;
9917        // Captures per accepted token: device targets photographed by the
9918        // batch, host targets from the verify's own walk. The last one
9919        // becomes the new tip's draft input; every one owes the ring an
9920        // entry for its position.
9921        let (hc, dim) = (cfg.hc_mult, cfg.dim);
9922        // Complete-chain layers are photographed by the fused submission;
9923        // partial device layers overwrite that slot after exact host cold-
9924        // expert correction.  Thus every target in the contiguous device
9925        // prefix has a valid per-token capture.
9926        let dev_caps: Vec<usize> = targets
9927            .iter()
9928            .copied()
9929            .filter(|&t| t < spec_gpu_end)
9930            .collect();
9931        let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
9932        if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
9933            return None;
9934        }
9935        for t in 0..accepted {
9936            let tip = t + 1 == accepted;
9937            for (slot, &tl) in targets.iter().enumerate() {
9938                if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
9939                    let lo = (di * b + t) * hc * dim;
9940                    crate::dsv4::dspark_capture(
9941                        &caps_all[lo..lo + hc * dim],
9942                        &cfg,
9943                        slot,
9944                        &mut ds.main_hidden,
9945                    );
9946                } else if tip
9947                    && crate::dsv4::dspark_peek_slot(slot, dim, {
9948                        let lo = slot * dim;
9949                        &mut ds.main_hidden[lo..lo + dim]
9950                    })
9951                {
9952                    // The tip's host-layer captures are the walk's own
9953                    // per-layer notes — exact. (The walk that ran last ended
9954                    // on exactly this token, on both the accept-all and the
9955                    // rollback path.)
9956                } else {
9957                    // Intermediate tokens: the post-tail state stands in for
9958                    // the per-layer capture on host targets below the last
9959                    // layer. Ring-entry quality only; the tip is exact.
9960                    crate::dsv4::dspark_capture(
9961                        &walked[t * hc * dim..(t + 1) * hc * dim],
9962                        &cfg,
9963                        slot,
9964                        &mut ds.main_hidden,
9965                    );
9966                }
9967            }
9968            crate::dsv4::dspark_ring_append(
9969                g,
9970                &self.dsv4_mtp,
9971                &cfg,
9972                ds,
9973                next_pos + t,
9974                self.pool.as_deref(),
9975            );
9976        }
9977        let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
9978        self.graph_logits = Some(row);
9979        // The speculative loop never runs the probe, so the trunk tally has
9980        // no other place to cycle. Armed only when someone asked for the
9981        // dump; the host tail is the only tallying path here, which is
9982        // precisely the population a partial pack would serve.
9983        if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
9984            crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
9985            crate::dsv4::pick_tally_arm();
9986        }
9987        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9988            eprintln!(
9989                "spec_step total {:.1} мс (k={accepted})",
9990                t_all.elapsed().as_secs_f64() * 1e3
9991            );
9992        }
9993        Some((fed[1..accepted].to_vec(), next_pos + accepted))
9994    }
9995
9996    fn dspark_probe(&mut self, position: usize, token_id: u32) {
9997        if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
9998            return;
9999        }
10000        // What the trunk just routed to, for this token.
10001        let trunk_now = crate::dsv4::pick_tally_take();
10002        crate::dsv4::trunk_freq_note(&trunk_now);
10003        if !trunk_now.is_empty() {
10004            self.dspark_trunk_picks.push(trunk_now);
10005            let keep = crate::dsv4::dspark_block();
10006            if self.dspark_trunk_picks.len() > keep {
10007                self.dspark_trunk_picks.remove(0);
10008            }
10009        }
10010        // Grade whatever is waiting: the token just decoded sits at
10011        // `position`, so it answers the draft made at `position - 1 - i`.
10012        for p in std::mem::take(&mut self.dspark_pending) {
10013            let Some(i) = position.checked_sub(p.0 + 1) else {
10014                continue;
10015            };
10016            let mut p = p;
10017            if i < p.1.len() {
10018                if p.2 && p.1[i] == token_id {
10019                    p.3 = i + 1;
10020                } else {
10021                    p.2 = false;
10022                }
10023                if i + 1 < p.1.len() {
10024                    self.dspark_pending.push(p);
10025                    continue;
10026                }
10027            }
10028            self.dspark_hist.push(p.3);
10029            self.dspark_real.push(token_id);
10030        }
10031        let Some(b) = &mut self.dsv4 else { return };
10032        let (g, layers, cfg) = (&b.0, &b.1, b.2);
10033        let n_layers = layers.len();
10034        if self.dspark.is_none() {
10035            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
10036            if t.is_empty() {
10037                return;
10038            }
10039            eprintln!(
10040                "DSpark: захват со слоёв {t:?}, блок {}",
10041                crate::dsv4::dspark_block()
10042            );
10043            crate::dsv4::dspark_arm(&t, cfg.dim);
10044            self.dspark = Some(crate::dsv4::DsparkState::new(
10045                self.dsv4_mtp.len(),
10046                &cfg,
10047                t.len(),
10048            ));
10049        }
10050        let ds = self.dspark.as_mut().unwrap();
10051        if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
10052            return; // this token ran on a path that captures nothing
10053        }
10054        let mut conf = Vec::new();
10055        crate::dsv4::pick_tally_arm();
10056        // The trunk has already consumed the adaptive VRAM budget. Until the
10057        // draft owns an explicit bounded device pack, its tensors are an
10058        // out-of-core CPU/disk tier by contract: never let per-op probes try
10059        // to squeeze another multi-gigabyte MTP expert cache onto the card.
10060        let draft_started = std::time::Instant::now();
10061        #[cfg(feature = "gpu")]
10062        let gpu_draft = crate::dsv4::dspark_gpu_on();
10063        #[cfg(not(feature = "gpu"))]
10064        let gpu_draft = false;
10065        let props = if gpu_draft {
10066            #[cfg(feature = "gpu")]
10067            {
10068                let kv_id = b.3.kv_id;
10069                match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
10070                    Some(pk) => crate::dsv4::dspark_draft_gpu(
10071                        g,
10072                        &self.dsv4_mtp,
10073                        &cfg,
10074                        ds,
10075                        pk,
10076                        kv_id,
10077                        token_id,
10078                        position,
10079                        self.pool.as_deref(),
10080                        &mut conf,
10081                    ),
10082                    None => Vec::new(),
10083                }
10084            }
10085            #[cfg(not(feature = "gpu"))]
10086            Vec::new()
10087        } else {
10088            crate::gpu::cpu_scope(|| {
10089                crate::dsv4::dspark_draft(
10090                    g,
10091                    &self.dsv4_mtp,
10092                    &cfg,
10093                    ds,
10094                    token_id,
10095                    position,
10096                    self.pool.as_deref(),
10097                    &mut conf,
10098                )
10099            })
10100        };
10101        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
10102        let draft_picks = crate::dsv4::pick_tally_take();
10103        crate::dsv4::dspark_freq_note(&draft_picks);
10104        // Re-arm for the NEXT trunk token; the probe runs after the forward,
10105        // so this is the only place that can.
10106        crate::dsv4::pick_tally_arm();
10107        if !props.is_empty() {
10108            // Two ratios, side by side: what a batched verify over the trunk
10109            // would read against what it asks for, and the same for the
10110            // draft's three stages. Near 1.0 means a batch amortises nothing.
10111            let (tu, tt) = {
10112                let flat: Vec<(usize, Vec<usize>)> = self
10113                    .dspark_trunk_picks
10114                    .iter()
10115                    .flat_map(|v| v.iter().cloned())
10116                    .collect();
10117                // Per layer, across the window of tokens.
10118                let mut per: std::collections::HashMap<usize, Vec<usize>> =
10119                    std::collections::HashMap::new();
10120                for (li, picks) in flat {
10121                    per.entry(li).or_default().extend(picks);
10122                }
10123                let n = per.len().max(1);
10124                let mut u = 0usize;
10125                let mut t = 0usize;
10126                for (_, v) in per {
10127                    t += v.len();
10128                    u += v.iter().collect::<std::collections::HashSet<_>>().len();
10129                }
10130                (u / n, t / n)
10131            };
10132            let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
10133            self.dspark_exp.push((tu, tt, du, dt));
10134            self.dspark_pending.push((position, props, true, 0));
10135        }
10136        if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
10137            let n = self.dspark_hist.len() as f32;
10138            let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
10139            let block = crate::dsv4::dspark_block();
10140            let mut at = vec![0usize; block + 1];
10141            for &k in &self.dspark_hist {
10142                at[k] += 1;
10143            }
10144            // Prefix survival: S_i = P(the first i positions all held).
10145            let mut surv = Vec::with_capacity(block);
10146            for i in 1..=block {
10147                let k = at[i..].iter().sum::<usize>() as f32 / n;
10148                surv.push(format!("{k:.2}"));
10149            }
10150            let distinct = self
10151                .dspark_real
10152                .iter()
10153                .collect::<std::collections::HashSet<_>>()
10154                .len();
10155            let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
10156                (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
10157            });
10158            let m = self.dspark_exp.len().max(1);
10159            eprintln!(
10160                "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
10161                 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
10162                self.dspark_hist.len(),
10163                mean + 1.0,
10164                surv.join(" ")
10165            );
10166            eprintln!(
10167                "DSpark: разных токенов {distinct} из {} (вырожденность), \
10168                 эксперты ствол {}/{} на слой за {block} токенов, \
10169                 черновик {}/{} за блок, draft {:.2} мс/блок",
10170                self.dspark_real.len(),
10171                tu / m,
10172                tt / m,
10173                du / m,
10174                dt / m,
10175                self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
10176            );
10177        }
10178    }
10179
10180    fn forward_layers_upto(
10181        &mut self,
10182        hidden: &[f32],
10183        position: usize,
10184        task_mask: Option<&TaskMask>,
10185        upto: Option<usize>,
10186    ) -> Vec<f32> {
10187        // In-process multi-GPU: each segment runs pinned to its card,
10188        // and the only thing crossing the boundary is one hidden vector
10189        // that never leaves this address space. Same layer split the
10190        // network mode does, minus the second process, the socket, the
10191        // serialization and the dir_hash handshake.
10192        if let Some(plan) = self.gpu_plan.clone() {
10193            if upto.is_none() && plan.len() > 1 {
10194                let mut h = hidden.to_vec();
10195                for &(dev, from, upto_incl) in plan.iter() {
10196                    h = crate::gpu::with_device(dev, || {
10197                        self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
10198                    });
10199                }
10200                return h;
10201            }
10202        }
10203        self.forward_layers_span(hidden, position, task_mask, 0, upto)
10204    }
10205
10206    /// Split this pipeline's layer stack across local GPUs: segment i
10207    /// runs on `devices[i]`. Contiguous and even by layer count — the
10208    /// VRAM-weighted planner is the next step, and an uneven card pair
10209    /// is why it will be needed. `None` clears the plan.
10210    pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
10211        self.set_gpu_plan_at(devices, None)
10212    }
10213
10214    /// The same, with an explicit first boundary (`--peer-split`): card
10215    /// 0 takes layers `[0..at)`, the rest split what remains. Uneven
10216    /// cards, or an attention-heavy head, are why this knob exists.
10217    pub fn set_gpu_plan_at(
10218        &mut self,
10219        devices: Option<&[usize]>,
10220        at: Option<usize>,
10221    ) -> Result<(), String> {
10222        let Some(devs) = devices.filter(|d| d.len() > 1) else {
10223            self.gpu_plan = None;
10224            return Ok(());
10225        };
10226        self.split_supported()?;
10227        let n = self.num_layers;
10228        if devs.len() > n {
10229            return Err(format!("{} devices for {n} layers", devs.len()));
10230        }
10231        if let Some(k) = at {
10232            if k == 0 || k >= n {
10233                return Err(format!("split at {k}: the model has {n} layers"));
10234            }
10235            if devs.len() == 2 {
10236                self.gpu_plan = Some(std::sync::Arc::new(vec![
10237                    (devs[0], 0, k - 1),
10238                    (devs[1], k, n - 1),
10239                ]));
10240                return Ok(());
10241            }
10242            return Err(format!(
10243                "an explicit split point takes exactly 2 devices, got {}",
10244                devs.len()
10245            ));
10246        }
10247        let per = n.div_ceil(devs.len());
10248        let mut plan = Vec::with_capacity(devs.len());
10249        let mut from = 0usize;
10250        for &d in devs {
10251            if from >= n {
10252                break;
10253            }
10254            let upto = (from + per - 1).min(n - 1);
10255            plan.push((d, from, upto));
10256            from = upto + 1;
10257        }
10258        self.gpu_plan = Some(std::sync::Arc::new(plan));
10259        Ok(())
10260    }
10261
10262    /// The active in-process split, if any: (device, first layer, last).
10263    pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
10264        self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
10265    }
10266
10267    /// Layer span [from ..= upto] (upto None = last layer): the building
10268    /// block the network pipeline-split rides on. `from > 0` skips the
10269    /// arch escape hatches (the pub `forward_span` refuses those archs
10270    /// first) and the whole-token graph — the plain per-layer loop is
10271    /// the canonical executor for a partial stack.
10272    fn forward_layers_span(
10273        &mut self,
10274        hidden: &[f32],
10275        position: usize,
10276        task_mask: Option<&TaskMask>,
10277        from: usize,
10278        upto: Option<usize>,
10279    ) -> Vec<f32> {
10280        debug_assert!(
10281            from == 0
10282                || (self.dsv4.is_none()
10283                    && self.dsv41.is_none()
10284                    && self.qwen4_exp.is_none()
10285                    && self.g3n.is_none())
10286        );
10287        if let Some(b) = &mut self.qwen4_exp {
10288            let _ = (task_mask, upto);
10289            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10290            let mut logits = Vec::new();
10291            crate::qwen4_exp::forward_token(
10292                &b.0,
10293                &b.1,
10294                &b.2,
10295                &mut b.3,
10296                token_id,
10297                position,
10298                &self.inv_freq,
10299                self.pool.as_deref(),
10300                &mut logits,
10301                true,
10302            );
10303            self.graph_logits = Some(logits);
10304            return vec![0.0; self.hidden_size];
10305        }
10306        // DeepSeek-V4 runs its own stack: the state is hc_mult copies, and
10307        // the forward returns LOGITS, not a hidden — the head is inside it
10308        // (the final fold sits between the last layer and the norm). The
10309        // token id rides in `hidden[0]`, written by embed_single, because
10310        // the hash layers route by id rather than by content.
10311        if let Some(b) = &mut self.dsv4 {
10312            let _ = (task_mask, upto);
10313            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10314            let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
10315            st.pos = position;
10316            let mut logits = Vec::new();
10317            crate::dsv4::forward_token(
10318                g,
10319                layers,
10320                &cfg,
10321                st,
10322                token_id,
10323                &self.inv_freq,
10324                self.pool.as_deref(),
10325                &mut logits,
10326            );
10327            self.graph_logits = Some(logits);
10328            self.dspark_probe(position, token_id);
10329            // The caller expects a hidden; the logits went out of band, as
10330            // with the fused lm_head path.
10331            return vec![0.0; self.hidden_size];
10332        }
10333        // DeepSeek-V4.1 owns its complete stack and emits logits out of band.
10334        if let Some(b) = &mut self.dsv41 {
10335            let _ = (task_mask, upto);
10336            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10337            let mut logits = Vec::new();
10338            crate::dsv41::forward_token(
10339                &b.0,
10340                &b.1,
10341                &b.2,
10342                &mut b.3,
10343                token_id,
10344                position,
10345                self.pool.as_deref(),
10346                &mut logits,
10347            );
10348            self.graph_logits = Some(logits);
10349            return vec![0.0; self.hidden_size];
10350        }
10351        // Gemma-3n runs its own stack (4 AltUp replicas don't fit this
10352        // loop); `hidden` is the extended embedding from embed_single.
10353        if let Some(b) = &self.g3n {
10354            let _ = (task_mask, upto);
10355            return crate::g3n::g3n_forward(
10356                &b.0,
10357                &b.1,
10358                hidden,
10359                position,
10360                &mut self.kv_cache.layers,
10361                self.num_heads,
10362                self.num_kv_heads,
10363                self.head_dim,
10364                self.pool.as_deref(),
10365            );
10366        }
10367        let mut h = hidden.to_vec();
10368        // Split borrows: copy scalars / clone handles so the per-layer
10369        // cfg does not hold `&self` while the KV cache is `&mut`.
10370        let (nh, _nkv, _hd, hs, _rd, eps) = (
10371            self.num_heads,
10372            self.num_kv_heads,
10373            self.head_dim,
10374            self.hidden_size,
10375            self.rotary_dim,
10376            self.rms_eps,
10377        );
10378        let pool = self.pool.clone();
10379        // Opt-in wgpu token-graph attention (discrete Vulkan/DX12): the whole
10380        // attention sub-block runs resident in one submit. Off by default.
10381        // Whole-token wgpu graph: eligibility + arbitration.
10382        //  - explicit CMF_GPU_WGPU_GRAPH forces it on/off;
10383        //  - discrete adapters (4090: decode 76 -> 137 tok/s) and GDN
10384        //    hybrids (recurrent state device-resident, no CPU twin to
10385        //    race) TRUST it;
10386        //  - integrated/mobile adapters RACE it against the normal path
10387        //    at generation granularity (gpu::graph_race_*) — tiled
10388        //    mobile GPUs can turn the ~300-dispatch graph into seconds
10389        //    per token, while a fast phone GPU keeps its win.
10390        let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
10391        let graph_on = match graph_env.as_deref() {
10392            Some("0") => false,
10393            Some("prefill") => false, // decode keeps the per-op path
10394            Some(_) => true,
10395            // Unset: same discrete-only default as every other graph
10396            // site. "Is the GPU on" used to stand in here — which made
10397            // the 0.2 tok/s whole-token graph race-eligible on mobile
10398            // adapters and cost 12-14× on first tokens (cmfmobile
10399            // TUNING.md); integrated GPUs keep the per-op probe path.
10400            None => crate::gpu::wgpu_graph_default(),
10401        };
10402        let graph_trusted =
10403            graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
10404        let race_eligible = graph_on
10405            && upto.is_none()
10406            && task_mask.is_none()
10407            && from == 0
10408            && !crate::gpu::graph_unsupported();
10409        let mut tail_start = 0usize;
10410        if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
10411            let t_graph = std::time::Instant::now();
10412            let mut lg = Vec::new();
10413            let mut gl = 0usize;
10414            let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
10415            let declined = built.is_none();
10416            let built = match built {
10417                Some(Ok(hh)) => Some(hh),
10418                Some(Err(())) => {
10419                    // O(1) state was admitted before the device failure; the
10420                    // CPU mirrors are stale by construction.  Clear the whole
10421                    // sequence and stop rather than walking that stale state.
10422                    self.clear_sequence_state();
10423                    self.graph_failed
10424                        .store(true, std::sync::atomic::Ordering::Relaxed);
10425                    self.cancel
10426                        .store(true, std::sync::atomic::Ordering::Relaxed);
10427                    tracing::error!("token graph failed after admission; sequence state cleared");
10428                    return vec![0.0; self.hidden_size];
10429                }
10430                None => None,
10431            };
10432            // Past the transient guards (o1 still collecting, a softcap)
10433            // a refusal is about the weights and will never change —
10434            // remember it instead of walking every layer again next
10435            // token.
10436            if declined && !self.o1_active() && self.attn_softcap == 0.0 {
10437                crate::gpu::graph_mark_unsupported();
10438            }
10439            graph_note(built.is_some(), gl, self.num_layers);
10440            if let Some(hh) = built {
10441                let dur = t_graph.elapsed();
10442                if std::env::var("CMF_GRAPH_PROF").is_ok() {
10443                    eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
10444                }
10445                if gl > 0 && gl < self.num_layers {
10446                    // Device prefix: the graph ran layers 0..gl and handed
10447                    // back the boundary hidden — the loop below owns the
10448                    // tail. The prefix layers' KV/state advanced on the
10449                    // device; the tail's advances on the host below. One
10450                    // boundary crossing per token.
10451                    h = hh;
10452                    tail_start = gl;
10453                } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
10454                    if !graph_trusted {
10455                        crate::gpu::graph_race_record(true, dur);
10456                    }
10457                    if !lg.is_empty() {
10458                        // Graph produced logits (final-norm + lm_head folded in) —
10459                        // pad/cap to vocab and hand them to the sampler directly.
10460                        lg.resize(self.vocab_size, 0.0);
10461                        if let Some(c) = self.final_softcap {
10462                            for l in lg.iter_mut() {
10463                                *l = c * (*l / c).tanh();
10464                            }
10465                        }
10466                        self.graph_logits = Some(lg);
10467                    }
10468                    return hh;
10469                }
10470                // Hopeless first graph token: discard it and fall through
10471                // to the normal path. Safe exactly here — the prompt KV is
10472                // still CPU-owned (chunked prefill), so recomputing this
10473                // position is exact; the mirror's extra row is never read
10474                // (the race just settled on the normal path).
10475            }
10476        }
10477        // KIMI-LINEAR HAS NO SPLIT BUG. The 2.6× reported from the
10478        // model rotation (12.2 tok/s on one card against 4.6 on two)
10479        // was a single measurement of a model whose arm arbitration is
10480        // borderline, and it did not survive repetition. Three runs an
10481        // arm, same binary, back to back:
10482        //   probe on : 1 GPU 9.5 / 5.7 / 5.9   2 GPU 7.8 / 13.0 / 13.3
10483        //   pinned   : 1 GPU 5.6 / 5.3 / 5.2   2 GPU 3.5 / 4.2 / 3.4
10484        // With the arms pinned the split costs about 1.45×, which is
10485        // what a layer split costs. With the probe free, TWO CARDS RUN
10486        // FASTER — because for this model the CPU arm wins some op
10487        // classes and the probe finds that.
10488        //
10489        // Two things do stand, and both are measured. The token graph
10490        // builds NOTHING here (`covered 0 of 14 layers [0..14)`), so
10491        // every layer walks per-op on either arm — that is where the
10492        // headroom is, not in the split. And this model's benchmark is
10493        // unusable without `CMF_GPU_PROBE=0`: the arbitration alone
10494        // moves it by more than 2×.
10495        //
10496        // Span runs (network split): the graph covers exactly [from..=upto]
10497        // — one submit per SEGMENT per token. No race: its state is global
10498        // and calibrated on full stacks, so spans take the graph only where
10499        // it is trusted by default (discrete adapters / CMF_GPU_WGPU_GRAPH).
10500        let span = from > 0 || upto.is_some();
10501        if span && graph_on && task_mask.is_none() && graph_trusted {
10502            let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
10503            let mut lg = Vec::new();
10504            let mut gl = 0usize;
10505            let span_res =
10506                self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
10507            let span_res = match span_res {
10508                Some(Ok(hh)) => Some(hh),
10509                Some(Err(())) => {
10510                    self.clear_sequence_state();
10511                    self.graph_failed
10512                        .store(true, std::sync::atomic::Ordering::Relaxed);
10513                    self.cancel
10514                        .store(true, std::sync::atomic::Ordering::Relaxed);
10515                    tracing::error!(
10516                        "span token graph failed after admission; sequence state cleared"
10517                    );
10518                    return vec![0.0; self.hidden_size];
10519                }
10520                None => None,
10521            };
10522            graph_note(span_res.is_some(), gl, upto_excl - from);
10523            if std::env::var("CMF_GPU_DEBUG").is_ok() {
10524                // How much of the span the graph actually covered. A
10525                // prefix of nothing means every layer walks per-op and
10526                // the split's extra cost is elsewhere.
10527                static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
10528                if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
10529                    eprintln!(
10530                        "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
10531                        upto_excl - from,
10532                        span_res.is_some()
10533                    );
10534                }
10535            }
10536            if let Some(hh) = span_res {
10537                if gl == upto_excl - from {
10538                    if !lg.is_empty() {
10539                        lg.resize(self.vocab_size, 0.0);
10540                        if let Some(c) = self.final_softcap {
10541                            for l in lg.iter_mut() {
10542                                *l = c * (*l / c).tanh();
10543                            }
10544                        }
10545                        self.graph_logits = Some(lg);
10546                    }
10547                    crate::gpu::set_layer(-1);
10548                    return hh;
10549                }
10550                // Partial device prefix of the span: CPU owns the tail.
10551                h = hh;
10552                tail_start = from + gl;
10553            }
10554        }
10555        let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
10556
10557        // A partial graph is an explicit GPU-prefix / CPU-tail split. Keep
10558        // the tail PURE host-side: letting its QTensor hooks re-enter the
10559        // residency arena streams every omitted layer through Vulkan and the
10560        // driver's freed-allocation cache can grow to the full model size
10561        // (25.4 GiB observed with a 14 GiB budget on Granite 30B Q8_2F).
10562        let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
10563        let automatic_gpu_prefix = self.automatic_gpu_prefix();
10564
10565        #[cfg(target_os = "macos")]
10566        let mut gpu_skip_until = 0usize;
10567        for li in tail_start.max(from)..self.num_layers {
10568            let _capacity_tail = automatic_gpu_prefix
10569                .filter(|&prefix| li >= prefix)
10570                .map(|_| crate::gpu::enter_cpu_scope());
10571            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU (CMF_GPU_LAYERS)
10572            if let Some(u) = upto {
10573                if li > u {
10574                    break;
10575                }
10576            }
10577            if let Some(mask) = task_mask {
10578                if !mask.layer_alive(li) {
10579                    continue; // dead layer: residual pass-through
10580                }
10581            }
10582            // Whole-block q1 token graph: a run of consecutive q1
10583            // layers — GDN and full attention — executes with one sync
10584            // per CPU attend instead of per op (macOS/Metal).
10585            #[cfg(target_os = "macos")]
10586            {
10587                if li < gpu_skip_until {
10588                    continue;
10589                }
10590                if task_mask.is_none() {
10591                    let end = self.q1_graph_gpu(li, upto, position, &mut h);
10592                    if self
10593                        .graph_failed
10594                        .load(std::sync::atomic::Ordering::Relaxed)
10595                    {
10596                        // The graph may have mutated device state before a
10597                        // command-buffer error. Never continue with a CPU
10598                        // tail or read a stale host mirror after admission.
10599                        return vec![0.0; self.hidden_size];
10600                    }
10601                    if end > li {
10602                        gpu_skip_until = end;
10603                        // Looped Transformer: the graph stopped at a loop
10604                        // boundary — apply final norm before the next iteration.
10605                        if self.is_loop_end(end - 1) && end < self.num_layers {
10606                            h = inference::rms_norm(
10607                                &h,
10608                                &self.weights.final_norm,
10609                                self.rms_eps,
10610                                self.norm_style,
10611                            );
10612                        }
10613                        continue;
10614                    }
10615                }
10616            }
10617
10618            let lw = &self.weights.layers[self.phys_layer(li)];
10619            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10620                if tp.parse::<usize>().ok() == Some(position) {
10621                    let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
10622                    eprintln!(
10623                        "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10624                        h[0], h[1]
10625                    );
10626                }
10627            }
10628            // Norm into the pipeline scratch — the returning rms_norm
10629            // allocated twice per layer per token (roadmap §3 P0).
10630            inference::rms_norm_into(
10631                &h,
10632                &lw.input_norm,
10633                self.rms_eps,
10634                self.norm_style,
10635                &mut self.ws.n1,
10636            );
10637
10638            let attn_out = match &lw.attn {
10639                AttnKind::Mla(w) => {
10640                    let inv_freq_l = self.layer_inv_freq(li);
10641                    let rs = self.layer_rope_scale(li);
10642                    let eps = self.rms_eps;
10643                    let pool = self.pool.clone();
10644                    mla_attention(
10645                        w,
10646                        &self.ws.n1,
10647                        &mut self.kv_cache.layers[li],
10648                        position,
10649                        &inv_freq_l,
10650                        rs,
10651                        eps,
10652                        pool.as_deref(),
10653                    )
10654                }
10655                AttnKind::Linear(w) => {
10656                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
10657                    vmf_phase_forward(
10658                        &self.ws.n1,
10659                        w,
10660                        &cfg,
10661                        &mut self.kv_cache.layers[li].linear_state,
10662                        self.pool.as_deref(),
10663                    )
10664                }
10665                AttnKind::Kda(w) => {
10666                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10667                    crate::linear_core::kda_forward(
10668                        &self.ws.n1,
10669                        w,
10670                        &cfg,
10671                        &mut self.kv_cache.layers[li].linear_state,
10672                        self.pool.as_deref(),
10673                    )
10674                }
10675                AttnKind::LinearGdn(w) => {
10676                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10677                    gdn_forward(
10678                        &self.ws.n1,
10679                        w,
10680                        &cfg,
10681                        &mut self.kv_cache.layers[li].linear_state,
10682                        self.pool.as_deref(),
10683                    )
10684                }
10685                AttnKind::ShortConv(w) => {
10686                    let cfg = self
10687                        .short_conv_cfg
10688                        .expect("short-conv layer without short_conv_cfg");
10689                    short_conv_forward(
10690                        &self.ws.n1,
10691                        w,
10692                        &cfg,
10693                        &mut self.kv_cache.layers[li].linear_state,
10694                        self.pool.as_deref(),
10695                    )
10696                }
10697                AttnKind::Full {
10698                    wq,
10699                    wk,
10700                    wv,
10701                    wo,
10702                    q_norm,
10703                    k_norm,
10704                    output_gate,
10705                    softplus_gate,
10706                    bias,
10707                } if self.kv_cache.layers[li].o1_sealed() => {
10708                    // O(1) override: decode on the sealed Nyström state
10709                    // instead of the growing KV cache.
10710                    let inv_freq_l = self.layer_inv_freq(li);
10711                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10712                    let cfg = QwenAttnCfg {
10713                        num_heads: self.layer_num_heads(li),
10714                        num_kv_heads: nkv_l,
10715                        head_dim: hd_l,
10716                        hidden_size: hs,
10717                        position,
10718                        inv_freq: &inv_freq_l,
10719                        rotary_dim: rd_l,
10720                        scale: self.attn_scale,
10721                        softcap: self.attn_softcap,
10722                        window: None,
10723                        v_norm: self.attn_v_norm,
10724                        qk_norm_after_rope: self.qk_norm_after_rope,
10725                        q_norm: q_norm.as_deref(),
10726                        k_norm: k_norm.as_deref(),
10727                        output_gate: *output_gate,
10728                        softplus_gate: softplus_gate
10729                            .as_ref()
10730                            .map(|(gate, per_head)| (gate, *per_head)),
10731                        rope_scale: self.layer_rope_scale(li),
10732                        bias: bias
10733                            .as_ref()
10734                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10735                        rms_eps: eps,
10736                        norm_style: self.norm_style,
10737                        pool: pool.as_deref(),
10738                    };
10739                    attention::qwen_attention_nystrom(
10740                        &self.ws.n1,
10741                        wq,
10742                        wk,
10743                        wv,
10744                        wo,
10745                        &mut self.kv_cache.layers[li],
10746                        &cfg,
10747                    )
10748                }
10749                AttnKind::Full {
10750                    wq,
10751                    wk,
10752                    wv,
10753                    wo,
10754                    q_norm,
10755                    k_norm,
10756                    output_gate,
10757                    softplus_gate,
10758                    bias,
10759                } => 'attn: {
10760                    // wgpu token-graph attention (opt-in): whole sub-block in
10761                    // one submit, device K/V mirror. q1 only, no gate/bias/mask.
10762                    if graph_on
10763                        && !*output_gate
10764                        && softplus_gate.is_none()
10765                        && self.attention_heads_per_layer.is_none()
10766                        && bias.is_none()
10767                        && task_mask.is_none()
10768                    {
10769                        let inv_freq_l = self.layer_inv_freq(li);
10770                        let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10771                        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10772                        if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
10773                            wq.mapped_q1(),
10774                            wk.mapped_q1(),
10775                            wv.mapped_q1(),
10776                            wo.mapped_q1(),
10777                        ) {
10778                            let gm = gm.clone();
10779                            let mut out = vec![0f32; hs];
10780                            let cache = &self.kv_cache.layers[li];
10781                            if crate::gpu::attn_dropin(
10782                                &gm,
10783                                self.graph_kv_id,
10784                                li,
10785                                &self.ws.n1,
10786                                qi,
10787                                ki,
10788                                vi,
10789                                oi,
10790                                q_norm.as_deref(),
10791                                k_norm.as_deref(),
10792                                self.qk_norm_after_rope,
10793                                &inv_freq_l,
10794                                nh,
10795                                nkv_l,
10796                                hd_l,
10797                                rd_l,
10798                                hs,
10799                                position,
10800                                self.kv_cache.max_seq_len,
10801                                gemma,
10802                                eps as f32,
10803                                cache.k_heads(),
10804                                cache.v_heads(),
10805                                &mut out,
10806                            ) {
10807                                break 'attn out;
10808                            }
10809                        }
10810                    }
10811                    let masked = task_mask
10812                        .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
10813                        .unwrap_or(false);
10814                    let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
10815                    match (masked, f32_view) {
10816                        // Historical masked path (f32 slices; the loader
10817                        // keeps masked models in f32).
10818                        (true, (Some(q), Some(k), Some(v), Some(o))) => {
10819                            let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
10820                            attention::multi_head_attention(
10821                                &self.ws.n1,
10822                                q,
10823                                k,
10824                                v,
10825                                o,
10826                                &mut self.kv_cache.layers[li],
10827                                self.num_heads,
10828                                self.num_kv_heads,
10829                                self.head_dim,
10830                                self.hidden_size,
10831                                position,
10832                                &active_heads,
10833                                &self.inv_freq,
10834                            )
10835                        }
10836                        (masked, _) => {
10837                            if masked {
10838                                tracing::warn!(
10839                                    "layer {li}: head mask on quantized weights not \
10840                                     supported yet — executing dense"
10841                                );
10842                            }
10843                            let inv_freq_l = self.layer_inv_freq(li);
10844                            let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10845                            let cfg = QwenAttnCfg {
10846                                num_heads: self.layer_num_heads(li),
10847                                num_kv_heads: nkv_l,
10848                                head_dim: hd_l,
10849                                hidden_size: hs,
10850                                position,
10851                                inv_freq: &inv_freq_l,
10852                                rotary_dim: rd_l,
10853                                scale: self.attn_scale,
10854                                softcap: self.attn_softcap,
10855                                window: self.layer_window(li),
10856                                v_norm: self.attn_v_norm,
10857                                qk_norm_after_rope: self.qk_norm_after_rope,
10858                                q_norm: q_norm.as_deref(),
10859                                k_norm: k_norm.as_deref(),
10860                                output_gate: *output_gate,
10861                                softplus_gate: softplus_gate
10862                                    .as_ref()
10863                                    .map(|(gate, per_head)| (gate, *per_head)),
10864                                rope_scale: self.layer_rope_scale(li),
10865                                bias: bias
10866                                    .as_ref()
10867                                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10868                                rms_eps: eps,
10869                                norm_style: self.norm_style,
10870                                pool: pool.as_deref(),
10871                            };
10872                            attention::qwen_attention(
10873                                &self.ws.n1,
10874                                wq,
10875                                wk,
10876                                wv,
10877                                wo,
10878                                &mut self.kv_cache.layers[li],
10879                                &cfg,
10880                            )
10881                        }
10882                    }
10883                }
10884            };
10885            // Gemma sandwich norm: normalize the attention branch before
10886            // it joins the residual stream.
10887            let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
10888                Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
10889                None => attn_out,
10890            };
10891            let lw = &self.weights.layers[self.phys_layer(li)];
10892            inference::add_rmsnorm_fused_into(
10893                &mut h,
10894                &attn_out,
10895                &lw.post_norm,
10896                self.rms_eps,
10897                self.norm_style,
10898                &mut self.ws.p1,
10899            );
10900            let mut attn_out = attn_out;
10901            attention::recycle_buf(&mut attn_out);
10902            let post_normed = &self.ws.p1;
10903
10904            let ffn_masked = task_mask
10905                .map(|m| m.ffn_active_count(li) < self.intermediate_size)
10906                .unwrap_or(false);
10907            // One masked dense CONTRACT, dispatched by cost. The
10908            // activation-zeroing arm (the batched sweep's, validated
10909            // against the replica to 0.8%) computes the FULL fused FFN
10910            // and zeroes the dead — right whenever most neurons live.
10911            // The sparse arm reads ONLY active rows and down columns —
10912            // per-row dots are slower per element than the fused kernel,
10913            // so it pays only once the mask is deep enough. The 0.5
10914            // crossover is first-principles (fused kernels run ~2x the
10915            // per-row dot throughput); a shallow specialist (95% alive)
10916            // stays fused, a --target-sparsity bake flips arms on its
10917            // own weight.
10918            let ffn_out = match (ffn_masked, &lw.ffn) {
10919                // A defragged tube layer answers its own mask: the core
10920                // always runs, each tube runs when its bit is on, and
10921                // the tubes that are off are never read from the mmap.
10922                (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
10923                    let row = task_mask
10924                        .and_then(|tm| tm.ffn_masks.get(li))
10925                        .map(|v| v.as_slice());
10926                    tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
10927                }
10928                (true, FfnKind::Dense(d)) => {
10929                    let tm = task_mask.unwrap();
10930                    let alive = tm.ffn_active_count(li);
10931                    let deep = alive * 2 <= self.intermediate_size;
10932                    if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
10933                        let active = tm.ffn_active_indices(li);
10934                        sparse_ffn_quant(
10935                            d,
10936                            post_normed,
10937                            &active,
10938                            self.hidden_size,
10939                            self.pool.as_deref(),
10940                        )
10941                    } else if deep
10942                        && let (Some(g), Some(u), Some(dn)) = (
10943                            d.gate_proj.as_f32(),
10944                            d.up_proj.as_f32(),
10945                            d.down_proj.as_f32(),
10946                        )
10947                    {
10948                        let active = tm.ffn_active_indices(li);
10949                        inference::sparse_ffn_forward(
10950                            post_normed,
10951                            g,
10952                            u,
10953                            dn,
10954                            self.hidden_size,
10955                            self.intermediate_size,
10956                            &active,
10957                            self.pool.as_deref(),
10958                        )
10959                    } else {
10960                        let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
10961                        dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
10962                    }
10963                }
10964                (true, FfnKind::Moe(m)) => {
10965                    // MoE is sparse by expert selection; a task mask
10966                    // narrows the ROUTABLE set via its expert fields
10967                    // (spec §5) when it carries them.
10968                    let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
10969                    ffn_forward(
10970                        &lw.ffn,
10971                        post_normed,
10972                        self.pool.as_deref(),
10973                        allowed.as_deref(),
10974                    )
10975                }
10976                (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
10977                    dm,
10978                    post_normed,
10979                    &h,
10980                    self.rms_eps,
10981                    self.norm_style,
10982                    self.pool.as_deref(),
10983                ),
10984                (false, _) => match &lw.ffn {
10985                    FfnKind::DenseMoe(dm) => dense_moe_ffn(
10986                        dm,
10987                        post_normed,
10988                        &h,
10989                        self.rms_eps,
10990                        self.norm_style,
10991                        self.pool.as_deref(),
10992                    ),
10993                    _ => {
10994                        let allowed = match (&lw.ffn, task_mask) {
10995                            (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
10996                            _ => None,
10997                        };
10998                        ffn_forward(
10999                            &lw.ffn,
11000                            post_normed,
11001                            self.pool.as_deref(),
11002                            allowed.as_deref(),
11003                        )
11004                    }
11005                },
11006            };
11007            let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
11008                Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
11009                None => ffn_out,
11010            };
11011            for (i, &f) in ffn_out.iter().enumerate() {
11012                h[i] += f;
11013            }
11014            let mut ffn_out = ffn_out;
11015            attention::recycle_buf(&mut ffn_out);
11016
11017            // Gemma-4: the layer output is scaled by a learned scalar.
11018            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
11019                for v in h.iter_mut() {
11020                    *v *= sc;
11021                }
11022            }
11023
11024            // Looped Transformer: apply final norm at the end of each loop iteration.
11025            // Nanbeige 4.2: after layer 21 (virtual), apply norm before looping back to layer 0.
11026            if self.is_loop_end(li) && li + 1 < self.num_layers {
11027                h = inference::rms_norm(
11028                    &h,
11029                    &self.weights.final_norm,
11030                    self.rms_eps,
11031                    self.norm_style,
11032                );
11033            }
11034
11035            // Dynamic routing φ capture (on-policy): the
11036            // EMA of the post-residual hidden at the router's phi_layer,
11037            // updated as the context evolves during decode.
11038            if self.dyn_phi_layer == Some(li) {
11039                self.update_dyn_phi(&h);
11040            }
11041        }
11042        crate::gpu::set_layer(-1); // layers done — lm_head outside layer-split
11043        if let Some(t) = t_race_cpu {
11044            crate::gpu::graph_race_record(false, t.elapsed());
11045        }
11046
11047        h
11048    }
11049
11050    /// EMA of φ at the router layer (rolling, weight 0.2 = ~5-token
11051    /// horizon). First observation seeds it exactly.
11052    fn update_dyn_phi(&mut self, h: &[f32]) {
11053        const A: f32 = 0.2;
11054        if self.dyn_phi_ema.len() != h.len() {
11055            self.dyn_phi_ema = vec![0.0; h.len()];
11056            self.dyn_phi_seen = 0;
11057        }
11058        if self.dyn_phi_seen == 0 {
11059            self.dyn_phi_ema.copy_from_slice(h);
11060        } else {
11061            for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
11062                *e = (1.0 - A) * *e + A * v;
11063            }
11064        }
11065        self.dyn_phi_seen += 1;
11066    }
11067
11068    /// Current router φ (EMA at phi_layer); empty until first capture.
11069    pub fn dyn_phi(&self) -> &[f32] {
11070        &self.dyn_phi_ema
11071    }
11072
11073    /// Enable/disable φ capture at the router layer, reset the EMA.
11074    pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
11075        self.dyn_phi_layer = layer;
11076        self.dyn_phi_ema.clear();
11077        self.dyn_phi_seen = 0;
11078    }
11079
11080    /// Skills eligible for dynamic switching: (index, id, phi_layer).
11081    pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
11082        let Some(model) = &self.model else {
11083            return Vec::new();
11084        };
11085        model
11086            .header
11087            .skills
11088            .iter()
11089            .enumerate()
11090            .filter_map(|(i, sk)| {
11091                let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
11092                let sel = sk.selection.as_ref()?;
11093                (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
11094            })
11095            .collect()
11096    }
11097
11098    /// Index of the currently overlaid skill (None = backbone).
11099    pub fn active_skill(&self) -> Option<usize> {
11100        self.dyn_active
11101    }
11102
11103    /// Enable dynamic per-token skill routing: build the hysteresis
11104    /// router from the container's routable skills, start φ capture at
11105    /// their (shared) phi_layer. Returns the number of routable skills
11106    /// (0 = nothing to route; router stays off). Idempotent.
11107    pub fn enable_dynamic_routing(&mut self) -> usize {
11108        use crate::swarm::{DynRouter, RoutableSkill};
11109        let Some(model) = self.model.clone() else {
11110            return 0;
11111        };
11112        // A blend materialized f32 working tensors into the layers; there
11113        // is no single skill index to revert from → refuse (honest).
11114        if self.dyn_blend_loaded {
11115            tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
11116            return 0;
11117        }
11118        // A statically-overlaid skill that is NOT FFN-eligible can't be
11119        // cheaply reverted at generation start → refuse rather than
11120        // silently keep it overlaid.
11121        if let Some(a) = self.dyn_active {
11122            if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
11123                tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
11124                return 0;
11125            }
11126        }
11127        let hidden = self.hidden_size;
11128        let mut skills = Vec::new();
11129        for (idx, id, _phi) in self.dynamic_skills() {
11130            if let Some(sel) = model.header.skills[idx].selection.as_ref() {
11131                if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
11132                    skills.push(rs);
11133                }
11134            }
11135        }
11136        if skills.is_empty() {
11137            return 0;
11138        }
11139        // Skills should share a phi_layer; warn (not fail) if they don't.
11140        let phi = skills[0].phi_layer;
11141        if skills.iter().any(|s| s.phi_layer != phi) {
11142            tracing::warn!("routable skills disagree on phi_layer; using {phi}");
11143        }
11144        let n = skills.len();
11145        self.set_dyn_phi_layer(Some(phi));
11146        self.dyn_router = Some(DynRouter::new(skills));
11147        n
11148    }
11149
11150    /// Human-readable switch log from the last dynamic-routed generation.
11151    pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
11152        self.dyn_router
11153            .as_ref()
11154            .map(|r| r.switches.clone())
11155            .unwrap_or_default()
11156    }
11157
11158    /// LM head: hidden → logits [vocab_size]. The dominant matvec of
11159    /// every decode step — row-parallel on the worker pool.
11160    fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
11161        let rows = self.weights.lm_head.rows();
11162        let mut logits = attention::take_buf(rows.min(self.vocab_size));
11163        self.weights
11164            .lm_head
11165            .matvec(hidden, &mut logits, self.pool.as_deref());
11166        logits.resize(self.vocab_size, 0.0);
11167        if let Some(m) = self.logit_multiplier {
11168            for l in logits.iter_mut() {
11169                *l *= m;
11170            }
11171        }
11172        if let Some(c) = self.final_softcap {
11173            for l in logits.iter_mut() {
11174                *l = c * (*l / c).tanh();
11175            }
11176        }
11177        if let Some(cm) = self.head_clusters.as_ref() {
11178            self.hierarchical_head_logprobs(hidden, cm, &mut logits);
11179        }
11180        logits
11181    }
11182
11183    /// Two-level head (Cortiq Embryo): in place, logits[v] ← log p(v) =
11184    /// (lc[c] − lse(lc)) + (logit[v] − lse over v's cluster block), c = v / S.
11185    fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
11186        let h = hidden.len();
11187        let ncl = cm.len() / h.max(1);
11188        if ncl == 0 || logits.len() % ncl != 0 {
11189            return;
11190        }
11191        let cs = logits.len() / ncl;
11192        // cluster logits + log-softmax
11193        let mut lc = vec![0.0f32; ncl];
11194        for c in 0..ncl {
11195            let row = &cm[c * h..(c + 1) * h];
11196            let mut s = 0.0f32;
11197            for j in 0..h {
11198                s += row[j] * hidden[j];
11199            }
11200            lc[c] = s;
11201        }
11202        let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11203        let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
11204        for c in 0..ncl {
11205            let blk = &mut logits[c * cs..(c + 1) * cs];
11206            let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11207            let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
11208            let add = lc[c] - lse - bl;
11209            for v in blk.iter_mut() {
11210                *v += add;
11211            }
11212        }
11213    }
11214
11215    /// Prefill `ids` and return the next-token logits — what the model
11216    /// would predict next, WITHOUT committing to generation (introspection
11217    /// for `cortiq explain`). Clears and repopulates the KV cache; leaves
11218    /// the active overlay untouched.
11219    pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
11220        self.clear_sequence_state();
11221        // This helper is used by the pooled classification endpoint, where
11222        // every request is a fresh sequence. The shared reset also clears the
11223        // wgpu token graph's device-side recurrent state.
11224        crate::gpu::graph_race_begin_generation();
11225        if task_mask.is_none() {
11226            self.o1_begin();
11227        }
11228        let mut hidden = vec![0.0f32; self.hidden_size];
11229        for (pos, &id) in ids.iter().enumerate() {
11230            let emb = self.embed_single(id);
11231            hidden = self.forward_layers(&emb, pos, task_mask);
11232        }
11233        if let Err(err) = self.o1_seal_checked() {
11234            self.o1_fail(err);
11235        }
11236        inference::rms_norm_into(
11237            &hidden,
11238            &self.weights.final_norm,
11239            self.rms_eps,
11240            self.norm_style,
11241            &mut self.ws.n1,
11242        );
11243        self.lm_head_forward(&self.ws.n1)
11244    }
11245}
11246
11247/// Convenience: deterministic tiny pipeline for tests.
11248pub fn create_test_pipeline(
11249    hidden_size: usize,
11250    intermediate_size: usize,
11251    num_heads: usize,
11252    num_kv_heads: usize,
11253    head_dim: usize,
11254    num_layers: usize,
11255    vocab_size: usize,
11256) -> Pipeline {
11257    // Small pseudo-random weights: constant weights make attention
11258    // degenerate and hide indexing bugs.
11259    let synth = |n: usize, salt: usize| -> Vec<f32> {
11260        (0..n)
11261            .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
11262            .collect()
11263    };
11264    let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
11265        QTensor::from_f32(synth(rows * cols, salt), rows, cols)
11266    };
11267    let layer_weights: Vec<LayerWeights> = (0..num_layers)
11268        .map(|li| LayerWeights {
11269            input_norm: vec![1.0; hidden_size],
11270            post_norm: vec![1.0; hidden_size],
11271            attn_out_norm: None,
11272            ffn_out_norm: None,
11273            layer_scale: None,
11274            ffn: FfnKind::Dense(DenseFfn {
11275                gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
11276                up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
11277                down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
11278                act: Act::Silu,
11279                down_t: None,
11280                segs: Vec::new(),
11281            }),
11282            attn: AttnKind::Full {
11283                bias: None,
11284                wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
11285                wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
11286                wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
11287                wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
11288                q_norm: None,
11289                k_norm: None,
11290                output_gate: false,
11291                softplus_gate: None,
11292            },
11293        })
11294        .collect();
11295
11296    Pipeline::new(
11297        Tokenizer::byte_level(),
11298        PipelineWeights {
11299            embed_tokens: qt(vocab_size, hidden_size, 100),
11300            layers: layer_weights,
11301            lm_head: qt(vocab_size, hidden_size, 200),
11302            final_norm: vec![1.0; hidden_size],
11303        },
11304        hidden_size,
11305        intermediate_size,
11306        num_heads,
11307        num_kv_heads,
11308        head_dim,
11309        num_layers,
11310        num_layers, // physical_layers = num_layers (non-looped)
11311        false,      // loop_final_norm
11312        vocab_size,
11313        1e-6,
11314        10_000.0,
11315        NormStyle::Qwen,
11316        4096,
11317        SamplerConfig {
11318            seed: Some(42),
11319            ..Default::default()
11320        },
11321    )
11322}
11323
11324/// Batched dense-FFN: gate/up/down via matmat (element-wise the same
11325/// math as b × dense_ffn — the same dot kernels).
11326/// One mask bit, LSB-first per byte — `TaskMask::ffn_active_indices`'s
11327/// convention.
11328#[inline]
11329fn mask_bit(row: &[u8], j: usize) -> bool {
11330    (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
11331}
11332
11333/// Zero the CLOSED neurons' activations in a [rows × inter] panel — the
11334/// masked-inference fast path's whole trick: full fused quant compute,
11335/// then the mask lands on the ACTIVATIONS, which is arithmetically the
11336/// pruned network without touching a quantized weight byte. Whole open
11337/// bytes (0xFF = 8 open neurons) skip in one test.
11338/// `CMF_FFN_MASK_GAIN` — Patent 12 FIG. 4, variance-preserving
11339/// rescaling: truncation removes a share of the layer's output energy,
11340/// so the survivors are scaled up to put the variance back where the
11341/// downstream norm expects it. A scalar here; per layer it is
11342/// `sqrt(total energy / kept energy)`.
11343fn mask_gain() -> f32 {
11344    static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
11345    *G.get_or_init(|| {
11346        std::env::var("CMF_FFN_MASK_GAIN")
11347            .ok()
11348            .and_then(|v| v.parse().ok())
11349            .unwrap_or(1.0)
11350    })
11351}
11352
11353fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
11354    // With CMF_FFN_MEANFILL a closed neuron contributes its average
11355    // instead of nothing — same bytes read, one constant restored.
11356    let fill = meanfill().and_then(|(i, v)| {
11357        let li = crate::gpu::cur_layer();
11358        (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
11359    });
11360    for r in 0..rows {
11361        let base = r * inter;
11362        for (bi, &byte) in row.iter().enumerate() {
11363            if byte == 0xFF {
11364                continue;
11365            }
11366            let j0 = bi * 8;
11367            for bit in 0..8 {
11368                let j = j0 + bit;
11369                if j < inter && byte & (1 << bit) == 0 {
11370                    g[base + j] = fill.map_or(0.0, |f| f[j]);
11371                }
11372            }
11373        }
11374    }
11375    let gain = mask_gain();
11376    if gain != 1.0 {
11377        for v in g[..rows * inter].iter_mut() {
11378            *v *= gain;
11379        }
11380    }
11381}
11382
11383/// True when neuron `i`'s bit is set (no mask = everything runs).
11384#[inline]
11385fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
11386    row.is_none_or(|r| mask_bit(r, i))
11387}
11388
11389/// Every bit below `n` set — the common case for a tube file's CORE,
11390/// where only the tube bits vary per task.
11391fn all_bits_on(row: &[u8], n: usize) -> bool {
11392    (0..n).all(|i| mask_bit(row, i))
11393}
11394
11395/// `CMF_TUBE_TOPK` — how many tubes a TOKEN may open (0 = the task mask
11396/// decides alone). This is the dense FFN read as a mixture: the tubes
11397/// are the experts a k-means over `gate_proj` rows found, and the token
11398/// picks among them. `CMF_TUBE_SCORE=gate` scores a tube by its own
11399/// gate (realizable: only `up`/`down` of the losers go unread),
11400/// `=oracle` scores by the true `silu(gate)·up` mass (the ceiling —
11401/// only `down` is saved, and the selection has read what it predicts).
11402fn tube_topk() -> usize {
11403    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11404    *K.get_or_init(|| {
11405        std::env::var("CMF_TUBE_TOPK")
11406            .ok()
11407            .and_then(|v| v.parse().ok())
11408            .unwrap_or(0)
11409    })
11410}
11411
11412fn tube_score_oracle() -> bool {
11413    static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11414    *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
11415}
11416
11417/// The routed arm of `tube_ffn`: a token opens only its best `k` tubes.
11418/// At `b == 1` (decode) the losers are genuinely never read — that is
11419/// the speed. At `b > 1` (the scoring sweep) every tube is computed and
11420/// the losers' activations are zeroed instead: same arithmetic, so the
11421/// perplexity is the routed model's, measured without a per-token
11422/// gather in the middle of a GEMM.
11423fn tube_ffn_routed(
11424    d: &DenseFfn,
11425    xs: &[f32],
11426    b: usize,
11427    pool: Option<&Pool>,
11428    mask_row: Option<&[u8]>,
11429    k: usize,
11430) -> Vec<f32> {
11431    let hidden = d.down_proj.rows();
11432    let core = d.gate_proj.rows();
11433    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11434    let mut out = match (b, core_full, mask_row) {
11435        (1, true, _) => dense_ffn(d, xs, pool),
11436        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11437        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11438        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11439    };
11440    let cand: Vec<usize> = (0..d.segs.len())
11441        .filter(|&i| tube_bit(mask_row, d.segs[i].start))
11442        .collect();
11443    if cand.is_empty() {
11444        return out;
11445    }
11446    // gate (and, where the score or the batch needs it, up) per tube.
11447    // The SCORE is taken at the point the serving path could take it:
11448    // off the gate alone, or off the finished activation for the oracle.
11449    let oracle = tube_score_oracle();
11450    let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
11451    let mut scores = vec![0f32; b * cand.len()];
11452    for (ci, &i) in cand.iter().enumerate() {
11453        let seg = &d.segs[i];
11454        let w = seg.width;
11455        let mut g = vec![0.0f32; b * w];
11456        if b == 1 {
11457            seg.gate.matvec(xs, &mut g, pool);
11458        } else {
11459            seg.gate.matmat(xs, b, &mut g, pool);
11460        }
11461        for v in g.iter_mut() {
11462            *v = Act::Silu.combine(*v, 1.0);
11463        }
11464        if !oracle {
11465            for t in 0..b {
11466                scores[t * cand.len() + ci] =
11467                    g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11468            }
11469        }
11470        if oracle || b > 1 {
11471            let mut u = vec![0.0f32; b * w];
11472            if b == 1 {
11473                seg.up.matvec(xs, &mut u, pool);
11474            } else {
11475                seg.up.matmat(xs, b, &mut u, pool);
11476            }
11477            for (a, &v) in g.iter_mut().zip(u.iter()) {
11478                *a *= v;
11479            }
11480            if oracle {
11481                for t in 0..b {
11482                    scores[t * cand.len() + ci] =
11483                        g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11484                }
11485            }
11486        }
11487        acts.push(g);
11488    }
11489    // per-token scores and the winners
11490    let keep = k.min(cand.len());
11491    let mut scratch: Vec<f32> = Vec::new();
11492    for t in 0..b {
11493        let mut sc: Vec<(f32, usize)> = (0..cand.len())
11494            .map(|ci| (scores[t * cand.len() + ci], ci))
11495            .collect();
11496        sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
11497        let mut alive = vec![false; cand.len()];
11498        for &(_, ci) in sc.iter().take(keep) {
11499            alive[ci] = true;
11500        }
11501        if b > 1 {
11502            for (ci, a) in acts.iter_mut().enumerate() {
11503                if !alive[ci] {
11504                    let w = d.segs[cand[ci]].width;
11505                    a[t * w..(t + 1) * w].fill(0.0);
11506                }
11507            }
11508        } else {
11509            // decode: finish only the winners — the losers' up/down
11510            // (and, with the gate score, everything but their gate)
11511            // are never touched.
11512            for (ci, &i) in cand.iter().enumerate() {
11513                if !alive[ci] {
11514                    continue;
11515                }
11516                let seg = &d.segs[i];
11517                let w = seg.width;
11518                let g = &mut acts[ci];
11519                if !tube_score_oracle() {
11520                    scratch.clear();
11521                    scratch.resize(w, 0.0);
11522                    seg.up.matvec(xs, &mut scratch, pool);
11523                    for (a, &v) in g.iter_mut().zip(scratch.iter()) {
11524                        *a *= v;
11525                    }
11526                }
11527                let mut acc = vec![0.0f32; hidden];
11528                seg.down.matvec(g, &mut acc, pool);
11529                for (o, a) in out.iter_mut().zip(&acc) {
11530                    *o += *a;
11531                }
11532            }
11533        }
11534    }
11535    if b > 1 {
11536        for (ci, &i) in cand.iter().enumerate() {
11537            let seg = &d.segs[i];
11538            let mut acc = vec![0.0f32; b * hidden];
11539            seg.down.matmat(&acts[ci], b, &mut acc, pool);
11540            for (o, a) in out.iter_mut().zip(&acc) {
11541                *o += *a;
11542            }
11543        }
11544    }
11545    out
11546}
11547
11548/// FFN of a defragged tube layer: the always-on core plus the tubes the
11549/// task mask switches on. Each tube is a normal tensor triple, so the
11550/// same kernels run it and an inactive tube's bytes are never read —
11551/// that is the whole point of the defrag (a scattered mask cannot skip
11552/// bytes; a contiguous one is just a smaller matrix).
11553fn tube_ffn(
11554    d: &DenseFfn,
11555    xs: &[f32],
11556    b: usize,
11557    pool: Option<&Pool>,
11558    mask_row: Option<&[u8]>,
11559) -> Vec<f32> {
11560    if tube_topk() > 0 {
11561        return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
11562    }
11563    let hidden = d.down_proj.rows();
11564    let core = d.gate_proj.rows();
11565    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11566    let mut out = match (b, core_full, mask_row) {
11567        (1, true, _) => dense_ffn(d, xs, pool),
11568        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11569        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11570        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11571    };
11572    TUBE_SCRATCH.with(|sc| {
11573        let mut sc = sc.borrow_mut();
11574        let [g, u, acc] = &mut *sc;
11575        for seg in &d.segs {
11576            if !tube_bit(mask_row, seg.start) {
11577                continue;
11578            }
11579            let w = seg.width;
11580            g.resize(b * w, 0.0);
11581            if b == 1
11582                && d.act == Act::Silu
11583                && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
11584            {
11585                // g holds silu(gate)·up.
11586            } else {
11587                u.resize(b * w, 0.0);
11588                if b == 1 {
11589                    QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
11590                } else {
11591                    seg.gate.matmat(xs, b, g, pool);
11592                    seg.up.matmat(xs, b, u, pool);
11593                }
11594                for i in 0..b * w {
11595                    g[i] = d.act.combine(g[i], u[i]);
11596                }
11597            }
11598            acc.resize(b * hidden, 0.0);
11599            acc.fill(0.0);
11600            if b == 1 {
11601                seg.down.matvec(g, acc, pool);
11602            } else {
11603                seg.down.matmat(g, b, acc, pool);
11604            }
11605            for (o, a) in out.iter_mut().zip(acc.iter()) {
11606                *o += *a;
11607            }
11608        }
11609        out
11610    })
11611}
11612
11613thread_local! {
11614    /// gate / up / down-accumulator scratch for the tube loop — a tube
11615    /// runs once per layer per token, and a fresh Vec each time is a
11616    /// malloc per tube per layer per token.
11617    static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
11618        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
11619}
11620
11621fn dense_ffn_batch(
11622    d: &DenseFfn,
11623    xs: &[f32],
11624    b: usize,
11625    pool: Option<&Pool>,
11626    mask_row: Option<&[u8]>,
11627) -> Vec<f32> {
11628    let inter = d.gate_proj.rows();
11629    let hidden = d.down_proj.rows();
11630    // Fused on-device SwiGLU when the device is in play: three separate
11631    // `matmat` calls are three round trips per layer, and the gate/up
11632    // panels (b × inter — 22 MB each at a 512-token chunk) cross the bus
11633    // twice for nothing. The kernel already existed for the image DiT;
11634    // the LLM prefill was simply never wired to it. A task mask needs the
11635    // activations on the host between the halves, so it keeps the CPU
11636    // arm below.
11637    if mask_row.is_none()
11638        && d.act == Act::Silu
11639        && b >= 32
11640        && crate::gpu::enabled_here()
11641        && !crate::gpu::mm_killed()
11642        // The refit pass needs this layer's activations on the host; the
11643        // fused chain keeps them on the device. Refusing it here costs
11644        // one round trip and keeps every GEMM on the card — the
11645        // alternative was running the whole calibration on the CPU.
11646        && refit_dir().is_none()
11647        // Same for the mass/hit probes. The accumulator at the bottom of
11648        // this function only sees `g` when `g` came back to the host, so
11649        // a fused batch would leave it summing nothing — a probe that
11650        // reports zeros rather than failing, which is worse.
11651        && !ffn_probe_active()
11652    {
11653        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11654            d.gate_proj.mapped_q4t(),
11655            d.up_proj.mapped_q4t(),
11656            d.down_proj.mapped_q4t(),
11657        ) {
11658            let mut out = vec![0.0f32; b * hidden];
11659            if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11660                return out;
11661            }
11662        }
11663        // The q4tp twin (same kernel family, scale from the row ladder) —
11664        // the DiT has run it in production since the pipeline containers;
11665        // the LLM prefill was simply never wired to it, so a q4tp model's
11666        // prefill panels stayed on the CPU.
11667        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11668            d.gate_proj.mapped_q4tp(),
11669            d.up_proj.mapped_q4tp(),
11670            d.down_proj.mapped_q4tp(),
11671        ) {
11672            let mut out = vec![0.0f32; b * hidden];
11673            if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11674                return out;
11675            }
11676        }
11677    }
11678    let mut g = vec![0.0f32; b * inter];
11679    d.gate_proj.matmat(xs, b, &mut g, pool);
11680    let mut u = vec![0.0f32; b * inter];
11681    d.up_proj.matmat(xs, b, &mut u, pool);
11682    if gate_topk() > 0 && d.act == Act::Silu {
11683        for t in 0..b {
11684            let row = &mut g[t * inter..(t + 1) * inter];
11685            for v in row.iter_mut() {
11686                *v = Act::Silu.combine(*v, 1.0);
11687            }
11688            keep_top_k(row, gate_topk());
11689        }
11690        for i in 0..b * inter {
11691            g[i] *= u[i];
11692        }
11693    } else {
11694        for i in 0..b * inter {
11695            g[i] = d.act.combine(g[i], u[i]);
11696        }
11697    }
11698    if let Some(row) = mask_row {
11699        zero_masked_cols(&mut g, b, inter, row);
11700    }
11701    if oracle_topk() > 0 {
11702        for t in 0..b {
11703            keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
11704        }
11705    }
11706    let mut out = vec![0.0f32; b * hidden];
11707    d.down_proj.matmat(&g, b, &mut out, pool);
11708    if refit_dir().is_some() {
11709        let li = crate::gpu::cur_layer();
11710        if li >= 0 {
11711            refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
11712        }
11713    }
11714    // The DTG-MA probe, on the batched path: one prefill sweep gives the
11715    // same per-neuron statistic the per-position probe does, and on a 27B
11716    // that is minutes instead of hours.
11717    FFN_PROBE.with(|pr| {
11718        if let Some(acc) = pr.borrow_mut().as_mut() {
11719            let li = crate::gpu::cur_layer();
11720            if li < 0 {
11721                return;
11722            }
11723            let Some(row) = acc.get_mut(li as usize) else {
11724                return;
11725            };
11726            let sq = probe_sq();
11727            for t in 0..b {
11728                for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
11729                    *a += if sq {
11730                        (v as f64) * (v as f64)
11731                    } else {
11732                        (v as f64).abs()
11733                    };
11734                }
11735            }
11736        }
11737    });
11738    out
11739}
11740
11741/// Batched MoE-FFN: router batched, positions are GROUPED by expert —
11742/// an expert's weights are read once for all its positions in the chunk
11743/// (the main prefill-GEMM win on MoE: 960MB/token of 35B experts).
11744/// Accumulate per-channel activation energy for `CMF_RMS_TRACE`.
11745fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
11746    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11747    static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11748    let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
11749    let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
11750    if (!on && !dump) || b == 0 {
11751        return;
11752    }
11753    let hidden = xs.len() / b;
11754    if on {
11755        let mut acc = m.act_sq.borrow_mut();
11756        if acc.len() < hidden {
11757            acc.resize(hidden, 0.0);
11758        }
11759        for t in 0..b {
11760            let row = &xs[t * hidden..(t + 1) * hidden];
11761            for (a, &v) in acc.iter_mut().zip(row) {
11762                *a += (v as f64) * (v as f64);
11763            }
11764        }
11765    }
11766    if dump {
11767        // Cap the capture: the covariance needs a few thousand rows, and a
11768        // whole prefill of every layer would be gigabytes for no extra rank.
11769        let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
11770            .ok()
11771            .and_then(|v| v.parse().ok())
11772            .unwrap_or(4096);
11773        let mut rows = m.act_rows.borrow_mut();
11774        if rows.len() < cap * hidden {
11775            let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
11776            rows.extend_from_slice(&xs[..take * hidden]);
11777        }
11778    }
11779}
11780
11781/// Send-able cursor over a Vec-of-Vecs: each pool worker writes only its
11782/// own slots (disjoint by construction in the caller).
11783#[derive(Clone, Copy)]
11784struct SendVecs(*mut Vec<f32>);
11785unsafe impl Send for SendVecs {}
11786unsafe impl Sync for SendVecs {}
11787impl SendVecs {
11788    #[inline]
11789    fn at(self, i: usize) -> *mut Vec<f32> {
11790        unsafe { self.0.add(i) }
11791    }
11792}
11793
11794fn moe_ffn_batch(
11795    m: &MoeFfn,
11796    xs: &[f32],
11797    b: usize,
11798    hidden: usize,
11799    pool: Option<&Pool>,
11800    allowed: Option<&[bool]>,
11801) -> Vec<f32> {
11802    accumulate_act(m, xs, b);
11803    let ne = m.experts.len();
11804    let mut logits = vec![0.0f32; b * ne];
11805    match &m.resonance {
11806        Some(r) => {
11807            let hdim = xs.len() / b.max(1);
11808            for bi in 0..b {
11809                r.scores(
11810                    &xs[bi * hdim..(bi + 1) * hdim],
11811                    &mut logits[bi * ne..(bi + 1) * ne],
11812                );
11813            }
11814        }
11815        None => m.router.matmat(xs, b, &mut logits, pool),
11816    }
11817
11818    // Assignments: expert → [(position, weight)] — same routing as
11819    // moe_ffn, per position (see `moe_route`).
11820    let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
11821    {
11822        let mut st = m.stats.borrow_mut();
11823        if st.len() < ne {
11824            st.resize(ne, 0);
11825        }
11826        for bi in 0..b {
11827            let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
11828            for &e in &idx {
11829                st[e] += 1;
11830                assign[e].push((bi, p[e] / wsum));
11831            }
11832        }
11833    }
11834
11835    let mut out = vec![0.0f32; b * hidden];
11836    let cols = m.experts[0].gate_proj.cols();
11837    let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
11838        let sb = list.len();
11839        let mut sub = vec![0.0f32; sb * cols];
11840        for (k, &(bi, _)) in list.iter().enumerate() {
11841            sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11842        }
11843        let eo = dense_ffn_batch(d, &sub, sb, pool, None);
11844        for (k, &(bi, w)) in list.iter().enumerate() {
11845            for i in 0..hidden {
11846                out[bi * hidden + i] += w * eo[k * hidden + i];
11847            }
11848        }
11849    };
11850    // Routed experts: the panels are TINY (b·top_k spread over every
11851    // expert — a few positions each), so a pool dispatch per expert is
11852    // pure barrier cost. Invert the parallelism: workers take WHOLE
11853    // experts (serial math inside), then one deterministic scatter in
11854    // expert order — the exact accumulation order the serial loop had.
11855    let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
11856    if pool.is_some() && active.len() >= 8 {
11857        let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
11858        {
11859            let panel_ptr = SendVecs(panels.as_mut_ptr());
11860            // Capture only the expert table: `m` itself carries RefCell
11861            // stats and must not cross the pool boundary.
11862            let experts = &m.experts;
11863            let (active_r, assign_r) = (&active, &assign);
11864            let run = |start: usize, end: usize| {
11865                for ai in start..end {
11866                    let e = active_r[ai];
11867                    let list = &assign_r[e];
11868                    let sb = list.len();
11869                    let mut sub = vec![0.0f32; sb * cols];
11870                    for (k, &(bi, _)) in list.iter().enumerate() {
11871                        sub[k * cols..(k + 1) * cols]
11872                            .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11873                    }
11874                    // SAFETY: each worker owns a disjoint panels[ai].
11875                    unsafe {
11876                        *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
11877                    }
11878                }
11879            };
11880            match pool {
11881                Some(p) => p.run_rows(active.len(), &run),
11882                None => run(0, active.len()),
11883            }
11884        }
11885        for (ai, &e) in active.iter().enumerate() {
11886            for (k, &(bi, w)) in assign[e].iter().enumerate() {
11887                let eo = &panels[ai][k * hidden..(k + 1) * hidden];
11888                for i in 0..hidden {
11889                    out[bi * hidden + i] += w * eo[i];
11890                }
11891            }
11892        }
11893    } else {
11894        for &e in &active {
11895            run_expert(&m.experts[e], &assign[e], &mut out);
11896        }
11897    }
11898    if let Some((se, gate)) = &m.shared {
11899        let all: Vec<(usize, f32)> = if let Some(gate) = gate {
11900            let mut gl = vec![0.0f32; b];
11901            gate.matmat(xs, b, &mut gl, pool);
11902            (0..b)
11903                .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
11904                .collect()
11905        } else {
11906            (0..b).map(|bi| (bi, 1.0)).collect()
11907        };
11908        run_expert(se, &all, &mut out);
11909    }
11910    out
11911}
11912
11913thread_local! {
11914    /// gate/up activation scratch for the dense FFN paths (single uses
11915    /// two slots, the fused pair all four) — these were fresh
11916    /// intermediate-size Vecs on every layer of every token.
11917    static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
11918        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
11919}
11920
11921/// Dense SwiGLU FFN through QTensor matvecs (any storage).
11922fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11923    // Per-token sparsity, when the file was built for it: gate first,
11924    // then only the chosen neurons' up/down rows leave the mmap.
11925    if gate_topk() > 0
11926        && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
11927    {
11928        return out;
11929    }
11930    // Whole-FFN GPU submit (этап 4.2 increment): gate → silu·up → down
11931    // chained in ONE command buffer with the intermediate activations
11932    // resident on the device — 3 per-op polls become 1 per layer. The
11933    // moe_block backend already implements exactly this chain; a dense
11934    // FFN is one expert with weight 1. Runtime probe: the chain still
11935    // pays one submit+poll per layer — alternate it against the pure-CPU
11936    // FFN and keep whichever is faster on this machine.
11937    // q1 FFNs offload at any practical size: the q1 CPU kernel is
11938    // compute-bound, so the UMA threshold logic does not apply — the
11939    // probe measures and decides either way.
11940    // The fused GPU block has no descriptor-aware Prism path: it would either
11941    // consume an unrotated activation or decline after inspecting the mixed
11942    // q2tp/q4tp tensors.  Do not let that structural refusal enter the FFN
11943    // probe's CPU_ONLY scope; the ordinary body below dispatches each matrix
11944    // through QTensor::matvec, which owns the signed FWHT + affine q2tp route.
11945    let prism_body = d.gate_proj.has_prism_contract()
11946        || d.up_proj.has_prism_contract()
11947        || d.down_proj.has_prism_contract();
11948    if !prism_body
11949        && crate::gpu::enabled_here()
11950        && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
11951    {
11952        let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
11953            crate::gpu::ProbeArm::Gpu
11954        } else {
11955            crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
11956        };
11957        match arm {
11958            crate::gpu::ProbeArm::Gpu => {
11959                let t0 = std::time::Instant::now();
11960                if let Some(out) = dense_ffn_gpu(d, x, pool) {
11961                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
11962                    return out;
11963                }
11964                // Declined: no timing exists, so say so. Silence here is
11965                // what left `ffn` undecided for 9000 calls and cost a
11966                // failed device attempt on half of them.
11967                crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
11968            }
11969            crate::gpu::ProbeArm::CpuTimed => {
11970                let t0 = std::time::Instant::now();
11971                let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11972                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
11973                return out;
11974            }
11975            crate::gpu::ProbeArm::Cpu => {
11976                return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11977            }
11978        }
11979    }
11980    dense_ffn_cpu(d, x, pool)
11981}
11982
11983/// The pure-CPU dense-FFN body (also the fallback of every GPU refusal).
11984fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11985    let inter = d.gate_proj.rows();
11986    FFN_SCRATCH.with(|s| {
11987        let mut s = s.borrow_mut();
11988        let [g, u, ..] = &mut *s;
11989        g.resize(inter, 0.0);
11990        // Fused gate+up+silu: one dispatch, no separate silu pass.
11991        // Falls back to matvec_many + silu loop for unsupported dtypes.
11992        if gate_topk() > 0 {
11993            // Gate first, select, and only then pay for `up`: the
11994            // measurement arm computes both and zeroes the losers, which
11995            // is the same arithmetic.
11996            u.resize(inter, 0.0);
11997            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11998            for i in 0..inter {
11999                g[i] = Act::Silu.combine(g[i], 1.0);
12000            }
12001            keep_top_k(g, gate_topk());
12002            for i in 0..inter {
12003                g[i] *= u[i];
12004            }
12005        } else if d.act == Act::Silu
12006            && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
12007        {
12008            // g now holds silu(gate)·up directly.
12009        } else {
12010            u.resize(inter, 0.0);
12011            // Multi-matrix job: gate+up under one pool dispatch.
12012            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12013            for i in 0..inter {
12014                g[i] = d.act.combine(g[i], u[i]);
12015            }
12016        }
12017        // DTG-MA bake probe (Patent 2): accumulate this layer's
12018        // per-neuron activation mass while a probe pass is active.
12019        // `CMF_FFN_PROBE_TOPK=k` switches the statistic from mass to a
12020        // HIT COUNT — how many tokens rank the neuron in their own top
12021        // k. Mass asks "how loud is this neuron overall", the count
12022        // asks "how often does this task actually need it", and the two
12023        // rank neurons differently whenever a few tokens are loud.
12024        FFN_PROBE.with(|pr| {
12025            if let Some(acc) = pr.borrow_mut().as_mut() {
12026                let li = crate::gpu::cur_layer();
12027                if li >= 0 {
12028                    if let Some(row) = acc.get_mut(li as usize) {
12029                        match probe_topk() {
12030                            0 if probe_sq() => {
12031                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12032                                    *a += (v as f64) * (v as f64);
12033                                }
12034                            }
12035                            0 if probe_signed() => {
12036                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12037                                    *a += v as f64;
12038                                }
12039                            }
12040                            0 => {
12041                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12042                                    *a += (v as f64).abs();
12043                                }
12044                            }
12045                            k => {
12046                                let n = g.len();
12047                                let k = k.min(n);
12048                                let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12049                                let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12050                                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12051                                });
12052                                let thr = *kth;
12053                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12054                                    if v.abs() >= thr {
12055                                        *a += 1.0;
12056                                    }
12057                                }
12058                            }
12059                        }
12060                    }
12061                }
12062            }
12063        });
12064        if oracle_topk() > 0 {
12065            keep_top_k(g, oracle_topk());
12066        }
12067        {
12068            let li = crate::gpu::cur_layer();
12069            if li >= 0 {
12070                adump_row(li as usize, g);
12071            }
12072        }
12073        let mut out = attention::take_buf(d.down_proj.rows());
12074        d.down_proj.matvec(g, &mut out, pool);
12075        out
12076    })
12077}
12078
12079/// Online accumulators for the AWNP refit of a narrowed FFN.
12080///
12081/// The refit needs `Gss = A_SᵀA_S` and `YA = YᵀA_S` per layer, where `A_S`
12082/// are the calibration activations of the KEPT neurons and `Y` the full
12083/// FFN output. Both are small enough to hold; the thing that is not is
12084/// the activations they are built from — a 27B layer would dump a
12085/// gigabyte per thousand tokens. So they are accumulated as the
12086/// calibration runs and written once at the end.
12087///
12088/// `CMF_FFN_REFIT=<dir>` holds `support.<L>.u32` (a u32 count then the
12089/// kept indices) for every layer to accumulate; `CMF_FFN_REFIT_FROM/TO`
12090/// bound the layer span so the accumulators fit in RAM.
12091pub struct RefitAcc {
12092    pub support: Vec<u32>,
12093    pub gss: Vec<f32>,
12094    pub ya: Vec<f32>,
12095    pub hidden: usize,
12096    pub tokens: u64,
12097    /// Activations staged transposed ([ns, t] and [hidden, t]) until the
12098    /// batch is worth a GEMM. The product costs `ns²` to move and add
12099    /// REGARDLESS of how many tokens went into it, so folding 16 chunks
12100    /// into one call cuts that cost 16× — it was 15 TB of traffic per
12101    /// calibration pass at one call per 256 tokens.
12102    pub buf_g: Vec<f32>,
12103    pub buf_o: Vec<f32>,
12104    pub buf_t: usize,
12105}
12106
12107/// The product buffer is SHARED across layers — one 473 MB allocation,
12108/// not one per layer (that was 30 GB of nothing on a 64-layer model).
12109/// It lives under the same lock as the accumulators.
12110type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
12111
12112static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
12113    std::sync::OnceLock::new();
12114
12115/// Is an FFN probe accumulator installed on this thread? The fused GPU
12116/// FFN must decline while one is, or the probe silently measures zero.
12117fn ffn_probe_active() -> bool {
12118    FFN_PROBE.with(|p| p.borrow().is_some())
12119}
12120
12121fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
12122    REFIT
12123        .get_or_init(|| {
12124            std::env::var("CMF_FFN_REFIT").ok().map(|d| {
12125                (
12126                    d,
12127                    std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
12128                )
12129            })
12130        })
12131        .as_ref()
12132}
12133
12134/// Accumulate one prefill panel into the layer's refit statistics.
12135fn refit_accumulate(
12136    li: usize,
12137    g: &[f32],
12138    b: usize,
12139    inter: usize,
12140    out: &[f32],
12141    hidden: usize,
12142    pool: Option<&Pool>,
12143) {
12144    let Some((dir, map)) = refit_dir() else {
12145        return;
12146    };
12147    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12148    let (from, to) = *SPAN.get_or_init(|| {
12149        let g = |k: &str, d: usize| {
12150            std::env::var(k)
12151                .ok()
12152                .and_then(|v| v.parse().ok())
12153                .unwrap_or(d)
12154        };
12155        (
12156            g("CMF_FFN_REFIT_FROM", 0),
12157            g("CMF_FFN_REFIT_TO", usize::MAX),
12158        )
12159    });
12160    if li < from || li > to {
12161        return;
12162    }
12163    let mut guard = map.lock().unwrap();
12164    let (map, shared) = &mut *guard;
12165    let acc = match map.entry(li) {
12166        std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
12167        std::collections::hash_map::Entry::Vacant(e) => {
12168            let path = format!("{dir}/support.{li}.u32");
12169            let Ok(bytes) = std::fs::read(&path) else {
12170                eprintln!("refit: no {path} — layer {li} skipped");
12171                return;
12172            };
12173            let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
12174            let support: Vec<u32> = bytes[4..4 + n * 4]
12175                .chunks_exact(4)
12176                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12177                .collect();
12178            eprintln!(
12179                "refit: layer {li} support {n} ({:.0} MB of accumulator)",
12180                (n * n + hidden * n) as f64 * 4.0 / 1e6
12181            );
12182            e.insert(RefitAcc {
12183                gss: vec![0.0; n * n],
12184                ya: vec![0.0; hidden * n],
12185                buf_g: Vec::new(),
12186                buf_o: Vec::new(),
12187                buf_t: 0,
12188                support,
12189                hidden,
12190                tokens: 0,
12191            })
12192        }
12193    };
12194    let ns = acc.support.len();
12195    // Stage this chunk transposed; the GEMM fires once the batch is full.
12196    let cap = refit_batch();
12197    if acc.buf_g.is_empty() {
12198        acc.buf_g = vec![0.0; ns * cap];
12199        acc.buf_o = vec![0.0; hidden * cap];
12200    }
12201    let take = b.min(cap - acc.buf_t);
12202    for t in 0..take {
12203        let col = acc.buf_t + t;
12204        for (j, &n) in acc.support.iter().enumerate() {
12205            acc.buf_g[j * cap + col] = g[t * inter + n as usize];
12206        }
12207        for h in 0..hidden {
12208            acc.buf_o[h * cap + col] = out[t * hidden + h];
12209        }
12210    }
12211    acc.buf_t += take;
12212    acc.tokens += take as u64;
12213    if acc.buf_t < cap {
12214        return;
12215    }
12216    let bt = acc.buf_t;
12217    acc.buf_t = 0;
12218    // The GEMM WRITES its C (it zeroes the accumulators it uses), so the
12219    // chunk product lands in scratch and is added on — the one thing that
12220    // silently turns a Gram over 13 000 tokens into a Gram over 256.
12221    // Both products are `C[n, m] += X[n, b] · Yᵀ[b, m]` with X and Y
12222    // stored row-major [·, b] — exactly `gemm_nt_f32`'s shape, so the
12223    // card does them when it is up (this is the whole calibration's
12224    // cost: O(|S|²) per token, 2.9 PFLOP for a 27B pass). The tiled CPU
12225    // loop stays as the fallback. Neither accumulates, so the product
12226    // lands in scratch and is added on.
12227    let RefitAcc {
12228        gss,
12229        ya,
12230        buf_g,
12231        buf_o,
12232        ..
12233    } = acc;
12234    let need = (ns * ns).max(hidden * ns);
12235    if shared.len() < need {
12236        shared.resize(need, 0.0);
12237    }
12238    let scratch = &mut shared[..];
12239    let _ = bt;
12240    if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
12241        add_into(gss, &scratch[..ns * ns], pool);
12242        if crate::gpu::gemm_nt_f32_transient(
12243            buf_o,
12244            buf_g,
12245            &mut scratch[..hidden * ns],
12246            hidden,
12247            cap,
12248            ns,
12249        ) {
12250            add_into(ya, &scratch[..hidden * ns], pool);
12251        } else {
12252            accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12253        }
12254    } else {
12255        accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
12256        accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12257    }
12258    // No zeroing: the batch is always filled exactly (cap is a multiple
12259    // of the prefill chunk), and a memset of 178 MB a layer would cost
12260    // more than the GEMM.
12261}
12262
12263/// `CMF_FFN_REFIT_BATCH` — tokens staged before each GEMM (default 4096).
12264fn refit_batch() -> usize {
12265    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12266    *B.get_or_init(|| {
12267        std::env::var("CMF_FFN_REFIT_BATCH")
12268            .ok()
12269            .and_then(|v| v.parse().ok())
12270            .unwrap_or(4096)
12271    })
12272}
12273
12274/// `c[m, n] += Σ_t left[m, t]·right[n, t]` — both operands transposed,
12275/// the CPU fallback for the staged batch.
12276fn accum_outer_t(
12277    c: &mut [f32],
12278    m: usize,
12279    n: usize,
12280    b: usize,
12281    left: &[f32],
12282    right: &[f32],
12283    pool: Option<&Pool>,
12284) {
12285    let ptr = SendMut(c.as_mut_ptr());
12286    let body = |i: usize| {
12287        let ptr = &ptr;
12288        let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
12289        for t in 0..b {
12290            let a = left[i * b + t];
12291            if a == 0.0 {
12292                continue;
12293            }
12294            for (j, o) in row.iter_mut().enumerate() {
12295                *o += a * right[j * b + t];
12296            }
12297        }
12298    };
12299    match pool {
12300        Some(p) if m > 1 => p.run_rows(m, &|s, e| {
12301            for i in s..e {
12302                body(i);
12303            }
12304        }),
12305        _ => {
12306            for i in 0..m {
12307                body(i);
12308            }
12309        }
12310    }
12311}
12312
12313/// `dst += src`, spread over the pool — at 118 M floats a layer this is
12314/// not a loop to leave on one core.
12315fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
12316    let n = dst.len().min(src.len());
12317    match pool {
12318        Some(p) if n >= 1 << 16 => {
12319            let ptr = SendMut(dst.as_mut_ptr());
12320            let f = |s: usize, e: usize| {
12321                let ptr = &ptr;
12322                for blk in s..e {
12323                    let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
12324                    for i in a..b {
12325                        unsafe { *ptr.0.add(i) += src[i] };
12326                    }
12327                }
12328            };
12329            p.run_rows(n.div_ceil(4096), &f);
12330        }
12331        _ => {
12332            for (d, v) in dst.iter_mut().zip(&src[..n]) {
12333                *d += *v;
12334            }
12335        }
12336    }
12337}
12338
12339/// `c[m, n] += Σ_t left[t, m]·right[t, n]`, with `left` stored [m, t] and
12340/// `right` [t, n]. Tiled over the rows of `c` so a tile stays in cache
12341/// while each token's `right` row streams past it once, and parallel
12342/// over tiles.
12343fn accum_outer(
12344    c: &mut [f32],
12345    m: usize,
12346    n: usize,
12347    b: usize,
12348    left: &[f32],
12349    right: &[f32],
12350    pool: Option<&Pool>,
12351) {
12352    const TILE: usize = 32;
12353    let tiles = m.div_ceil(TILE);
12354    let cp = SendMut(c.as_mut_ptr());
12355    let body = |ti: usize| {
12356        let cp = &cp;
12357        let i0 = ti * TILE;
12358        let i1 = (i0 + TILE).min(m);
12359        for t in 0..b {
12360            let r = &right[t * n..t * n + n];
12361            for i in i0..i1 {
12362                let a = left[i * b + t];
12363                if a == 0.0 {
12364                    continue;
12365                }
12366                // SAFETY: tiles partition c's rows; workers never overlap.
12367                let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
12368                for (o, v) in row.iter_mut().zip(r) {
12369                    *o += a * *v;
12370                }
12371            }
12372        }
12373    };
12374    match pool {
12375        Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
12376            for ti in s..e {
12377                body(ti);
12378            }
12379        }),
12380        _ => {
12381            for ti in 0..tiles {
12382                body(ti);
12383            }
12384        }
12385    }
12386}
12387
12388/// Write what the calibration accumulated: `gss.<L>.f32` and `ya.<L>.f32`.
12389pub fn refit_flush() -> usize {
12390    let Some((dir, map)) = refit_dir() else {
12391        return 0;
12392    };
12393    let guard = map.lock().unwrap();
12394    let mut n = 0;
12395    for (li, acc) in guard.0.iter() {
12396        // A silently truncated write here is a Gram that reshapes to
12397        // nothing an hour later — say it out loud instead.
12398        let w = |name: &str, v: &[f32]| {
12399            let path = format!("{dir}/{name}.{li}.f32");
12400            let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
12401            match std::fs::write(&path, &bytes) {
12402                Ok(()) => {}
12403                Err(e) => eprintln!(
12404                    "refit: FAILED to write {path} ({} MB): {e}",
12405                    bytes.len() / 1_000_000
12406                ),
12407            }
12408        };
12409        w("gss", &acc.gss);
12410        w("ya", &acc.ya);
12411        println!(
12412            "refit L{li}: {} support, {} tokens, hidden {}",
12413            acc.support.len(),
12414            acc.tokens,
12415            acc.hidden
12416        );
12417        n += 1;
12418    }
12419    n
12420}
12421
12422/// `CMF_FFN_ADUMP=<prefix>` — append every probed token's FFN activation
12423/// row to `<prefix>.<layer>.f16`. The co-activation record: which
12424/// neurons fire together, which is what a tube has to group if a token
12425/// is ever going to open one tube instead of sixteen.
12426fn adump_row(li: usize, g: &[f32]) {
12427    use std::io::Write as _;
12428    static FILES: std::sync::OnceLock<
12429        Option<(
12430            String,
12431            std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
12432        )>,
12433    > = std::sync::OnceLock::new();
12434    let Some((prefix, map)) = FILES
12435        .get_or_init(|| {
12436            std::env::var("CMF_FFN_ADUMP")
12437                .ok()
12438                .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
12439        })
12440        .as_ref()
12441    else {
12442        return;
12443    };
12444    // `CMF_FFN_ADUMP_FROM/_TO` narrow the dump to a layer span, so a big
12445    // calibration run fits on disk in a few passes instead of one.
12446    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12447    let (from, to) = *SPAN.get_or_init(|| {
12448        let g = |k: &str, d: usize| {
12449            std::env::var(k)
12450                .ok()
12451                .and_then(|v| v.parse().ok())
12452                .unwrap_or(d)
12453        };
12454        (
12455            g("CMF_FFN_ADUMP_FROM", 0),
12456            g("CMF_FFN_ADUMP_TO", usize::MAX),
12457        )
12458    });
12459    if li < from || li > to {
12460        return;
12461    }
12462    let mut map = map.lock().unwrap();
12463    let f = map.entry(li).or_insert_with(|| {
12464        std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
12465    });
12466    let mut bytes = Vec::with_capacity(g.len() * 2);
12467    for v in g {
12468        bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
12469    }
12470    let _ = f.write_all(&bytes);
12471}
12472
12473/// `CMF_FFN_ORACLE_TOPK` — keep only the k largest |silu(g)·u| of each
12474/// token and zero the rest. Not a serving mode: it is the CEILING of
12475/// contextual sparsity — what a per-token router would be chasing —
12476/// measured by cheating, since the selection reads the very activations
12477/// it would have to predict.
12478fn oracle_topk() -> usize {
12479    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12480    *K.get_or_init(|| {
12481        std::env::var("CMF_FFN_ORACLE_TOPK")
12482            .ok()
12483            .and_then(|v| v.parse().ok())
12484            .unwrap_or(0)
12485    })
12486}
12487
12488/// `CMF_FFN_GATE_TOPK` — the REALIZABLE cousin of the oracle: rank the
12489/// neurons by their gate alone (which the kernel has computed anyway
12490/// before it reads `up`), keep the k best, and drop the rest. Every
12491/// dropped neuron's `up` row and `down` column stay unread, so this is
12492/// the sparsity a serving path can actually take without a router.
12493fn gate_topk() -> usize {
12494    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12495    *K.get_or_init(|| {
12496        std::env::var("CMF_FFN_GATE_TOPK")
12497            .ok()
12498            .and_then(|v| v.parse().ok())
12499            .unwrap_or(0)
12500    })
12501}
12502
12503/// `CMF_FFN_GATE_BLOCK` — select in blocks of B neurons instead of one
12504/// by one. A scattered per-neuron choice cannot be read efficiently (a
12505/// row at a time, no prefetch runway); a block of 32 is a contiguous
12506/// 32-row slab of `up` and of the transposed `down`, which the ordinary
12507/// kernels stream. The question the measurement answers is what the
12508/// block costs in quality.
12509fn gate_block() -> usize {
12510    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12511    *B.get_or_init(|| {
12512        std::env::var("CMF_FFN_GATE_BLOCK")
12513            .ok()
12514            .and_then(|v| v.parse().ok())
12515            .unwrap_or(1)
12516    })
12517}
12518
12519/// Zero all but the `k` largest BLOCKS (by summed square) of a row.
12520fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
12521    let n = g.len();
12522    let nb = n.div_ceil(block);
12523    let kb = (keep_n.div_ceil(block)).clamp(1, nb);
12524    if kb >= nb {
12525        return;
12526    }
12527    let mut score: Vec<f32> = (0..nb)
12528        .map(|b| {
12529            g[b * block..((b + 1) * block).min(n)]
12530                .iter()
12531                .map(|v| v * v)
12532                .sum::<f32>()
12533        })
12534        .collect();
12535    let mut ord = score.clone();
12536    let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
12537        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12538    });
12539    let thr = *kth;
12540    for b in 0..nb {
12541        if score[b] < thr {
12542            g[b * block..((b + 1) * block).min(n)].fill(0.0);
12543        }
12544    }
12545    score.clear();
12546}
12547
12548/// Zero all but the `k` largest magnitudes of one token's activation row.
12549fn keep_top_k(g: &mut [f32], k: usize) {
12550    if gate_block() > 1 {
12551        return keep_top_blocks(g, k, gate_block());
12552    }
12553    let n = g.len();
12554    if k == 0 || k >= n {
12555        return;
12556    }
12557    let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12558    let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12559        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12560    });
12561    let thr = *kth;
12562    for v in g.iter_mut() {
12563        if v.abs() < thr {
12564            *v = 0.0;
12565        }
12566    }
12567}
12568
12569/// `CMF_FFN_PROBE_SQ` — accumulate Σa², so the dump divided by the token
12570/// count and square-rooted is the RMS activation trace Patent 12 weights
12571/// its matrices by.
12572fn probe_sq() -> bool {
12573    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12574    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
12575}
12576
12577/// `CMF_FFN_PROBE_SIGNED` — accumulate the SIGNED activation sum
12578/// instead of its magnitude: what a dropped neuron contributes ON
12579/// AVERAGE, which is the bias a narrowed FFN can add back for free.
12580fn probe_signed() -> bool {
12581    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12582    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
12583}
12584
12585/// `CMF_FFN_MEANFILL=<file>` — a masked-out neuron contributes its MEAN
12586/// activation instead of zero (`u32 layers, u32 inter, f32[…]`, the mass
12587/// dump layout, holding per-neuron means). Dropping a neuron outright
12588/// also drops its average contribution, which shifts the layer output by
12589/// a constant; filling the mean back is one add per layer and costs no
12590/// bytes off the bus. This is the measurement arm — in a tube file the
12591/// same correction ships as a per-task bias vector.
12592fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
12593    static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
12594    M.get_or_init(|| {
12595        let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
12596        let b = std::fs::read(&p).ok()?;
12597        let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
12598        let vals: Vec<f32> = b[8..]
12599            .chunks_exact(4)
12600            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12601            .collect();
12602        eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
12603        Some((inter, vals))
12604    })
12605    .as_ref()
12606}
12607
12608/// `CMF_FFN_PROBE_TOPK` — 0 (default) = accumulate mass, k>0 = count
12609/// how often a neuron lands in a token's top k.
12610fn probe_topk() -> usize {
12611    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12612    *K.get_or_init(|| {
12613        std::env::var("CMF_FFN_PROBE_TOPK")
12614            .ok()
12615            .and_then(|v| v.parse().ok())
12616            .unwrap_or(0)
12617    })
12618}
12619
12620thread_local! {
12621    /// DTG-MA activation probe: per-layer per-neuron Σ|silu(g)·u|
12622    /// accumulator, alive only during `Pipeline::probe_ffn_mass`.
12623    static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
12624        const { std::cell::RefCell::new(None) };
12625}
12626
12627/// Per-token structured sparsity, paid for in bytes.
12628///
12629/// The gate is the cheapest third of an FFN and it already says which
12630/// neurons matter: `silu(gate)` near zero means the neuron contributes
12631/// nothing whatever `up` says. So compute every gate, keep the `k`
12632/// loudest, and read ONLY those neurons' `up` rows and `down` rows —
12633/// the latter needs `down_proj` stored transposed, otherwise a neuron's
12634/// down weights are a strided column and "reading only those" costs a
12635/// full cache line each.
12636///
12637/// Returns `None` when the file has no transposed `down` (the caller
12638/// then runs the ordinary dense path).
12639fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
12640    // The scatter path reads individual rows/columns and cannot express the
12641    // per-matrix signed FWHT boundary.  Let the descriptor-aware dense path
12642    // handle Prism files rather than silently running an unrotated sparse
12643    // approximation.
12644    if d.gate_proj.has_prism_contract()
12645        || d.up_proj.has_prism_contract()
12646        || d.down_proj.has_prism_contract()
12647    {
12648        return None;
12649    }
12650    let dt = d.down_t.as_ref()?;
12651    let inter = d.gate_proj.rows();
12652    let hidden = dt.cols();
12653    if k == 0 || k >= inter || d.act != Act::Silu {
12654        return None;
12655    }
12656    DYN_SCRATCH.with(|sc| {
12657        let mut sc = sc.borrow_mut();
12658        let DynScratch {
12659            g,
12660            mag,
12661            live,
12662            parts,
12663        } = &mut *sc;
12664        g.resize(inter, 0.0);
12665        d.gate_proj.matvec(x, g, pool);
12666        for v in g.iter_mut() {
12667            *v = inference::silu(*v);
12668        }
12669        // The k-th largest |silu(gate)| is the threshold; ties keep more,
12670        // which is the safe side.
12671        mag.clear();
12672        mag.extend(g.iter().map(|v| v.abs()));
12673        let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12674            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12675        });
12676        let thr = *kth;
12677        live.clear();
12678        live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
12679        let mut out = vec![0.0f32; hidden];
12680        match pool {
12681            Some(p) if live.len() >= 64 => {
12682                let nw = p.n_workers() + 1;
12683                parts.clear();
12684                parts.resize(nw * hidden, 0.0);
12685                let ptr = SendMut(parts.as_mut_ptr());
12686                let n = live.len();
12687                let live_ref: &[u32] = live;
12688                let g_ref: &[f32] = g;
12689                p.run(&|w, workers| {
12690                    let chunk = n.div_ceil(workers);
12691                    let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
12692                    if s >= e {
12693                        return;
12694                    }
12695                    WORKER_SCRATCH.with(|ws| {
12696                        let mut ws = ws.borrow_mut();
12697                        let [scratch, acc] = &mut *ws;
12698                        scratch.resize(hidden.max(x.len()), 0.0);
12699                        acc.clear();
12700                        acc.resize(hidden, 0.0);
12701                        for (o, &nrm) in live_ref[s..e].iter().enumerate() {
12702                            // One neuron of runway: the next row's lines
12703                            // start moving while this one is multiplied.
12704                            if let Some(&nx) = live_ref[s..e].get(o + 1) {
12705                                d.up_proj.prefetch_row(nx as usize);
12706                                dt.prefetch_row(nx as usize);
12707                            }
12708                            let idx = nrm as usize;
12709                            let up = d.up_proj.row_dot(idx, x, scratch);
12710                            let a = g_ref[idx] * up;
12711                            if a != 0.0 {
12712                                dt.add_row_scaled(idx, a, acc, scratch);
12713                            }
12714                        }
12715                        for (j, v) in acc.iter().enumerate() {
12716                            unsafe { *ptr.at(w * hidden + j) = *v };
12717                        }
12718                    });
12719                });
12720                for w in 0..nw {
12721                    for (j, o) in out.iter_mut().enumerate() {
12722                        *o += parts[w * hidden + j];
12723                    }
12724                }
12725            }
12726            _ => {
12727                WORKER_SCRATCH.with(|ws| {
12728                    let mut ws = ws.borrow_mut();
12729                    let [scratch, _acc] = &mut *ws;
12730                    scratch.resize(hidden.max(x.len()), 0.0);
12731                    for &nrm in live.iter() {
12732                        let idx = nrm as usize;
12733                        let up = d.up_proj.row_dot(idx, x, scratch);
12734                        let a = g[idx] * up;
12735                        if a != 0.0 {
12736                            dt.add_row_scaled(idx, a, &mut out, scratch);
12737                        }
12738                    }
12739                });
12740            }
12741        }
12742        Some(out)
12743    })
12744}
12745
12746/// Caller-side scratch of the dynamic path — one allocation per thread,
12747/// not one per layer per token (that alone cost a third of the decode).
12748struct DynScratch {
12749    g: Vec<f32>,
12750    mag: Vec<f32>,
12751    live: Vec<u32>,
12752    parts: Vec<f32>,
12753}
12754
12755thread_local! {
12756    static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
12757        std::cell::RefCell::new(DynScratch {
12758            g: Vec::new(),
12759            mag: Vec::new(),
12760            live: Vec::new(),
12761            parts: Vec::new(),
12762        })
12763    };
12764    /// Pool-worker scratch: the row buffer and this worker's partial sum.
12765    static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
12766        const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
12767}
12768
12769/// `dense_ffn_cpu` with a per-visit mask landing on the activations —
12770/// the masked-inference fast path's decode arm. Full fused quant
12771/// compute, closed neurons zeroed before down: arithmetically the
12772/// pruned network, no dequant, no weight bytes touched.
12773fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
12774    let inter = d.gate_proj.rows();
12775    FFN_SCRATCH.with(|s| {
12776        let mut s = s.borrow_mut();
12777        let [g, u, ..] = &mut *s;
12778        g.resize(inter, 0.0);
12779        if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
12780            // g holds silu(gate)·up.
12781        } else {
12782            u.resize(inter, 0.0);
12783            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12784            for i in 0..inter {
12785                g[i] = d.act.combine(g[i], u[i]);
12786            }
12787        }
12788        zero_masked_cols(g, 1, inter, mask_row);
12789        let mut out = attention::take_buf(d.down_proj.rows());
12790        d.down_proj.matvec(g, &mut out, pool);
12791        out
12792    })
12793}
12794
12795/// Dense FFN as one GPU submission via the MoE block path (single
12796/// expert, weight 1.0): gate → silu·up → down chained in one command
12797/// buffer, intermediate activations device-resident. None → weights
12798/// not q8-mapped in the primary shard / over the VRAM budget / backend
12799/// refusal → honest CPU path.
12800fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
12801    if d.gate_proj.has_prism_contract()
12802        || d.up_proj.has_prism_contract()
12803        || d.down_proj.has_prism_contract()
12804    {
12805        return None;
12806    }
12807    // The GPU block hardcodes SiLU; GeLU FFNs (Gemma) stay on CPU.
12808    if d.act != Act::Silu {
12809        return None;
12810    }
12811    // Threshold: tiny FFNs are not worth a submission (q1 excepted —
12812    // see the caller's gate).
12813    if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
12814        return None;
12815    }
12816    let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
12817    let mut model_ref = None;
12818    moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
12819    let model = model_ref?;
12820    let hidden = jobs[0].down.1;
12821    let mut out = attention::take_buf(hidden);
12822    if crate::gpu::moe_block(&model, &jobs, &mut out) {
12823        Some(out)
12824    } else {
12825        let mut out = out;
12826        attention::recycle_buf(&mut out);
12827        None
12828    }
12829}
12830
12831/// q8-mapped primary-shard tensor parts for a GPU job: q8_2f carries
12832/// its column field, q8_row runs with empty col slices (the backend
12833/// skips the multiply). Shared by the MoE block and the dense-FFN
12834/// single-job path.
12835#[allow(clippy::type_complexity)]
12836#[allow(clippy::type_complexity)]
12837pub(crate) fn moe_parts(
12838    t: &QTensor,
12839) -> Option<(
12840    &std::sync::Arc<cortiq_core::CmfModel>,
12841    usize,
12842    usize,
12843    usize,
12844    &[f32],
12845    &[f32],
12846    bool,
12847    bool,
12848    bool,
12849)> {
12850    match t {
12851        QTensor::Mapped {
12852            model,
12853            idx,
12854            dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
12855            rows,
12856            cols,
12857            row_scale,
12858            col_field,
12859            ..
12860        } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
12861            model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
12862        )),
12863        // q1: tile-embedded scales — empty rs/col slices, raw xs.
12864        QTensor::Mapped {
12865            model,
12866            idx,
12867            dtype: cortiq_core::TensorDtype::Q1,
12868            rows,
12869            cols,
12870            ..
12871        } => Some((
12872            model,
12873            *idx,
12874            *rows,
12875            *cols,
12876            &[][..],
12877            &[][..],
12878            true,
12879            false,
12880            false,
12881        )),
12882        // q4_tiled: 18-byte tiles with embedded f16 scales — raw xs.
12883        QTensor::Mapped {
12884            model,
12885            idx,
12886            dtype: cortiq_core::TensorDtype::Q4Tiled,
12887            rows,
12888            cols,
12889            ..
12890        } => Some((
12891            model,
12892            *idx,
12893            *rows,
12894            *cols,
12895            &[][..],
12896            &[][..],
12897            false,
12898            true,
12899            false,
12900        )),
12901        // q4tp: same raw-xs contract, different stride and scale plane.
12902        QTensor::Mapped {
12903            model,
12904            idx,
12905            dtype: cortiq_core::TensorDtype::Q4TiledP,
12906            rows,
12907            cols,
12908            ..
12909        } => Some((
12910            model,
12911            *idx,
12912            *rows,
12913            *cols,
12914            &[][..],
12915            &[][..],
12916            false,
12917            true,
12918            false,
12919        )),
12920        // q2tp: the 2-bit expert plane of the mixed profile — q4 family
12921        // for stride bookkeeping, flagged q2 so the trio validation can
12922        // demand a q4tp down.
12923        QTensor::Mapped {
12924            model,
12925            idx,
12926            dtype: cortiq_core::TensorDtype::Q2TiledP,
12927            rows,
12928            cols,
12929            ..
12930        } => Some((
12931            model,
12932            *idx,
12933            *rows,
12934            *cols,
12935            &[][..],
12936            &[][..],
12937            false,
12938            true,
12939            true,
12940        )),
12941        _ => None,
12942    }
12943}
12944
12945/// Map a MoE onto the Metal token graph's contract: f32 router, a
12946/// shared expert (gated — Qwen — or ungated at weight 1 — DeepSeek-V3 /
12947/// HunYuan hy_v3), softmax or sigmoid scores with an optional selection
12948/// bias and routed scale, experts uniformly q4tp (or the mixed profile:
12949/// q2tp gate/up over a q4tp down). τ routers, masks, per-expert scales
12950/// and Gemma's router-input norm refuse here — those semantics stay on
12951/// the CPU path.
12952#[cfg(target_os = "macos")]
12953fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
12954    if m.router_input_norm
12955        || m.route_tau.is_some()
12956        || m.mask.is_some()
12957        || m.per_expert_scale.is_some()
12958        || m.experts.is_empty()
12959        || m.top_k == 0
12960        || m.resonance.is_some()
12961    {
12962        return None;
12963    }
12964    // The select kernel always fills the shared slot: a model without a
12965    // shared expert (LFM2-MoE) stays on the CPU path here.
12966    let (sh, sg) = match &m.shared {
12967        Some((sh, sg)) => (sh, sg.as_ref()),
12968        None => return None,
12969    };
12970    let (rf, rr, rc) = m.router.f32_parts()?;
12971    if rr != m.experts.len() || rc != hidden {
12972        return None;
12973    }
12974    let shared_gated = sg.is_some();
12975    let sf = match sg {
12976        Some(sg) => {
12977            let (sf, sr, sc) = sg.f32_parts()?;
12978            if sr * sc != hidden {
12979                return None;
12980            }
12981            sf
12982        }
12983        // Ungated: the router's first row stands in for the gate matvec
12984        // (its logit is never read — the kernel pins weight 1).
12985        None => &rf[..hidden],
12986    };
12987    if let Some(b) = &m.expert_bias {
12988        if b.len() != m.experts.len() {
12989            return None;
12990        }
12991    }
12992    let inter = m.experts[0].gate_proj.rows();
12993    // The first expert's gate decides the profile; every trio (shared
12994    // included) must agree — the jobs ladder flips ONE kernel for all.
12995    let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
12996    let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
12997        if e.act != Act::Silu
12998            || e.gate_proj.rows() != inter
12999            || e.gate_proj.cols() != hidden
13000            || e.up_proj.rows() != inter
13001            || e.up_proj.cols() != hidden
13002            || e.down_proj.rows() != hidden
13003            || e.down_proj.cols() != inter
13004        {
13005            return None;
13006        }
13007        let pick = |t: &QTensor| -> Option<usize> {
13008            if gu_q2 {
13009                t.mapped_q2tp().map(|(_, i)| i)
13010            } else {
13011                t.mapped_q4tp().map(|(_, i)| i)
13012            }
13013        };
13014        Some((
13015            pick(&e.gate_proj)?,
13016            pick(&e.up_proj)?,
13017            e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
13018        ))
13019    };
13020    let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
13021    let shared = trio(sh)?;
13022    Some(crate::gpu::GpuMoe {
13023        router: rf,
13024        sgate: sf,
13025        experts,
13026        shared,
13027        n_exp: m.experts.len(),
13028        top_k: m.top_k,
13029        inter,
13030        norm_topk: m.norm_topk_prob,
13031        route_scale: m.routed_scaling,
13032        gu_q2,
13033        sigmoid: m.router_sigmoid,
13034        bias: m.expert_bias.as_deref(),
13035        shared_gated,
13036    })
13037}
13038
13039/// Build one gate/up/down GPU job from three tensors. `moe_push_job` is the
13040/// DenseFfn-shaped caller; architectures that keep their experts in their own
13041/// structs (DeepSeek-V4) come here directly.
13042pub(crate) fn moe_push_job_parts<'a>(
13043    gate: &'a QTensor,
13044    up: &'a QTensor,
13045    down: &'a QTensor,
13046    x: &[f32],
13047    w: f32,
13048    swiglu_limit: f32,
13049    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13050    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13051) -> Option<()> {
13052    use crate::qtensor::prescale;
13053    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
13054    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
13055    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
13056    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13057        return None; // mixed-dtype trio — honest CPU path
13058    }
13059    // The 2-bit profile is gate/up q2tp over a PLAIN q4tp down; any other
13060    // 2-bit arrangement stays on the CPU.
13061    if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
13062        return None;
13063    }
13064    if !gq2 && dq2 {
13065        return None;
13066    }
13067    model_ref.get_or_insert_with(|| gm.clone());
13068    let dt = |cf: &[f32]| {
13069        if cf.is_empty() {
13070            cortiq_core::TensorDtype::Q8Row
13071        } else {
13072            cortiq_core::TensorDtype::Q8_2f
13073        }
13074    };
13075    jobs.push(crate::gpu::MoeJob {
13076        gate: (gi, gr, gc, grs),
13077        up: (ui, ur, uc, urs),
13078        down: (di, dr, dc, drs),
13079        xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
13080        xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
13081        down_col: dcf,
13082        w,
13083        q1: gq1,
13084        q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
13085        q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
13086        gu_q2: gq2,
13087        swiglu_limit,
13088    });
13089    Some(())
13090}
13091
13092/// Build one gate/up/down GPU job (see `moe_parts`).
13093fn moe_push_job<'a>(
13094    d: &'a DenseFfn,
13095    x: &[f32],
13096    w: f32,
13097    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13098    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13099) -> Option<()> {
13100    use crate::qtensor::prescale;
13101    if d.act != Act::Silu {
13102        return None; // GPU block hardcodes SiLU
13103    }
13104    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
13105    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
13106    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
13107    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13108        return None; // mixed-dtype trio — honest CPU path
13109    }
13110    if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
13111        return None;
13112    }
13113    if !gq2 && dq2 {
13114        return None;
13115    }
13116    model_ref.get_or_insert_with(|| gm.clone());
13117    let gdt = if gcf.is_empty() {
13118        cortiq_core::TensorDtype::Q8Row
13119    } else {
13120        cortiq_core::TensorDtype::Q8_2f
13121    };
13122    let udt = if ucf.is_empty() {
13123        cortiq_core::TensorDtype::Q8Row
13124    } else {
13125        cortiq_core::TensorDtype::Q8_2f
13126    };
13127    jobs.push(crate::gpu::MoeJob {
13128        gate: (gi, gr, gc, grs),
13129        up: (ui, ur, uc, urs),
13130        down: (di, dr, dc, drs),
13131        xs_gate: prescale(x, gcf, gdt).into_owned(),
13132        xs_up: prescale(x, ucf, udt).into_owned(),
13133        down_col: dcf,
13134        w,
13135        q1: gq1,
13136        q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
13137        q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
13138        gu_q2: gq2,
13139        swiglu_limit: 0.0,
13140    });
13141    Some(())
13142}
13143
13144/// Sparse dense-FFN directly on QUANTIZED weights (mask × mmap): reads
13145/// ONLY the active neurons' gate/up rows and down columns from the mmap
13146/// — no full-matrix dequant, no f32 model copy. This is what lets a
13147/// masked big model run at quantized RSS (the historical mask path
13148/// forced the whole model to f32). Semantics identical to the f32
13149/// sparse path within quant tolerance.
13150fn sparse_ffn_quant(
13151    d: &DenseFfn,
13152    x: &[f32],
13153    active: &[u16],
13154    hidden: usize,
13155    pool: Option<&Pool>,
13156) -> Vec<f32> {
13157    let n = active.len();
13158    let inter = d.gate_proj.rows();
13159    let mut act = vec![0.0f32; n];
13160    // Scratch is needed if EITHER projection is group-packed (q4/vbit);
13161    // gate/up normally share a dtype but sizing on both is robust.
13162    let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
13163    let compute = |ai: usize| -> f32 {
13164        let idx = active[ai] as usize;
13165        if idx >= inter {
13166            return 0.0; // defensive parity with the f32 sparse path
13167        }
13168        let mut s = if need_scratch {
13169            vec![0.0f32; hidden]
13170        } else {
13171            Vec::new()
13172        };
13173        let gate = d.gate_proj.row_dot(idx, x, &mut s);
13174        let up = d.up_proj.row_dot(idx, x, &mut s);
13175        d.act.combine(gate, up)
13176    };
13177    match pool {
13178        Some(p) if n >= 256 => {
13179            let ptr = SendMut(act.as_mut_ptr());
13180            p.run(&|widx, nw| {
13181                let chunk = n.div_ceil(nw);
13182                let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
13183                for ai in s..e {
13184                    unsafe { *ptr.at(ai) = compute(ai) };
13185                }
13186            });
13187        }
13188        _ => {
13189            for (ai, a) in act.iter_mut().enumerate() {
13190                *a = compute(ai);
13191            }
13192        }
13193    }
13194    // Scatter through active down columns (reads only those columns).
13195    let mut out = vec![0.0f32; hidden];
13196    for (ai, &idx) in active.iter().enumerate() {
13197        let w = act[ai];
13198        if w.abs() >= 1e-12 && (idx as usize) < inter {
13199            d.down_proj.add_col_scaled(idx as usize, w, &mut out);
13200        }
13201    }
13202    out
13203}
13204
13205/// Test-only re-export of the private sparse-quant FFN (mask × mmap gate).
13206#[doc(hidden)]
13207pub fn sparse_ffn_quant_for_test(
13208    d: &DenseFfn,
13209    x: &[f32],
13210    active: &[u16],
13211    hidden: usize,
13212) -> Vec<f32> {
13213    sparse_ffn_quant(d, x, active, hidden, None)
13214}
13215
13216/// Dequantize a DenseFfn's three matrices to f32 (transient; only the
13217/// q4/vbit-masked fallback uses it — the memory-lean path is
13218/// sparse_ffn_quant). Reuses row_f32 row-by-row.
13219fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
13220    let deq = |t: &QTensor| -> Vec<f32> {
13221        let (rows, cols) = (t.rows(), t.cols());
13222        let mut out = vec![0.0f32; rows * cols];
13223        for r in 0..rows {
13224            t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
13225        }
13226        out
13227    };
13228    (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
13229}
13230
13231/// Pointer wrapper for the worker-pool scatter (same pattern as qtensor).
13232struct SendMut(*mut f32);
13233unsafe impl Send for SendMut {}
13234unsafe impl Sync for SendMut {}
13235impl SendMut {
13236    #[inline]
13237    // Deliberate unsynchronized scatter: pool workers write disjoint indices
13238    // in parallel, so returning `&mut` from `&self` is intentional here.
13239    #[allow(clippy::mut_from_ref)]
13240    unsafe fn at(&self, i: usize) -> &mut f32 {
13241        unsafe { &mut *self.0.add(i) }
13242    }
13243}
13244
13245/// Router → (selected experts in torch.topk order, per-expert score
13246/// vector, normalizer). The final weight of expert `e` is `p[e] / wsum`.
13247///
13248/// Two regimes share this. Qwen: softmax over ALL experts, top-k of the
13249/// probabilities, optional renorm — `router_sigmoid=false`, no bias,
13250/// scale 1 → bit-identical to the historical path. LFM2-MoE /
13251/// DeepSeek-V3 `noaux_tc`: per-expert sigmoid scores, an optional
13252/// selection bias (top-k CHOICE only; weights stay unbiased), a 1e-6 renorm
13253/// floor and a routed scale.
13254pub(crate) fn moe_route(
13255    logits: &[f32],
13256    m: &MoeFfn,
13257    allowed: Option<&[bool]>,
13258) -> (Vec<usize>, Vec<f32>, f32) {
13259    let ne = logits.len();
13260    let p: Vec<f32> = if m.router_sigmoid {
13261        logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
13262    } else {
13263        let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
13264        let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
13265        let s: f32 = e.iter().sum();
13266        for v in &mut e {
13267            *v /= s;
13268        }
13269        e
13270    };
13271    // Expert restriction: the static env mask (CMF_MOE_MASK) AND the
13272    // active task mask's expert fields (spec §5) both narrow the
13273    // candidate set; selection happens over the admitted experts only.
13274    // With norm_topk the kept weights renormalize below; without it
13275    // the excluded mass is honestly dropped.
13276    let admit = |e: usize| {
13277        m.mask.as_ref().is_none_or(|mk| mk[e])
13278            && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
13279    };
13280    let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
13281    // Descending by selection score, lower index wins ties (torch.topk).
13282    match &m.expert_bias {
13283        Some(b) => idx.sort_unstable_by(|&x, &y| {
13284            (p[y] + b[y])
13285                .partial_cmp(&(p[x] + b[x]))
13286                .unwrap()
13287                .then(x.cmp(&y))
13288        }),
13289        None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
13290    }
13291    idx.truncate(m.top_k);
13292    // Adaptive τ-routing: trim the tail experts once the kept mass is
13293    // enough. wsum below renormalizes over the KEPT set, so the output
13294    // stays a proper weighted average.
13295    if let Some(tau) = m.route_tau {
13296        let total: f32 = idx.iter().map(|&e| p[e]).sum();
13297        if total > 0.0 {
13298            let mut acc = 0.0f32;
13299            let mut keep = idx.len();
13300            for (i, &e) in idx.iter().enumerate() {
13301                acc += p[e];
13302                if acc >= tau * total {
13303                    keep = i + 1;
13304                    break;
13305                }
13306            }
13307            idx.truncate(keep);
13308        }
13309    }
13310    let wsum: f32 = if m.norm_topk_prob {
13311        let s: f32 = idx.iter().map(|&e| p[e]).sum();
13312        // LFM2 floors the denom (matches HF `+ 1e-6`); the softmax path's
13313        // probs already sum near 1, so it stays exactly as before.
13314        (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
13315    } else {
13316        1.0 / m.routed_scaling
13317    };
13318    (idx, p, wsum)
13319}
13320
13321/// See the call site: one `layer:e1,e2,…` line per routed token.
13322fn moe_trace(idx: &[usize]) {
13323    moe_trace_at(crate::gpu::cur_layer() as i32, idx)
13324}
13325
13326/// The same, for callers that know their layer (DSV4 owns its layers and
13327/// never sets the pipeline's current-layer marker).
13328pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
13329    use std::io::Write;
13330    static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
13331        std::sync::OnceLock::new();
13332    let Some(f) = F.get_or_init(|| {
13333        let p = std::env::var("CMF_MOE_TRACE").ok()?;
13334        Some(std::sync::Mutex::new(
13335            std::fs::OpenOptions::new()
13336                .create(true)
13337                .append(true)
13338                .open(p)
13339                .ok()?,
13340        ))
13341    }) else {
13342        return;
13343    };
13344    let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
13345    let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
13346}
13347
13348/// MoE FFN: router → top-k experts (see `moe_route`). Only selected
13349/// experts' pages are touched in mmap.
13350pub(crate) fn moe_ffn(
13351    m: &MoeFfn,
13352    x: &[f32],
13353    pool: Option<&Pool>,
13354    allowed: Option<&[bool]>,
13355) -> Vec<f32> {
13356    accumulate_act(m, x, 1);
13357    let ne = m.experts.len();
13358    let mut logits = vec![0.0f32; ne];
13359    match &m.resonance {
13360        Some(r) => r.scores(x, &mut logits),
13361        None => m.router.matvec(x, &mut logits, pool),
13362    }
13363    let (idx, p, wsum) = moe_route(&logits, m, allowed);
13364    {
13365        let mut st = m.stats.borrow_mut();
13366        if st.len() < ne {
13367            st.resize(ne, 0);
13368        }
13369        for &e in &idx {
13370            st[e] += 1;
13371        }
13372    }
13373    // `CMF_MOE_TRACE=<file>`: append one line per (layer, token) with the
13374    // selected expert ids. The cumulative `stats` above answer "which
13375    // experts are popular"; a residency design needs the question they
13376    // cannot answer — whether CONSECUTIVE tokens reuse experts (the
13377    // temporal locality an LRU cache lives on, FreeToken §4).
13378    moe_trace(&idx);
13379    // D5: the whole layer MoE block in one GPU command buffer (experts — the
13380    // same mmap via a no-copy buffer; intermediate activations on the GPU).
13381    // Same Ffn probe class as the dense chain: one submit per layer
13382    // either wins on this driver stack or it doesn't.
13383    if crate::gpu::enabled_here() {
13384        match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
13385            crate::gpu::ProbeArm::Gpu => {
13386                let t0 = std::time::Instant::now();
13387                if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
13388                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
13389                    return out;
13390                }
13391            }
13392            crate::gpu::ProbeArm::CpuTimed => {
13393                let t0 = std::time::Instant::now();
13394                let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13395                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
13396                return out;
13397            }
13398            crate::gpu::ProbeArm::Cpu => {
13399                return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13400            }
13401        }
13402    }
13403    moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
13404}
13405
13406/// One-shot report of whether the whole-token wgpu graph actually formed.
13407/// A refusal silently reverts to the per-op path, which is how a model can
13408/// look "GPU-accelerated" while every layer walks the host.  A device prefix
13409/// is tracked separately because it still pays a host boundary for the tail.
13410fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
13411    use std::sync::atomic::{AtomicBool, Ordering};
13412    if built {
13413        GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
13414        if total_layers > 0 && layers_run < total_layers {
13415            GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
13416        } else {
13417            GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
13418        }
13419    } else {
13420        GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
13421    }
13422    static SAID: AtomicBool = AtomicBool::new(false);
13423    if !SAID.swap(true, Ordering::Relaxed) {
13424        if built {
13425            tracing::info!("wgpu whole-token graph: ACTIVE");
13426        } else {
13427            tracing::warn!("wgpu whole-token graph refused — per-op path");
13428        }
13429    }
13430}
13431
13432/// Whole-token graph outcomes, process-wide: a benchmark that claims a
13433/// GPU number while MISS climbs is measuring the CPU — the honest-bench
13434/// contract makes that an error, not a footnote.
13435pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13436pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13437/// Graph calls that returned a hidden after running only a leading device
13438/// prefix.  These are valid hybrid executions but must not be reported as a
13439/// full GPU graph in benchmark evidence.
13440pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13441/// Graph calls that covered the complete requested layer span.
13442pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13443
13444/// Native Metal TokenGraph completion counters. These are incremented only
13445/// after checked command-buffer completion and successful readback, so a
13446/// fused-head NLL report can prove the route rather than infer it from env.
13447pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
13448    std::sync::atomic::AtomicU64::new(0);
13449pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
13450    std::sync::atomic::AtomicU64::new(0);
13451pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
13452    std::sync::atomic::AtomicU64::new(0);
13453pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
13454    std::sync::atomic::AtomicU64::new(0);
13455pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
13456    std::sync::atomic::AtomicU64::new(0);
13457/// Ordinary native-Metal rows-prefill admissions and completed rows.  These
13458/// counters are separate from TokenGraph token/head counts so a batch NLL
13459/// receipt cannot accidentally claim serial execution as batched.
13460pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
13461    std::sync::atomic::AtomicU64::new(0);
13462pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
13463    std::sync::atomic::AtomicU64::new(0);
13464pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
13465    std::sync::atomic::AtomicU64::new(0);
13466pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
13467    std::sync::atomic::AtomicU64::new(0);
13468
13469/// `CMF_MOE_BATCH=0` restores the per-expert serial loop — the A/B lever
13470/// for the batched kernel, and how its bit-identity is checked.
13471fn moe_batch_enabled() -> bool {
13472    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13473    *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
13474}
13475
13476/// Two-dispatch CPU MoE: every routed expert (and the shared one) fused
13477/// into one gate/up/SiLU dispatch and one down dispatch, instead of two
13478/// pool barriers per expert. Bit-identical to the serial loop below —
13479/// see `moe_gate_up_many` / `moe_down_many`. `None` = the batched kernel
13480/// does not cover this layer, walk the serial path.
13481fn moe_ffn_cpu_batched(
13482    m: &MoeFfn,
13483    x: &[f32],
13484    idx: &[usize],
13485    p: &[f32],
13486    wsum: f32,
13487    pool: Option<&Pool>,
13488) -> Option<Vec<f32>> {
13489    if idx.is_empty() || !moe_batch_enabled() {
13490        return None;
13491    }
13492    // The bake probe reads per-neuron activation mass out of the
13493    // single-expert path; batching would skip it. Rare and offline —
13494    // hand those runs to the serial loop.
13495    if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
13496        return None;
13497    }
13498    let n = idx.len() + usize::from(m.shared.is_some());
13499    let mut pairs = Vec::with_capacity(n);
13500    let mut downs = Vec::with_capacity(n);
13501    let mut ws = Vec::with_capacity(n);
13502    for &e in idx {
13503        let d = &m.experts[e];
13504        if d.act != Act::Silu {
13505            return None;
13506        }
13507        pairs.push((&d.gate_proj, &d.up_proj));
13508        downs.push(&d.down_proj);
13509        ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
13510    }
13511    // The shared expert goes last, matching the serial loop's order —
13512    // the f32 accumulation order is part of the bit-identity claim.
13513    if let Some((se, gate)) = &m.shared {
13514        if se.act != Act::Silu {
13515            return None;
13516        }
13517        let g = gate.as_ref().map_or(1.0, |gate| {
13518            let mut gl = [0.0f32; 1];
13519            gate.matvec(x, &mut gl, pool);
13520            1.0 / (1.0 + (-gl[0]).exp())
13521        });
13522        pairs.push((&se.gate_proj, &se.up_proj));
13523        downs.push(&se.down_proj);
13524        ws.push(g);
13525    }
13526    let inter = pairs[0].0.rows();
13527    let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
13528    if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
13529        return None;
13530    }
13531    let mut out = attention::take_buf(x.len());
13532    if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
13533        attention::recycle_buf(&mut out);
13534        return None;
13535    }
13536    Some(out)
13537}
13538
13539/// Exact CPU completion for the routed experts a dynamic device cache did
13540/// not contain. The weights are already the router's final normalized mix.
13541/// Keeping this independent of `MoeFfn` makes the job `Sync`: its routing
13542/// statistics live in a `RefCell`, while the immutable expert tensors can be
13543/// evaluated safely in parallel with the GPU's resident subset.
13544pub(crate) fn moe_cold_experts_cpu(
13545    experts: &[(&DenseFfn, f32)],
13546    x: &[f32],
13547    pool: Option<&Pool>,
13548) -> Vec<f32> {
13549    let mut out = attention::take_buf(x.len());
13550    if experts.is_empty() {
13551        return out;
13552    }
13553    let pairs: Vec<_> = experts
13554        .iter()
13555        .map(|(e, _)| (&e.gate_proj, &e.up_proj))
13556        .collect();
13557    let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
13558    let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
13559    let inter = experts[0].0.gate_proj.rows();
13560    let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
13561    if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
13562        && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
13563    {
13564        return out;
13565    }
13566    out.fill(0.0);
13567    for &(expert, weight) in experts {
13568        let mut one = dense_ffn(expert, x, pool);
13569        for (o, v) in out.iter_mut().zip(&one) {
13570            *o += weight * v;
13571        }
13572        attention::recycle_buf(&mut one);
13573    }
13574    out
13575}
13576
13577/// The pure-CPU MoE expert loop (also the fallback of every GPU refusal).
13578fn moe_ffn_cpu(
13579    m: &MoeFfn,
13580    x: &[f32],
13581    idx: &[usize],
13582    p: &[f32],
13583    wsum: f32,
13584    pool: Option<&Pool>,
13585) -> Vec<f32> {
13586    if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
13587        return out;
13588    }
13589    let mut out = attention::take_buf(x.len());
13590    for &e in idx {
13591        let mut eo = dense_ffn(&m.experts[e], x, pool);
13592        let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
13593        for i in 0..out.len() {
13594            out[i] += w * eo[i];
13595        }
13596        attention::recycle_buf(&mut eo);
13597    }
13598    if let Some((se, gate)) = &m.shared {
13599        let mut so = dense_ffn(se, x, pool);
13600        let g = gate.as_ref().map_or(1.0, |gate| {
13601            let mut gl = [0.0f32; 1];
13602            gate.matvec(x, &mut gl, pool);
13603            1.0 / (1.0 + (-gl[0]).exp())
13604        });
13605        for i in 0..out.len() {
13606            out[i] += g * so[i];
13607        }
13608        attention::recycle_buf(&mut so);
13609    }
13610    out
13611}
13612
13613/// DeepSeek-V2 MLA forward, expand-to-MHA form (see `AttnKind::Mla`):
13614/// per token the latent expands to every head's K/V and the ordinary
13615/// cache + grouped attend do the rest. K head layout is [rope | nope]
13616/// (rotary_dim = qk_rope rotates the shared rope key and each q head's
13617/// prefix); V rows are zero-padded to the K head_dim inside the cache
13618/// and the pad is sliced off before O. Attention importance is not
13619/// accumulated for MLA yet (no eviction interplay).
13620#[allow(clippy::too_many_arguments)]
13621fn mla_attention(
13622    w: &MlaWeights,
13623    normed: &[f32],
13624    cache: &mut crate::kv_cache::LayerKvCache,
13625    position: usize,
13626    inv_freq: &[f32],
13627    rope_scale: f32,
13628    eps: f64,
13629    pool: Option<&Pool>,
13630) -> Vec<f32> {
13631    let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
13632    let hd = dr + dn;
13633    let mut q = vec![0.0f32; nh * hd];
13634    match (&w.q_a, &w.q_a_norm) {
13635        (Some(qa), Some(qn)) => {
13636            let mut t = vec![0.0f32; qa.rows()];
13637            qa.matvec(normed, &mut t, pool);
13638            let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
13639            w.q_proj.matvec(&tn, &mut q, pool);
13640        }
13641        _ => w.q_proj.matvec(normed, &mut q, pool),
13642    }
13643    let mut ca = vec![0.0f32; lora + dr];
13644    w.kv_a.matvec(normed, &mut ca, pool);
13645    let (c_lat, k_rope) = ca.split_at_mut(lora);
13646    let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
13647    let mut kvb = vec![0.0f32; nh * (dn + dv)];
13648    w.kv_b.matvec(&latn, &mut kvb, pool);
13649    if !w.nope {
13650        attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
13651    }
13652    for h in 0..nh {
13653        if !w.nope {
13654            attention::rope_rotate_scaled(
13655                &mut q[h * hd..h * hd + dr],
13656                position,
13657                inv_freq,
13658                rope_scale,
13659            );
13660        }
13661    }
13662    let mut k = vec![0.0f32; nh * hd];
13663    let mut v = vec![0.0f32; nh * hd];
13664    for h in 0..nh {
13665        k[h * hd..h * hd + dr].copy_from_slice(k_rope);
13666        k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
13667        v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
13668    }
13669    cache.append(&k, &v, &vec![true; nh]);
13670    let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
13671    attention::recycle_buf(&mut imp);
13672    let mut ov = vec![0.0f32; nh * dv];
13673    for h in 0..nh {
13674        ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
13675    }
13676    let mut out = vec![0.0f32; w.o_proj.rows()];
13677    w.o_proj.matvec(&ov, &mut out, pool);
13678    out
13679}
13680
13681/// Gemma-4 dual-branch FFN (spec: see `FfnKind::DenseMoe`). The dense
13682/// branch reads the pre-FFN-normed activation; the router and the
13683/// expert branch read the RAW residual — the router through a
13684/// scale-less rms norm (its constant gain is folded into the weights),
13685/// the experts through `pre_norm_2`. CPU path; GPU graphs refuse the
13686/// layer kind honestly.
13687fn dense_moe_ffn(
13688    dm: &DenseMoeFfn,
13689    x_normed: &[f32],
13690    h_raw: &[f32],
13691    eps: f64,
13692    norm_style: NormStyle,
13693    pool: Option<&Pool>,
13694) -> Vec<f32> {
13695    let mut d = dense_ffn(&dm.dense, x_normed, pool);
13696    d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
13697    let m = &dm.moe;
13698    let ne = m.experts.len();
13699    let mut logits = vec![0.0f32; ne];
13700    if m.router_input_norm {
13701        let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
13702        let inv = 1.0 / (ss + eps as f32).sqrt();
13703        let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
13704        m.router.matvec(&xr, &mut logits, pool);
13705    } else {
13706        m.router.matvec(h_raw, &mut logits, pool);
13707    }
13708    let (idx, p, wsum) = moe_route(&logits, m, None);
13709    {
13710        let mut st = m.stats.borrow_mut();
13711        if st.len() < ne {
13712            st.resize(ne, 0);
13713        }
13714        for &e in &idx {
13715            st[e] += 1;
13716        }
13717    }
13718    let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
13719    let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
13720    let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
13721    for (di, mi) in d.iter_mut().zip(&mo) {
13722        *di += mi;
13723    }
13724    d
13725}
13726
13727/// Building the MoE-layer GPU jobs: all selected experts (+shared) must
13728/// be q8_2f-Mapped from the primary mapping; otherwise None → CPU path.
13729/// One-shot report of why the MoE GPU block refused. A silent `?` here
13730/// sends every expert to the CPU with nothing in the logs to say so —
13731/// which is exactly how a q4tp MoE model looked "GPU-accelerated" while
13732/// running entirely on the host.
13733fn moe_gpu_refused(why: &'static str) {
13734    use std::sync::atomic::{AtomicBool, Ordering};
13735    static SAID: AtomicBool = AtomicBool::new(false);
13736    if !SAID.swap(true, Ordering::Relaxed) {
13737        tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
13738    }
13739}
13740
13741fn moe_ffn_gpu(
13742    m: &MoeFfn,
13743    x: &[f32],
13744    idx: &[usize],
13745    p: &[f32],
13746    wsum: f32,
13747    pool: Option<&Pool>,
13748) -> Option<Vec<f32>> {
13749    use crate::gpu::MoeJob;
13750
13751    let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
13752    let mut model_ref = None;
13753    for &e in idx {
13754        if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
13755            moe_gpu_refused("push_job(expert)");
13756            return None;
13757        }
13758    }
13759    if let Some((se, gate)) = &m.shared {
13760        let g = gate.as_ref().map_or(1.0, |gate| {
13761            let mut gl = [0.0f32; 1];
13762            gate.matvec(x, &mut gl, pool);
13763            1.0 / (1.0 + (-gl[0]).exp())
13764        });
13765        if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
13766            moe_gpu_refused("push_job(shared)");
13767            return None;
13768        }
13769    }
13770    let Some(model) = model_ref else {
13771        moe_gpu_refused("no model_ref");
13772        return None;
13773    };
13774    let hidden = jobs[0].down.1;
13775    let mut out = vec![0.0f32; hidden];
13776    if crate::gpu::moe_block(&model, &jobs, &mut out) {
13777        Some(out)
13778    } else {
13779        moe_gpu_refused("gpu::moe_block");
13780        None
13781    }
13782}
13783
13784/// Single-position FFN dispatch.
13785fn ffn_forward(
13786    ffn: &FfnKind,
13787    x: &[f32],
13788    pool: Option<&Pool>,
13789    experts_allowed: Option<&[bool]>,
13790) -> Vec<f32> {
13791    match ffn {
13792        FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
13793        FfnKind::Dense(d) => dense_ffn(d, x, pool),
13794        FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
13795        // Dual-branch layers need the raw residual — their callers
13796        // dispatch dense_moe_ffn directly; the auxiliary paths that land
13797        // here (MTP draft, o1 replay) do not co-occur with gemma-4 MoE.
13798        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13799    }
13800}
13801
13802/// Fused two-position FFN: gate/up/down streamed once (dense). MoE
13803/// falls back to two singles — expert sets differ per position, there
13804/// is nothing to fuse.
13805fn ffn_forward_pair(
13806    ffn: &FfnKind,
13807    x1: &[f32],
13808    x2: &[f32],
13809    pool: Option<&Pool>,
13810    experts_allowed: Option<&[bool]>,
13811) -> (Vec<f32>, Vec<f32>) {
13812    let d = match ffn {
13813        // A tube layer has nothing to fuse across the pair — the tubes
13814        // are separate matrices; two singles are the honest path.
13815        FfnKind::Dense(d) if !d.segs.is_empty() => {
13816            return (
13817                tube_ffn(d, x1, 1, pool, None),
13818                tube_ffn(d, x2, 1, pool, None),
13819            );
13820        }
13821        FfnKind::Dense(d) => d,
13822        FfnKind::Moe(m) => {
13823            return (
13824                moe_ffn(m, x1, pool, experts_allowed),
13825                moe_ffn(m, x2, pool, experts_allowed),
13826            );
13827        }
13828        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13829    };
13830    let inter = d.gate_proj.rows();
13831    FFN_SCRATCH.with(|s| {
13832        let mut s = s.borrow_mut();
13833        let [g1, g2, u1, u2] = &mut *s;
13834        g1.resize(inter, 0.0);
13835        g2.resize(inter, 0.0);
13836        u1.resize(inter, 0.0);
13837        u2.resize(inter, 0.0);
13838        // Multi-matrix pair job: gate+up under one pool dispatch
13839        // (o1s = lane-1 outputs across tensors, o2s = lane-2).
13840        QTensor::matvec2_many(
13841            [&d.gate_proj, &d.up_proj],
13842            x1,
13843            x2,
13844            [g1.as_mut_slice(), u1.as_mut_slice()],
13845            [g2.as_mut_slice(), u2.as_mut_slice()],
13846            pool,
13847        );
13848        for i in 0..inter {
13849            g1[i] = d.act.combine(g1[i], u1[i]);
13850            g2[i] = d.act.combine(g2[i], u2[i]);
13851        }
13852        let mut o1 = attention::take_buf(d.down_proj.rows());
13853        let mut o2 = attention::take_buf(d.down_proj.rows());
13854        d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
13855        (o1, o2)
13856    })
13857}
13858
13859#[cfg(test)]
13860mod tests {
13861
13862    #[test]
13863    fn nll_graph_policy_scopes_only_the_fused_head() {
13864        for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
13865            // A Vulkan/Wgpu hidden-only graph remains the quality route.
13866            ("vulkan graph", true, true, false, true, false),
13867            // Native Metal adds the strict fused graph-head contract.
13868            ("native Metal graph", true, true, true, true, true),
13869            // Masked NLL and the explicit non-graph fallback remain unchanged.
13870            ("masked", false, true, false, false, false),
13871            ("graph disabled", true, false, true, false, false),
13872        ] {
13873            let (graph_quality, graph_head_required) =
13874                super::nll_graph_policy(unmasked, prefer_graph, native_metal);
13875            assert_eq!(graph_quality, want_graph, "{label}: graph quality");
13876            assert_eq!(graph_head_required, want_head, "{label}: fused head");
13877        }
13878    }
13879
13880    #[test]
13881    fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
13882        assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
13883        assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
13884        assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
13885        assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
13886        assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
13887    }
13888
13889    #[test]
13890    fn cancel_flag_stops_generation() {
13891        let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
13892        // Set before the call: the prefill loops honour it, the run
13893        // returns immediately with the cancelled reason and no tokens.
13894        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13895        let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
13896        assert_eq!(r.finish_reason, "cancelled");
13897        assert!(
13898            r.token_ids.is_empty(),
13899            "no tokens after cancel: {:?}",
13900            r.token_ids
13901        );
13902        assert_eq!(p.kv_cache.seq_len(), 0);
13903        assert!(p.kv_history.is_empty());
13904        assert!(!p.graph_want_logits);
13905        assert!(p.graph_logits.is_none());
13906        // Flag auto-cleared: the next call generates normally.
13907        let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
13908        assert_ne!(r2.finish_reason, "cancelled");
13909    }
13910    use super::*;
13911
13912    /// sparse_ffn_quant must equal a dense FFN where inactive neurons are
13913    /// zeroed (mask × mmap correctness). On F32 tensors this is EXACT —
13914    /// it validates the row_dot / add_col_scaled / scatter indexing, the
13915    /// bug-prone part. The q8 branches reuse the golden-tested linear
13916    /// The per-token sparse path reads a transposed `down`; it must
13917    /// agree with the arm that computes everything and zeroes the
13918    /// losers, or the speed measurement is measuring a different model.
13919    #[test]
13920    fn dynamic_ffn_equals_the_zeroing_arm() {
13921        let (hidden, inter) = (8usize, 32usize);
13922        let synth = |n: usize, salt: usize| -> Vec<f32> {
13923            (0..n)
13924                .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
13925                .collect()
13926        };
13927        let down = synth(hidden * inter, 3);
13928        let mut down_t = vec![0.0f32; inter * hidden];
13929        for r in 0..hidden {
13930            for c in 0..inter {
13931                down_t[c * hidden + r] = down[r * inter + c];
13932            }
13933        }
13934        let d = DenseFfn {
13935            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
13936            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
13937            down_proj: QTensor::from_f32(down.clone(), hidden, inter),
13938            act: Act::Silu,
13939            down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
13940            segs: Vec::new(),
13941        };
13942        let x = synth(hidden, 11);
13943        let k = 12usize;
13944        let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
13945        // Reference: full compute, keep the k loudest |silu(gate)|.
13946        let mut g = vec![0.0f32; inter];
13947        d.gate_proj.matvec(&x, &mut g, None);
13948        let mut u = vec![0.0f32; inter];
13949        d.up_proj.matvec(&x, &mut u, None);
13950        for v in g.iter_mut() {
13951            *v = inference::silu(*v);
13952        }
13953        keep_top_k(&mut g, k);
13954        for i in 0..inter {
13955            g[i] *= u[i];
13956        }
13957        let mut want = vec![0.0f32; hidden];
13958        d.down_proj.matvec(&g, &mut want, None);
13959        for (a, b) in want.iter().zip(&got) {
13960            assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
13961        }
13962    }
13963
13964    /// A tube layer is the same layer, re-cut. With every tube open the
13965    /// answer must equal the dense FFN over the concatenated neurons
13966    /// (the permutation is an identity on the layer's function); with a
13967    /// tube closed it must equal the dense FFN with those neurons
13968    /// zeroed — the mask semantics, now paid for in bytes not read.
13969    #[test]
13970    fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
13971        let (hidden, core, tube) = (8usize, 12usize, 8usize);
13972        let inter = core + tube;
13973        let synth = |n: usize, salt: usize| -> Vec<f32> {
13974            (0..n)
13975                .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
13976                .collect()
13977        };
13978        let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
13979        let d_all = synth(hidden * inter, 3);
13980        // The dense layer, and the same weights cut into core + tube.
13981        let dense = DenseFfn {
13982            gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
13983            up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
13984            down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
13985            act: Act::Silu,
13986            down_t: None,
13987            segs: Vec::new(),
13988        };
13989        let rows =
13990            |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
13991        let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
13992            let mut o = Vec::with_capacity(hidden * (b - a));
13993            for r in 0..hidden {
13994                o.extend_from_slice(&v[r * inter + a..r * inter + b]);
13995            }
13996            o
13997        };
13998        let tubed = DenseFfn {
13999            down_t: None,
14000            gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
14001            up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
14002            down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
14003            act: Act::Silu,
14004            segs: vec![FfnSeg {
14005                gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
14006                up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
14007                down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
14008                start: core,
14009                width: tube,
14010            }],
14011        };
14012        let x = synth(hidden, 7);
14013        let want = dense_ffn(&dense, &x, None);
14014        let got = tube_ffn(&tubed, &x, 1, None, None);
14015        for (a, b) in want.iter().zip(&got) {
14016            assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
14017        }
14018        // Closed tube: bits on for the core, off for the tube.
14019        let mut bits = vec![0u8; inter.div_ceil(8)];
14020        for n in 0..core {
14021            bits[n / 8] |= 1 << (n % 8);
14022        }
14023        let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
14024        let masked = dense_ffn_masked(&dense, &x, None, &bits);
14025        for (a, b) in masked.iter().zip(&closed) {
14026            assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
14027        }
14028        // The batched arm must agree with the single-position one.
14029        let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
14030        for (a, b) in closed.iter().zip(&batch) {
14031            assert_eq!(a, b, "batch arm disagrees with decode arm");
14032        }
14033    }
14034
14035    /// scale, structurally identical to the matvec kernels.
14036    #[test]
14037    fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
14038        let (hidden, inter) = (16usize, 40usize);
14039        let synth = |n: usize, salt: usize| -> Vec<f32> {
14040            (0..n)
14041                .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
14042                .collect()
14043        };
14044        let d = DenseFfn {
14045            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
14046            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
14047            down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
14048            act: Act::Silu,
14049            down_t: None,
14050            segs: Vec::new(),
14051        };
14052        let x = synth(hidden, 9);
14053        // Active = every 3rd neuron.
14054        let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
14055
14056        let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
14057
14058        // Reference: full dense FFN but g[i]=0 for inactive neurons.
14059        let mut g = vec![0.0f32; inter];
14060        d.gate_proj.matvec(&x, &mut g, None);
14061        let mut u = vec![0.0f32; inter];
14062        d.up_proj.matvec(&x, &mut u, None);
14063        let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
14064        for i in 0..inter {
14065            g[i] = if act_set.contains(&(i as u16)) {
14066                inference::silu(g[i]) * u[i]
14067            } else {
14068                0.0
14069            };
14070        }
14071        let mut reference = vec![0.0f32; hidden];
14072        d.down_proj.matvec(&g, &mut reference, None);
14073
14074        let max_d = sparse
14075            .iter()
14076            .zip(&reference)
14077            .map(|(a, b)| (a - b).abs())
14078            .fold(0.0f32, f32::max);
14079        assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
14080    }
14081
14082    /// Attach a synthetic MTP head (same structure as a main layer).
14083    fn attach_test_mtp(p: &mut Pipeline) {
14084        let (h, inter, heads, kv, hd) = (
14085            p.hidden_size,
14086            p.intermediate_size,
14087            p.num_heads,
14088            p.num_kv_heads,
14089            p.head_dim,
14090        );
14091        let synth = |n: usize, salt: usize| -> Vec<f32> {
14092            (0..n)
14093                .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
14094                .collect()
14095        };
14096        let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
14097            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14098        };
14099        p.mtp = Some(MtpModule {
14100            enorm: vec![1.0; h],
14101            hnorm: vec![1.0; h],
14102            eh_proj: qt(h, 2 * h, 301),
14103            layer: LayerWeights {
14104                input_norm: vec![1.0; h],
14105                post_norm: vec![1.0; h],
14106                attn_out_norm: None,
14107                ffn_out_norm: None,
14108                layer_scale: None,
14109                ffn: FfnKind::Dense(DenseFfn {
14110                    gate_proj: qt(inter, h, 315),
14111                    up_proj: qt(inter, h, 316),
14112                    down_proj: qt(h, inter, 317),
14113                    act: Act::Silu,
14114                    down_t: None,
14115                    segs: Vec::new(),
14116                }),
14117                attn: AttnKind::Full {
14118                    bias: None,
14119                    wq: qt(heads * hd, h, 311),
14120                    wk: qt(kv * hd, h, 312),
14121                    wv: qt(kv * hd, h, 313),
14122                    wo: qt(h, heads * hd, 314),
14123                    q_norm: None,
14124                    k_norm: None,
14125                    output_gate: false,
14126                    softplus_gate: None,
14127                },
14128            },
14129            final_norm: vec![1.0; h],
14130            kv: crate::kv_cache::LayerKvCache::new(kv, hd),
14131        });
14132    }
14133
14134    #[test]
14135    fn speculative_equals_vanilla_greedy() {
14136        // Speculative decode and the wgpu token graph are mutually
14137        // exclusive; a leaked CMF_GPU=wgpu from a parallel gpu test
14138        // would silently disable drafting. Pin the graph off.
14139        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14140        let run = |spec: bool| {
14141            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14142            p.sampler_config.temperature = 0.0;
14143            attach_test_mtp(&mut p);
14144            p.speculative = spec;
14145            let r = p.generate("abcdef", 12, None, None).unwrap();
14146            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
14147        };
14148        let (vanilla, d0, _) = run(false);
14149        let (spec, d1, a1) = run(true);
14150        assert_eq!(d0, 0, "vanilla path must not draft");
14151        assert!(d1 > 0, "speculative path must draft");
14152        assert_eq!(
14153            vanilla, spec,
14154            "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
14155        );
14156    }
14157
14158    #[test]
14159    fn speculative_accepts_constant_oracle() {
14160        // See speculative_equals_vanilla_greedy: pin the wgpu graph off.
14161        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14162        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14163        p.sampler_config.temperature = 0.0;
14164        p.sampler_config.repetition_penalty = 1.0;
14165        // Constant lm_head → every logit equal → both the main model and
14166        // the draft head argmax to token 0: acceptance must be 100%.
14167        p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
14168        attach_test_mtp(&mut p);
14169        p.speculative = true;
14170        let r = p.generate("abcd", 10, None, None).unwrap();
14171        assert!(r.mtp_drafted > 0);
14172        assert_eq!(
14173            r.mtp_accepted, r.mtp_drafted,
14174            "constant logits → every draft accepted"
14175        );
14176        // Ties resolve to the same token in both the main and draft
14177        // heads — the sequence is one repeated token.
14178        assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
14179    }
14180
14181    #[test]
14182    fn empty_prompt_is_an_error_not_a_panic() {
14183        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14184        let r = p.generate("", 4, None, None);
14185        assert!(r.is_err(), "empty prompt must be a clean error");
14186    }
14187
14188    #[test]
14189    fn every_token_enters_kv_exactly_once() {
14190        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14191        // Greedy so no RNG variance; byte tokenizer → 3 prompt tokens.
14192        p.sampler_config.temperature = 0.0;
14193        let r = p.generate("abc", 2, None, None).unwrap();
14194        assert_eq!(r.prompt_tokens, 3);
14195        // prompt(3) + first sampled token forwarded before second logits:
14196        // step0 samples from prefill hidden (no extra forward), then
14197        // forwards t1 → cache 4; step1 samples, loop ends (max_tokens).
14198        assert_eq!(
14199            p.kv_cache.seq_len(),
14200            3 + r.tokens_generated - 1,
14201            "each token must be cached exactly once (v1 cached the last prompt token twice)"
14202        );
14203    }
14204
14205    #[test]
14206    fn generation_is_reproducible_with_seed() {
14207        let run = || {
14208            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14209            p.generate("hello", 8, None, None).unwrap().token_ids
14210        };
14211        assert_eq!(run(), run());
14212    }
14213
14214    #[test]
14215    fn resetting_sampler_restarts_the_seeded_stream() {
14216        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14217        let config = SamplerConfig {
14218            seed: Some(1234),
14219            ..SamplerConfig::default()
14220        };
14221        p.set_sampler_config(config.clone());
14222        let first = p.generate("hello", 8, None, None).unwrap().token_ids;
14223        p.set_sampler_config(config);
14224        let second = p.generate("hello", 8, None, None).unwrap().token_ids;
14225        assert_eq!(first, second);
14226    }
14227
14228    #[test]
14229    fn eviction_bounds_the_cache() {
14230        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14231        p.kv_cache.max_seq_len = 6;
14232        p.sampler_config.temperature = 0.0;
14233        let _ = p.generate("abcd", 12, None, None).unwrap();
14234        assert!(
14235            p.kv_cache.seq_len() <= 6 + 1,
14236            "cache must stay bounded by max_seq_len (got {})",
14237            p.kv_cache.seq_len()
14238        );
14239    }
14240
14241    #[test]
14242    fn confidence_matches_tokens_and_is_a_probability() {
14243        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14244        p.sampler_config.temperature = 0.0;
14245        p.sampler_config.repetition_penalty = 1.0;
14246        let r = p.generate("abcd", 10, None, None).unwrap();
14247        assert_eq!(
14248            r.token_confidence.len(),
14249            r.token_ids.len(),
14250            "one confidence per emitted token"
14251        );
14252        for &c in &r.token_confidence {
14253            assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
14254        }
14255        // top1_prob is a valid softmax probability.
14256        let logits = [1.0f32, 3.0, 0.5, 3.0];
14257        let p0 = top1_prob_t(&logits, 1, 1.0);
14258        let p1 = top1_prob_t(&logits, 3, 1.0);
14259        assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
14260        assert!(p0 > 0.0 && p0 < 1.0);
14261        // Calibration temperature > 1 softens an over-confident peak.
14262        let sharp = top1_prob_t(&logits, 1, 1.0);
14263        let soft = top1_prob_t(&logits, 1, 2.0);
14264        assert!(soft < sharp, "higher temperature lowers peak confidence");
14265    }
14266
14267    #[test]
14268    fn trace_is_opt_in_and_parallels_the_output() {
14269        // Off by default: the runtime is silent unless observation asked.
14270        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14271        p.sampler_config.temperature = 0.0;
14272        p.sampler_config.repetition_penalty = 1.0;
14273        let r = p.generate("abcd", 10, None, None).unwrap();
14274        assert!(r.traces.is_empty(), "trace must be empty unless enabled");
14275
14276        // On: exactly one row per emitted token, aligned with the output.
14277        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14278        p.sampler_config.temperature = 0.0;
14279        p.sampler_config.repetition_penalty = 1.0;
14280        p.set_trace(true);
14281        let r = p.generate("abcd", 10, None, None).unwrap();
14282        assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
14283        for (i, tr) in r.traces.iter().enumerate() {
14284            assert_eq!(tr.t, i, "trace index is sequential");
14285            assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
14286            assert_eq!(
14287                tr.confidence, r.token_confidence[i],
14288                "trace confidence matches the confidence channel"
14289            );
14290            // No dynamic router in this pipeline → no skill, no coherence.
14291            assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
14292        }
14293    }
14294
14295    #[test]
14296    fn explain_prefill_logits_match_greedy_first_token() {
14297        // `cortiq explain` shows the next-token distribution from
14298        // prefill_next_logits; its argmax must equal what greedy generate
14299        // actually emits first — otherwise explain would lie.
14300        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14301        p.sampler_config.temperature = 0.0;
14302        p.sampler_config.repetition_penalty = 1.0;
14303        let ids = p.tokenizer.encode("abcd");
14304        let logits = p.prefill_next_logits(&ids, None);
14305        let argmax = logits
14306            .iter()
14307            .enumerate()
14308            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14309            .unwrap()
14310            .0 as u32;
14311        let r = p.generate("abcd", 1, None, None).unwrap();
14312        assert_eq!(
14313            argmax, r.token_ids[0],
14314            "explain preview must match greedy emit"
14315        );
14316    }
14317
14318    #[test]
14319    fn laguna_shared_expert_is_unconditionally_added() {
14320        let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
14321        let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
14322        let zero_dense = || DenseFfn {
14323            gate_proj: matrix(vec![0.0; 4]),
14324            up_proj: matrix(vec![0.0; 4]),
14325            down_proj: matrix(vec![0.0; 4]),
14326            act: Act::Silu,
14327            down_t: None,
14328            segs: Vec::new(),
14329        };
14330        let shared = DenseFfn {
14331            gate_proj: identity(),
14332            up_proj: identity(),
14333            down_proj: identity(),
14334            act: Act::Silu,
14335            down_t: None,
14336            segs: Vec::new(),
14337        };
14338        let x = [1.0, 2.0];
14339        let expected = dense_ffn(&shared, &x, None);
14340        let moe = MoeFfn {
14341            router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
14342            experts: vec![zero_dense()],
14343            top_k: 1,
14344            norm_topk_prob: true,
14345            router_sigmoid: true,
14346            expert_bias: None,
14347            routed_scaling: 1.0,
14348            route_tau: None,
14349            shared: Some((shared, None)),
14350            stats: std::cell::RefCell::new(Vec::new()),
14351            act_sq: std::cell::RefCell::new(Vec::new()),
14352            act_rows: std::cell::RefCell::new(Vec::new()),
14353            mask: None,
14354            per_expert_scale: None,
14355            router_input_norm: false,
14356            resonance: None,
14357        };
14358        let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
14359        for (actual, expected) in actual.iter().zip(expected) {
14360            assert!((actual - expected).abs() < 1e-6);
14361        }
14362    }
14363
14364    #[test]
14365    fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
14366        const B: usize = 19;
14367        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14368        p.set_o1(Some(crate::nystrom::O1Cfg {
14369            layers: crate::nystrom::O1Layers::All,
14370            m: 4,
14371            w: 8,
14372            sink: 2,
14373            rect: crate::nystrom::O1Rect::Aggregate,
14374        }));
14375        p.o1_begin_with_prefix(Some(B));
14376        let ids: Vec<u32> = (0..B as u32).collect();
14377        let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
14378
14379        assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
14380        assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
14381        let next = p.embed_single(B as u32);
14382        let _ = p.forward_layers(&next, B, None);
14383        assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
14384    }
14385
14386    #[test]
14387    fn o1_pair_transition_commits_scratch_before_epoch_publication() {
14388        const B: usize = 19;
14389        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14390        // Keep a real recurrent layer ahead of the Full O(1) layer so the
14391        // pair test observes the GDN lane-2 scratch swap at the same
14392        // boundary, rather than only exercising an artificial scratch vec.
14393        let gdn_cfg = crate::linear_core::GdnCfg {
14394            num_v_heads: 2,
14395            num_k_heads: 1,
14396            key_head_dim: 2,
14397            value_head_dim: 4,
14398            conv_kernel: 3,
14399            hidden_size: 8,
14400            rms_eps: 1e-6,
14401            output_gate_sigmoid: false,
14402        };
14403        let synth = |n: usize, salt: usize| -> Vec<f32> {
14404            (0..n)
14405                .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
14406                .collect()
14407        };
14408        let qt = |rows: usize, cols: usize, salt: usize| {
14409            crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14410        };
14411        let c_dim = gdn_cfg.conv_dim();
14412        let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
14413        p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
14414            in_proj_qkv: qt(c_dim, 8, 1),
14415            in_proj_z: qt(vd, 8, 2),
14416            in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
14417            in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
14418            conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
14419            a_log: vec![0.2, 0.5],
14420            dt_bias: synth(gdn_cfg.num_v_heads, 6),
14421            norm: vec![1.0; gdn_cfg.value_head_dim],
14422            out_proj: qt(8, vd, 7),
14423        });
14424        p.gdn_cfg = Some(gdn_cfg);
14425        p.set_o1(Some(crate::nystrom::O1Cfg {
14426            layers: crate::nystrom::O1Layers::All,
14427            m: 4,
14428            w: 8,
14429            sink: 2,
14430            rect: crate::nystrom::O1Rect::Aggregate,
14431        }));
14432        p.o1_begin_with_prefix(Some(B));
14433        for pos in 0..B - 2 {
14434            let emb = p.embed_single(pos as u32);
14435            let _ = p.forward_layers(&emb, pos, None);
14436        }
14437        let lane1_state = p.kv_cache.layers[0].linear_state.clone();
14438
14439        let e1 = p.embed_single((B - 2) as u32);
14440        let e2 = p.embed_single((B - 1) as u32);
14441        let _ = p.forward_pair(&e1, &e2, B - 2);
14442
14443        assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
14444        assert!(
14445            p.kv_cache
14446                .layers
14447                .iter()
14448                .enumerate()
14449                .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
14450        );
14451        assert!(!p.kv_cache.layers[0].linear_state.is_empty());
14452        assert_ne!(
14453            p.kv_cache.layers[0].linear_state, lane1_state,
14454            "real pair must commit GDN lane 2 before returning"
14455        );
14456        assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
14457        let next = p.embed_single(B as u32);
14458        let _ = p.forward_layers(&next, B, None);
14459        assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
14460    }
14461
14462    #[test]
14463    fn o1_error_observation_stays_terminal_until_reset() {
14464        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14465        p.set_o1(Some(crate::nystrom::O1Cfg {
14466            layers: crate::nystrom::O1Layers::All,
14467            m: 4,
14468            w: 8,
14469            sink: 2,
14470            rect: crate::nystrom::O1Rect::Aggregate,
14471        }));
14472        p.o1_begin();
14473        p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
14474
14475        assert!(p.o1_seal_checked().is_err());
14476        assert!(
14477            p.o1_seal_checked().is_err(),
14478            "retry must see the sticky error"
14479        );
14480        let k = vec![0.2f32; 4];
14481        let v = vec![0.3f32; 4];
14482        p.kv_cache.layers[0].append(&k, &v, &[]);
14483        assert_eq!(p.kv_cache.layers[0].seq_len, 0);
14484
14485        p.reset_session();
14486        p.o1_begin();
14487        p.kv_cache.layers[0].append(&k, &v, &[]);
14488        assert_eq!(p.kv_cache.layers[0].seq_len, 1);
14489    }
14490
14491    #[test]
14492    fn nll_graph_failure_is_terminal_and_request_is_reusable() {
14493        let ids = vec![1u32, 2, 3, 4, 5, 6];
14494        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14495        p.graph_logits = Some(vec![123.0]);
14496        p.graph_want_logits = true;
14497        p.graph_failed
14498            .store(true, std::sync::atomic::Ordering::Relaxed);
14499        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14500        let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
14501        assert!(err.contains("before NLL"));
14502        assert!(p.graph_logits.is_none());
14503        assert!(!p.graph_want_logits);
14504        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14505        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14506
14507        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14508        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14509        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14510        assert_eq!(actual.1, expected.1);
14511        assert!((actual.0 - expected.0).abs() < 1e-9);
14512    }
14513
14514    #[test]
14515    fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
14516        let ids = vec![1u32, 2, 3, 4, 5, 6];
14517        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14518        p.nll_test_fail_at = Some(1);
14519        let err = p
14520            .nll_ids_from(&ids, 0)
14521            .expect_err("one-shot forward failure");
14522        assert!(err.contains("forward") || err.contains("score row"));
14523        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14524        assert!(!p.graph_want_logits);
14525        assert!(p.graph_logits.is_none());
14526        assert!(p.kv_history.is_empty());
14527
14528        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14529        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14530        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14531        assert_eq!(actual.1, expected.1);
14532        assert!((actual.0 - expected.0).abs() < 1e-9);
14533    }
14534
14535    #[test]
14536    fn nll_serial_failure_before_first_row_is_reported() {
14537        let ids = vec![1u32, 2, 3, 4];
14538        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14539        p.nll_test_force_serial = true;
14540        p.nll_test_fail_at = Some(0);
14541        let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
14542        assert!(err.contains("serial forward"));
14543        assert!(p.kv_history.is_empty());
14544        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14545        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14546    }
14547
14548    #[test]
14549    fn ffn_probe_failure_discards_recorder_and_state() {
14550        let ids = vec![1u32, 2, 3, 4];
14551        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14552        p.nll_test_fail_at = Some(0);
14553        let err = p
14554            .probe_ffn_mass_batch(&ids)
14555            .expect_err("probe forward failure");
14556        assert!(err.contains("NLL"));
14557        assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
14558        assert!(p.kv_history.is_empty());
14559        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14560    }
14561
14562    #[test]
14563    fn nll_test_controls_are_pipeline_scoped() {
14564        let ids = vec![1u32, 2, 3, 4];
14565        let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14566        let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14567        failing.nll_test_force_serial = true;
14568        failing.nll_test_fail_at = Some(0);
14569
14570        assert!(!failing.can_prefill_batched());
14571        assert!(unaffected.can_prefill_batched());
14572        let expected = unaffected
14573            .nll_ids_from(&ids, 0)
14574            .expect("unaffected pipeline remains usable");
14575        let err = failing
14576            .nll_ids_from(&ids, 0)
14577            .expect_err("failure injection belongs to failing pipeline");
14578        assert!(err.contains("serial forward"));
14579        assert!(failing.nll_test_fail_at.is_none());
14580        assert!(unaffected.can_prefill_batched());
14581        let actual = unaffected
14582            .nll_ids_from(&ids, 0)
14583            .expect("unaffected pipeline remains reusable");
14584        assert_eq!(actual.1, expected.1);
14585        assert!((actual.0 - expected.0).abs() < 1e-9);
14586    }
14587
14588    #[test]
14589    fn forward_ids_failure_channel_is_terminal_and_reusable() {
14590        let ids = vec![1u32, 2, 3, 4, 5, 6];
14591        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14592        p.graph_logits = Some(vec![123.0]);
14593        p.graph_want_logits = true;
14594        p.graph_failed
14595            .store(true, std::sync::atomic::Ordering::Relaxed);
14596        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14597
14598        let err = p
14599            .forward_ids(&ids, None)
14600            .expect_err("a failed forward must not become a valid head result");
14601        assert!(err.contains("forward_ids setup"));
14602        assert!(p.graph_logits.is_none());
14603        assert!(!p.graph_want_logits);
14604        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14605        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14606        assert_eq!(p.kv_cache.seq_len(), 0);
14607
14608        let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
14609            .forward_ids(&ids, None)
14610            .expect("fresh forward_ids");
14611        let actual = p
14612            .forward_ids(&ids, None)
14613            .expect("pipeline remains reusable after a failed forward");
14614        assert_eq!(actual.len(), expected.len());
14615        assert!(
14616            actual
14617                .iter()
14618                .zip(expected)
14619                .all(|(a, b)| (a - b).abs() < 1e-9)
14620        );
14621        assert_eq!(p.kv_cache.seq_len(), ids.len());
14622    }
14623}