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    /// Keep generating past end-of-sequence ids (the llama-bench contract
175    /// for a timed run). A loop flag, deliberately NOT a sampler
176    /// suppression: suppressed ids count as a penalty and switch the
177    /// speculative round and the greedy burst off, so a benchmark that
178    /// suppressed EOS never measured either.
179    pub ignore_eos: bool,
180    /// Draft-head shortlist guard: tokens left during which the draft
181    /// uses the FULL head because a recently committed id lay past the
182    /// `CMF_DRAFT_VOCAB` cut (Cyrillic and CJK ids sit above 131072 in
183    /// Qwen's table, so a prefix shortlist would draft nothing usable
184    /// there — measured on Russian prose: 2.9 → 1.6 accepted a round).
185    pub draft_full_streak: u32,
186    /// Adaptive draft depth for the speculative round (None until the
187    /// first round): grows while nearly every draft is accepted, shrinks
188    /// when fewer than half are. The verify's cost climbs with the rows on
189    /// a discrete card (RTX PRO 4000: 52 ms at 2 rows, 74 at 5, 80 at 6),
190    /// so prose wants k≈3 and code or the repetitive bench k≈5 — measured
191    /// 33.6 vs 27.6 tok/s on an essay at k=3 vs 5, 45.6 vs 38 on code.
192    /// `CMF_GRAPH_SPEC_K` pins it.
193    pub spec_k_adapt: Option<usize>,
194    /// EWMA of the accepted fraction that drives `spec_k_adapt`.
195    pub spec_acc_ewma: f32,
196    rng: SplitMix64,
197    sampler_scratch: SamplerScratch,
198    /// Speculative SAMPLING state (graph_spec_step, temperature > 0): the
199    /// correction token a rejected draft produced — committed by the loop
200    /// top in place of a fresh draw — and the per-round draft
201    /// distributions / target scratch, reused so a round allocates
202    /// nothing at the vocab size.
203    spec_forced: Option<u32>,
204    spec_q: Vec<Vec<f32>>,
205    spec_p: Vec<f32>,
206    spec_res: Vec<f32>,
207    /// The same three for the sparse chain (top-k configs).
208    spec_qs: Vec<sampler::Sparse>,
209    spec_ps: sampler::Sparse,
210    spec_ress: sampler::Sparse,
211    /// Which arm the MTP draft block runs on this generation: Some(true)
212    /// = the whole-token graph (device attention, one submit a step),
213    /// Some(false) = the per-op path; None = not decided yet. Decided
214    /// on the first draft and held, because the two arms keep the MTP
215    /// KV in different places (device mirror vs the CPU cache) and a
216    /// mid-run switch would read the wrong one.
217    mtp_graph_mode: Option<bool>,
218    /// The Metal verify graph of the round in flight, between its sync
219    /// (logits read) and the commit that replays the accepted prefix.
220    #[cfg(target_os = "macos")]
221    metal_verify: Option<MetalVerifyPending>,
222    /// Precomputed RoPE inverse frequencies [head_dim/2]. Arc: the
223    /// forward path clones a handle to escape the &mut self borrow —
224    /// cloning the table itself was a per-forward allocation.
225    pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
226    /// Reusable norm buffers for the decode hot path (roadmap §3 P0:
227    /// steady-state forward should not heap-allocate). Disjoint field
228    /// from `weights`/`kv_cache`, so split borrows keep working.
229    ws: ForwardScratch,
230    /// Persistent worker pool (None = serial; see CMF_THREADS).
231    pool: Option<std::sync::Arc<Pool>>,
232    // ── Dynamic per-token skill routing (spec §9, claim 14/16) ──
233    /// Source model, retained so a skill switch can re-resolve the
234    /// touched layers' FFN tensors (Mapped = mmap pointers, cheap).
235    pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
236    /// Masks present → weights are dequantized f32 (rebuild path).
237    pub(crate) dyn_force_f32: bool,
238    /// Per-skill FFN layers actually replaced (derived from tensors, not
239    /// the meta `layers` field — ru2 replaces down_proj in 0..23 while
240    /// its meta says [20..23]). None = skill touches non-FFN tensors →
241    /// ineligible for cheap dynamic switching (honest refusal).
242    pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
243    /// Currently overlaid skill (index into model.header.skills); None =
244    /// backbone. Set at load time to the statically-overlaid skill so
245    /// `set_active_skill(None)` correctly reverts it (else a static
246    /// skill would silently persist — the union-diff assumes dyn_active
247    /// always mirrors the live overlay). Switched by `set_active_skill`.
248    pub(crate) dyn_active: Option<usize>,
249    /// Pipeline was loaded with a soft blend (materialized working
250    /// tensors, not a single skill index) → dynamic routing refuses:
251    /// there is no single index to revert the blend from.
252    pub(crate) dyn_blend_loaded: bool,
253    /// Layer whose post-residual hidden feeds the router φ (shared by
254    /// swarm skills). None = φ capture off.
255    pub(crate) dyn_phi_layer: Option<usize>,
256    /// EMA of φ at `dyn_phi_layer` over the decode window (on-policy).
257    dyn_phi_ema: Vec<f32>,
258    dyn_phi_seen: usize,
259    /// Hysteresis router driving per-token skill switches during decode
260    /// (None = static/no dynamic routing). Taken out during generation.
261    pub dyn_router: Option<crate::swarm::DynRouter>,
262    /// O(1) Nyström attention setting (CLI/env/header-hint resolved by
263    /// the caller; None = plain cache attention everywhere).
264    o1_cfg: Option<crate::nystrom::O1Cfg>,
265    /// Bumped once per collecting→sealed transition — the GPU state mirror
266    /// re-uploads when it sees a new epoch (each fresh sealed state).
267    o1_epoch: u64,
268    /// Per-layer o1 flags derived from `o1_cfg` (Full layers only).
269    o1_flags: Vec<bool>,
270    /// Emit a structured per-token trace (B4 telemetry channel). Off by
271    /// default — the runtime is silent unless observation is requested.
272    trace: bool,
273    /// Confidence-calibration temperature (B1): reported probability is
274    /// softmax(logits / calib_temp). 1.0 = raw. Set from header.calibration.
275    calib_temp: f32,
276    /// Process-unique id keying this pipeline's device KV mirrors.
277    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
278    graph_kv_id: u64,
279    /// Decode asks the token graph to also run final-norm + lm_head on
280    /// the device (drops the separate per-op lm_head round trip).
281    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
282    graph_want_logits: bool,
283    /// NLL quality gates require the graph's fused head rather than silently
284    /// accepting a CPU head fallback. Generation keeps the historical
285    /// best-effort `graph_want_logits` behavior.
286    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
287    graph_head_required: bool,
288    /// Logits the graph produced for the token just forwarded (taken by
289    /// the decode loop; None = compute on the CPU path).
290    graph_logits: Option<Vec<f32>>,
291    /// Token embeddings are multiplied by this at input (Gemma: √hidden).
292    pub embed_multiplier: f32,
293    /// Attention score scale (1/√head_dim unless the arch overrides —
294    /// Gemma's query_pre_attn_scalar).
295    pub attn_scale: f32,
296    /// Sliding-window attention: (window, every-Nth-layer-is-global
297    /// pattern) — Gemma-3.
298    pub swa: Option<(usize, usize)>,
299    /// Explicit local/global schedule for architectures that cannot be
300    /// represented by Gemma's every-Nth-global convention.
301    pub sliding_layers: Option<Vec<bool>>,
302    /// RoPE table of the sliding (local) layers, when they use their
303    /// own base frequency (Gemma-3: 10k local vs 1M global).
304    pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
305    pub rotary_dim_local: Option<usize>,
306    pub rope_scale: f32,
307    pub rope_scale_local: f32,
308    /// Gemma-4: global layers run their own geometry — (head_dim,
309    /// num_kv_heads); sliding layers keep the base fields.
310    pub global_attn: Option<(usize, usize)>,
311    /// Gemma-4: the global layers' proportional RoPE table (len
312    /// global_head_dim/2, zero-padded tail = identity rotation).
313    pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
314    /// Scale-less RMS normalization of V heads before caching (Gemma-4).
315    pub attn_v_norm: bool,
316    /// HunYuan dense: per-head q/k norm runs after RoPE (see the arch flag).
317    pub qk_norm_after_rope: bool,
318    /// Final-logit soft-capping C: logits = C·tanh(logits/C) (Gemma-4).
319    pub final_softcap: Option<f32>,
320    /// Cortiq Embryo hierarchical head: cluster matrix [C, hidden]. The
321    /// flat logits h·Eᵀ are turned into the two-level log-probabilities
322    /// log softmax_c(h·Cᵀ)[c(v)] + log softmax_{s∈c(v)}(h·E_c(v)ᵀ)[v].
323    pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
324    /// Gemma-2 attention-logit soft-capping (0.0 = off).
325    pub attn_softcap: f32,
326    /// Compute per-token confidence (a full-vocab softmax each
327    /// token). On by default; `bench --core` turns it off to match
328    /// llama-bench's core timing.
329    confidence_on: bool,
330    /// Test-only one-shot forward failure, scoped to this pipeline so
331    /// parallel scoring tests cannot consume one another's injection.
332    #[cfg(test)]
333    nll_test_fail_at: Option<usize>,
334    /// Test-only route override; avoids mutating the process-wide
335    /// `CMF_PREFILL` environment variable while forcing the serial path.
336    #[cfg(test)]
337    nll_test_force_serial: bool,
338}
339
340#[cfg(target_os = "macos")]
341impl Drop for Pipeline {
342    fn drop(&mut self) {
343        // the async replay writes into `kv_cache` Vecs about to be freed
344        let _ = crate::gpu_metal::wait_replay();
345        crate::gpu::kv_mirror_drop(self.graph_kv_id);
346    }
347}
348
349/// Model weights. Matrices are `QTensor` (owned f32 for small models
350/// and tests — bit-identical to the historical paths — or quantized
351/// bytes zero-copy from the CMF mmap for big models). 1-D norms are
352/// always small and stay f32.
353pub struct PipelineWeights {
354    /// Embedding table: [vocab_size, hidden_size]
355    pub embed_tokens: QTensor,
356    /// Per-layer weights
357    pub layers: Vec<LayerWeights>,
358    /// LM head: [vocab_size, hidden_size]
359    pub lm_head: QTensor,
360    /// Final norm: [hidden_size]
361    pub final_norm: Vec<f32>,
362}
363
364/// One transformer layer: shared norms + MLP, attention by kind.
365pub struct LayerWeights {
366    pub input_norm: Vec<f32>,
367    /// The pre-FFN norm (`post_attention_layernorm` classically;
368    /// `pre_feedforward_layernorm` on Gemma-2/3 sandwich layers).
369    pub post_norm: Vec<f32>,
370    /// Gemma-2/3 sandwich: norm applied to the ATTENTION OUTPUT before
371    /// its residual add (`post_attention_layernorm` there).
372    pub attn_out_norm: Option<Vec<f32>>,
373    /// Gemma-4: the whole layer output is multiplied by this scalar.
374    pub layer_scale: Option<f32>,
375    /// Gemma-2/3 sandwich: norm applied to the FFN OUTPUT before its
376    /// residual add (`post_feedforward_layernorm`).
377    pub ffn_out_norm: Option<Vec<f32>>,
378    pub ffn: FfnKind,
379    pub attn: AttnKind,
380}
381
382/// FFN gate activation: SiLU (SwiGLU family) or tanh-GELU (Gemma's
383/// GeGLU). A property of the model, carried on every FFN triple.
384#[derive(Clone, Copy, PartialEq, Debug, Default)]
385pub enum Act {
386    #[default]
387    Silu,
388    GeluTanh,
389    /// Kimi-K3 SituAndMul: BOTH halves transform —
390    /// a = β·tanh(g/β)·σ(g), up' = linβ·tanh(u/linβ) (linβ>0), out = a·up'.
391    Situ {
392        beta: f32,
393        linear_beta: f32,
394    },
395}
396
397impl Act {
398    pub fn from_arch(name: &str) -> Self {
399        if name == "gelu_tanh" {
400            Self::GeluTanh
401        } else {
402            Self::Silu
403        }
404    }
405
406    /// Arch-driven constructor (activation name + situ betas).
407    pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
408        match arch.hidden_act.as_str() {
409            "situ" => Self::Situ {
410                beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
411                linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
412            },
413            other => Self::from_arch(other),
414        }
415    }
416
417    #[inline]
418    pub fn apply(self, x: f32) -> f32 {
419        match self {
420            Self::Silu => inference::silu(x),
421            Self::GeluTanh => inference::gelu_tanh(x),
422            Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
423        }
424    }
425
426    /// Gated combine — the FFN contract. Situ transforms the UP half
427    /// too, so callers must use this instead of apply(g)·u.
428    #[inline]
429    pub fn combine(self, g: f32, u: f32) -> f32 {
430        match self {
431            Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
432                self.apply(g) * (linear_beta * (u / linear_beta).tanh())
433            }
434            _ => self.apply(g) * u,
435        }
436    }
437}
438
439/// Dense gated triple — the FFN of a dense layer or of one expert.
440pub struct DenseFfn {
441    pub gate_proj: QTensor,
442    pub up_proj: QTensor,
443    pub down_proj: QTensor,
444    /// Gate activation (SiLU default; Gemma: tanh-GELU).
445    pub act: Act,
446    /// `down_proj` stored transposed (`[inter, hidden]`), when the file
447    /// carries it. Only the per-token sparse path reads it: a neuron's
448    /// down weights are a contiguous ROW there, so the token's chosen
449    /// neurons are the only bytes touched. `None` = the ordinary layout,
450    /// and the sparse path stays off.
451    pub down_t: Option<QTensor>,
452    /// Task tubes (spec: defragged task-conditional width). The three
453    /// matrices above are the CORE — the neurons every task computes;
454    /// each tube is an independently quantized slice of the SAME layer
455    /// holding the neurons only some tasks need. A tube is a normal
456    /// tensor triple, so every kernel runs it unchanged, and the bytes
457    /// of an inactive tube are never read. Empty = ordinary dense FFN.
458    pub segs: Vec<FfnSeg>,
459}
460
461/// One task tube: a contiguous slice of a layer's FFN neurons, stored
462/// as its own `[w, hidden]` / `[hidden, w]` triple. `start` is the
463/// neuron's index in the layer's FULL space (core first, then tubes in
464/// order) — the bit a task mask sets to switch this tube on.
465pub struct FfnSeg {
466    pub gate: QTensor,
467    pub up: QTensor,
468    pub down: QTensor,
469    pub start: usize,
470    pub width: usize,
471}
472
473/// FFN operator of a layer, decided by tensor presence at load time
474/// (router `mlp.gate.weight` in the directory = MoE layer).
475pub enum FfnKind {
476    Dense(DenseFfn),
477    /// Mixture-of-Experts (Qwen2-MoE / Qwen3-MoE): softmax over ALL
478    /// expert logits → top-k, optional renorm; experts stay quantized
479    /// in mmap — only the selected ones are touched per token.
480    Moe(MoeFfn),
481    /// Gemma-4 MoE: a dense MLP branch AND a routed-expert branch in
482    /// the SAME layer, each with its own norm sandwich. The dense
483    /// branch reads the pre-FFN-normed input; the expert branch (and
484    /// the router) read the RAW residual through `pre_norm_2`:
485    ///   d = post_norm_1(dense(x̂));  m = post_norm_2(Σwₑ·FFNₑ(pre_norm_2(h)))
486    ///   ffn_out = d + m   (the caller's ffn_out_norm + residual follow)
487    DenseMoe(Box<DenseMoeFfn>),
488}
489
490/// Gemma-4 dual-branch FFN (see `FfnKind::DenseMoe`).
491pub struct DenseMoeFfn {
492    pub dense: DenseFfn,
493    pub moe: MoeFfn,
494    /// post_feedforward_layernorm_1 — dense-branch output norm.
495    pub post_norm_1: Vec<f32>,
496    /// pre_feedforward_layernorm_2 — expert-branch input norm (applied
497    /// to the RAW residual, not the pre-FFN-normed activation).
498    pub pre_norm_2: Vec<f32>,
499    /// post_feedforward_layernorm_2 — expert-branch output norm.
500    pub post_norm_2: Vec<f32>,
501}
502
503pub struct MoeFfn {
504    /// Router `mlp.gate.weight` [num_experts, hidden].
505    pub router: QTensor,
506    pub experts: Vec<DenseFfn>,
507    pub top_k: usize,
508    pub norm_topk_prob: bool,
509    /// Router scores per-expert with a sigmoid (LFM2-MoE / DeepSeek-V3
510    /// `noaux_tc`) instead of a softmax over all experts (Qwen).
511    pub router_sigmoid: bool,
512    /// Per-expert selection bias `mlp.expert_bias` [num_experts]
513    /// (LFM2-MoE): added to the sigmoid scores for the top-k CHOICE only;
514    /// the gathered weights use the unbiased scores. None = no bias.
515    pub expert_bias: Option<Vec<f32>>,
516    /// Top-k weights are multiplied by this after the optional renorm
517    /// (LFM2-MoE `routed_scaling_factor`; 1.0 = off).
518    pub routed_scaling: f32,
519    /// Adaptive routing (CMF_MOE_TAU, opt-in): keep the smallest
520    /// prefix of the top-k whose renormalized mass reaches τ —
521    /// confident tokens touch 1–2 experts, flat ones keep all k.
522    /// MoE decode is memory-bound, so skipped experts are skipped
523    /// weight traffic. None = classic fixed top-k (bit-identical).
524    pub route_tau: Option<f32>,
525    /// Always-on shared expert. Qwen2-MoE carries an additional sigmoid
526    /// gate; Laguna adds the shared expert unconditionally (`None`).
527    pub shared: Option<(DenseFfn, Option<QTensor>)>,
528    /// Expert-selection counters (truncated Fisher B-field of claim 12:
529    /// routing frequency during calibration). Filled by every forward,
530    /// read by the CLI via CMF_MOE_STATS. RefCell: decode is single-threaded.
531    pub stats: std::cell::RefCell<Vec<u64>>,
532    /// Per-CHANNEL sum of squares of this FFN's input, accumulated over a
533    /// calibration run (`CMF_RMS_TRACE`). These are the RMS activation
534    /// traces AWNP needs: raw weight magnitude says every channel matters
535    /// equally, and the question AWNP asks is whether the ACTIVATIONS
536    /// disagree. Off unless the env var is set — an f64 add per channel
537    /// per token is cheap, but not free.
538    pub act_sq: std::cell::RefCell<Vec<f64>>,
539    /// Raw FFN-input rows captured for the layers named by `CMF_ACT_DUMP`
540    /// (`"9,19"`). AWNP is nullspace PROJECTION: after dropping channels the
541    /// survivors are refitted to absorb what was removed, and how much they
542    /// can absorb depends on the activation COVARIANCE, not on per-channel
543    /// RMS. Per-channel numbers can only bound the cost from above.
544    pub act_rows: std::cell::RefCell<Vec<f32>>,
545    /// Task mask over routed experts (DTG-MA over MoE, claim-12 B-field
546    /// applied): `false` experts are excluded from selection, the
547    /// softmax renormalizes over the allowed set. Built by the loader
548    /// from CMF_MOE_MASK=<stats.json> + CMF_MOE_MASK_COVER. None = all.
549    pub mask: Option<Vec<bool>>,
550    /// Gemma-4: per-expert weight scale applied AFTER the top-k renorm
551    /// (`router.per_expert_scale`). None = 1.0 everywhere.
552    pub per_expert_scale: Option<Vec<f32>>,
553    /// Gemma-4: the router reads a SCALE-LESS rms-norm of its input
554    /// (the constant gain router.scale·√hidden is folded into the
555    /// router weights at convert time).
556    pub router_input_norm: bool,
557    /// Cortiq Embryo: resonance routing (P1) — the "logits" are
558    /// bias_e − ‖(x−μ_e) − U_eᵀU_e(x−μ_e)‖², argmax = the expert whose
559    /// descriptor reconstructs the input best. `router` is a placeholder.
560    pub resonance: Option<Resonance>,
561}
562
563/// Per-expert resonance descriptors of one MoE layer (`mlp.desc.*`).
564pub struct Resonance {
565    /// [E, hidden]
566    pub mu: Vec<f32>,
567    /// [E, k, hidden] orthonormal directions (k may be 0)
568    pub u: Vec<f32>,
569    pub k: usize,
570    /// [E] selection bias (loss-free balancing, trained online)
571    pub bias: Vec<f32>,
572}
573
574impl Resonance {
575    /// Routing scores for one input row (higher = better).
576    pub fn scores(&self, x: &[f32], out: &mut [f32]) {
577        let h = x.len();
578        let ne = out.len();
579        for e in 0..ne {
580            let mu = &self.mu[e * h..(e + 1) * h];
581            let mut d2 = 0.0f32;
582            for j in 0..h {
583                let d = x[j] - mu[j];
584                d2 += d * d;
585            }
586            let mut proj = 0.0f32;
587            for i in 0..self.k {
588                let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
589                let mut p = 0.0f32;
590                for j in 0..h {
591                    p += (x[j] - mu[j]) * u[j];
592                }
593                proj += p * p;
594            }
595            out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
596        }
597    }
598}
599
600/// Attention operator of a layer. Extension point: new operators are
601/// new variants here + a forward in their own module.
602pub enum AttnKind {
603    /// GQA softmax attention (+ optional Qwen3.5 qk-norm / output gate).
604    Full {
605        wq: QTensor,
606        wk: QTensor,
607        wv: QTensor,
608        wo: QTensor,
609        q_norm: Option<Vec<f32>>,
610        k_norm: Option<Vec<f32>>,
611        output_gate: bool,
612        /// Laguna: a separate softplus projection applied to the attention
613        /// output before O. The bool means one scalar per head (broadcast
614        /// across head_dim); false means one scalar per element.
615        softplus_gate: Option<(QTensor, bool)>,
616        /// Qwen2-family projection biases (q, k, v).
617        bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
618    },
619    /// Canonical linear core (VMF phase attention).
620    Linear(VmfPhaseWeights),
621    /// Faithful vendor linear operator (Qwen3.5 GatedDeltaNet).
622    LinearGdn(GdnWeights),
623    /// LFM2 gated short-convolution mixer (no KV cache; conv ring state
624    /// lives in the layer's `linear_state`).
625    ShortConv(ShortConvWeights),
626    /// DeepSeek-V2 Multi-head Latent Attention. v1 executes it as
627    /// expand-to-MHA: the latent is projected per token, K/V expand to
628    /// every head and live in the ordinary cache (K head layout
629    /// [rope | nope] so the standard partial rotary covers the shared
630    /// rope key; V rows are zero-padded to the K head_dim and the pad
631    /// is sliced off before O). Latent-resident cache is a later
632    /// optimization, not a semantic change.
633    Mla(Box<MlaWeights>),
634    /// Kimi Delta Attention (Kimi Linear / Kimi-K3): per-channel decayed
635    /// delta rule, separate q/k/v short convs, sigmoid-gated output norm.
636    /// State lives in the layer's `linear_state` (no KV cache).
637    Kda(Box<crate::linear_core::KdaWeights>),
638}
639
640/// DeepSeek-V2 MLA projections (see `AttnKind::Mla`).
641pub struct MlaWeights {
642    /// `[nh·(rope+nope), hidden]` (or `[…, q_lora]` when compressed) —
643    /// the converter permutes each head rope-first so rotary_dim =
644    /// qk_rope works unchanged.
645    pub q_proj: QTensor,
646    /// Compressed q (K3/V3 class): x → q_a `[q_lora, hidden]` →
647    /// rms(q_a_norm) → q_proj (= q_b). None = direct q (V2-Lite).
648    pub q_a: Option<QTensor>,
649    pub q_a_norm: Option<Vec<f32>>,
650    /// `kv_a_proj_with_mqa` `[lora + rope, hidden]` (latent first).
651    pub kv_a: QTensor,
652    /// RMS-norm weights over the latent (`kv_a_layernorm`, [lora]).
653    pub kv_a_norm: Vec<f32>,
654    /// `[nh·(nope+v), lora]` — per head [k_nope | v].
655    pub kv_b: QTensor,
656    /// `[hidden, nh·v]`.
657    pub o_proj: QTensor,
658    pub nh: usize,
659    pub qk_rope: usize,
660    pub qk_nope: usize,
661    pub v_dim: usize,
662    pub lora: usize,
663    /// Softmax scale (1/√(rope+nope), YaRN-mscale-corrected at load).
664    pub scale: f32,
665    /// Kimi Linear NoPE: skip the rotary entirely (layout unchanged).
666    pub nope: bool,
667}
668
669/// Multi-token-prediction head (DeepSeek/Qwen style, spec §2.1):
670/// `x = eh_proj·[enorm(embed(next)); hnorm(hidden)]` → one transformer
671/// block over its own KV → shared lm_head. Drafts the token after next;
672/// the main model verifies, so output is exact — MTP only buys speed.
673pub struct MtpModule {
674    pub enorm: Vec<f32>,
675    pub hnorm: Vec<f32>,
676    /// [hidden, 2·hidden]
677    pub eh_proj: QTensor,
678    pub layer: LayerWeights,
679    pub final_norm: Vec<f32>,
680    pub kv: crate::kv_cache::LayerKvCache,
681}
682
683/// A Metal verify graph after its sync: what the commit needs — the
684/// graph (per-layer replay scratch), the GDN layers in encode order (their
685/// CPU states receive the replay), and the attention layers with the CPU
686/// row count they were encoded against (the accepted rows are pulled from
687/// the mirror from there).
688/// One item of the Metal rows-graph plan.
689#[cfg(target_os = "macos")]
690enum MetalRowsItem<'a> {
691    Gdn {
692        run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
693        first: usize,
694    },
695    Attn {
696        l: crate::gpu_metal::AttnGpuLayer<'a>,
697        li: usize,
698        q_norm: Option<&'a [f32]>,
699        k_norm: Option<&'a [f32]>,
700        output_gate: bool,
701    },
702}
703
704#[cfg(target_os = "macos")]
705struct MetalVerifyPending {
706    graph: crate::gpu_metal::VerifyGraph,
707    gdn_layers: Vec<usize>,
708    attn_layers: Vec<(usize, usize)>,
709}
710
711/// A round's batched MTP warm-up, submitted but not yet waited
712/// (`mtp_warm_batch_submit` → `mtp_warm_batch_finish`): the trunk commit's
713/// GDN replay is queued between the two.
714#[cfg(target_os = "macos")]
715struct MetalWarmPending {
716    graph: crate::gpu_metal::VerifyGraph,
717    cpu_stored: usize,
718    b: usize,
719}
720
721#[cfg(target_os = "macos")]
722enum MetalRowsRun {
723    /// Capability/preflight refusal before a command buffer was committed.
724    Declined,
725    /// A graph was admitted and then failed; callers must clear the sequence
726    /// rather than replaying it through CPU/serial state.
727    Failed,
728    Completed(MetalVerifyPending),
729}
730
731#[cfg(target_os = "macos")]
732enum MetalPrefillOutcome {
733    Declined,
734    Failed,
735    Completed(Vec<f32>),
736}
737
738#[cfg(target_os = "macos")]
739enum MetalBatchNllOutcome {
740    Declined,
741    Failed(String),
742    Completed(f64, usize),
743}
744
745/// The speculation trial's phases (see the decode loop): four timed
746/// speculative rounds, eight timed plain tokens, then the faster arm
747/// until a re-check.
748#[derive(Clone, Copy)]
749enum SpecTrial {
750    Spec {
751        t0: std::time::Instant,
752        gen0: usize,
753        rounds: usize,
754    },
755    Plain {
756        t0: std::time::Instant,
757        gen0: usize,
758    },
759    Decided {
760        spec: bool,
761        recheck_at: usize,
762    },
763}
764
765/// `CMF_GRAPH_SPEC_TIME`: 0 = off, 1 = one line per speculative round
766/// plus the host stamps of any OUTLIER round (wall > 1.4× the running
767/// median), 2 = the host stamps of every round.
768pub(crate) fn spec_time_level() -> u8 {
769    static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
770    *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
771        Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
772        Err(_) => 0,
773    })
774}
775
776/// The round's host stamps: `spec_stamp(name)` records the time since
777/// the previous stamp (the section that just ended) — from anywhere on
778/// the round's call chain (the Metal verify, the draft step, the commit),
779/// no plumbing. Off (a single atomic load) unless `CMF_GRAPH_SPEC_TIME`
780/// is set; one decode thread at a time is assumed (diagnostics).
781struct SpecStampLog {
782    t_last: std::time::Instant,
783    items: Vec<(&'static str, f32)>,
784}
785
786static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
787
788pub(crate) fn spec_stamp(name: &'static str) {
789    if spec_time_level() == 0 {
790        return;
791    }
792    if let Ok(mut g) = SPEC_STAMPS.lock() {
793        if let Some(log) = g.as_mut() {
794            let now = std::time::Instant::now();
795            log.items
796                .push((name, (now - log.t_last).as_secs_f32() * 1e3));
797            log.t_last = now;
798        }
799    }
800}
801
802fn spec_stamps_begin() {
803    if spec_time_level() == 0 {
804        return;
805    }
806    if let Ok(mut g) = SPEC_STAMPS.lock() {
807        *g = Some(SpecStampLog {
808            t_last: std::time::Instant::now(),
809            items: Vec::with_capacity(64),
810        });
811    }
812}
813
814fn spec_stamps_take() -> Vec<(&'static str, f32)> {
815    SPEC_STAMPS
816        .lock()
817        .ok()
818        .and_then(|mut g| g.take())
819        .map(|l| l.items)
820        .unwrap_or_default()
821}
822
823/// One line: every stamp name in first-seen order with its total over the
824/// round and, when it fired more than once (the draft steps), the count.
825fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
826    let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
827    for &(n, ms) in items {
828        match agg.iter_mut().find(|e| e.0 == n) {
829            Some(e) => {
830                e.1 += ms;
831                e.2 += 1;
832            }
833            None => agg.push((n, ms, 1)),
834        }
835    }
836    let mut s = String::with_capacity(agg.len() * 16);
837    for (n, ms, k) in agg {
838        if k > 1 {
839            s.push_str(&format!("{n} {ms:.1}/{k} "));
840        } else {
841            s.push_str(&format!("{n} {ms:.1} "));
842        }
843    }
844    s
845}
846
847/// The speculation monitor: exponential averages of a round's wall time
848/// and of the tokens it produced, and the plain token's wall time — the
849/// three numbers the keep/stop rule needs. A round pays when
850/// `tokens_per_round · plain_ms > round_ms · 1.03`. The one-shot trial
851/// (four rounds against eight tokens) mis-called prose: the first rounds
852/// after a prompt are formulaic and accept well, the body does not (an
853/// essay measured 39 against a plain 44.8 with the trial saying
854/// "speculate"), so the rule now runs on EVERY round and stops after four
855/// consecutive losing rounds; a stopped speculation is retried 128 tokens
856/// later.
857///
858/// Native Metal (`metal: true`) does not pay the eight plain tokens up
859/// front: on the 27B a plain token is ~150 ms, so the trial alone cost
860/// ~1.2 s of every answer. There the plain phase is (a) skipped while the
861/// rounds land at least `SPEC_PROXY_TOKENS` tokens each — a k=7 round on
862/// Metal costs ~1.9 plain tokens (286 against 148 ms measured on the M4),
863/// so 3.5 tokens/round cannot lose on any Metal round/plain ratio seen —
864/// and (b) otherwise bounded to the fewest tokens that time it: two, or
865/// as many as fit in `SPEC_PLAIN_MIN_MS` (a 150-ms token measures itself;
866/// a 10-ms one needs the eight). The keep/stop rule itself is unchanged:
867/// the moment a plain rate exists, it decides.
868#[derive(Default, Clone, Copy)]
869struct SpecMon {
870    round_ms: f64,
871    tokens: f64,
872    plain_ms: f64,
873    n: u32,
874    fails: u32,
875    metal: bool,
876}
877
878/// Tokens per round at or above which a Metal round pays without a plain
879/// measurement (see `SpecMon`).
880const SPEC_PROXY_TOKENS: f64 = 3.5;
881/// The Metal plain phase: at least two tokens, and more until this much
882/// wall time has been timed (up to the eight the other backends time).
883const SPEC_PLAIN_MIN_MS: f64 = 200.0;
884
885impl SpecMon {
886    fn round(&mut self, dt_ms: f64, produced: usize) {
887        self.n += 1;
888        if self.n == 1 {
889            return; // round 1 pays the batch scratch and the draft mirror
890        }
891        let a = if self.n == 2 { 1.0 } else { 0.3 };
892        self.round_ms += a * (dt_ms - self.round_ms);
893        self.tokens += a * (produced as f64 - self.tokens);
894    }
895    fn pays(&self) -> bool {
896        if self.plain_ms > 0.0 {
897            self.tokens * self.plain_ms > self.round_ms * 1.03
898        } else {
899            self.metal && self.tokens >= SPEC_PROXY_TOKENS
900        }
901    }
902    /// Has the plain phase timed enough tokens to decide?
903    fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
904        let n = generated.saturating_sub(gen0);
905        if n >= 8 {
906            return true;
907        }
908        self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
909    }
910}
911
912/// Result of a generation call.
913pub struct GenerateResult {
914    pub text: String,
915    pub token_ids: Vec<u32>,
916    pub prompt_tokens: usize,
917    pub tokens_generated: usize,
918    pub finish_reason: String,
919    /// Speculative-decode stats (0/0 when MTP is absent or inactive).
920    pub mtp_drafted: usize,
921    pub mtp_accepted: usize,
922    /// Per-generated-token confidence = softmax probability of the token
923    /// that was actually emitted (softmax probability on the chosen state). High =
924    /// the model was sure; low = it was guessing. Same length as the
925    /// generated slice of `token_ids`.
926    pub token_confidence: Vec<f32>,
927    /// Structured per-token telemetry (B4 channel). Empty unless
928    /// `set_trace(true)`; otherwise same length as the generated slice.
929    pub traces: Vec<TokenTrace>,
930}
931
932/// One row of the structured telemetry trace (B4): the model's internal
933/// routing state at the moment a token was emitted. Every field is a
934/// quantity the runtime already computes — nothing is inferred or
935/// estimated (anti-principle: only measured bytes).
936#[derive(Clone, Debug)]
937pub struct TokenTrace {
938    /// 0-based index within the generated slice.
939    pub t: usize,
940    /// The emitted token id.
941    pub token_id: u32,
942    /// Softmax probability on the emitted token — how sure the model was.
943    pub confidence: f32,
944    /// Skill in force while this token was generated (None = backbone).
945    pub active_skill: Option<String>,
946    /// Recon error E = ‖r−BBᵀr‖²/‖φ‖² at the last routing eval — coherence
947    /// with the active skill's subspace (low = coherent). None = no router
948    /// or not yet evaluated.
949    pub recon: Option<f32>,
950    /// The router changed the active skill right after this token (a
951    /// domain boundary crossed under the hysteresis barrier).
952    pub switched: bool,
953}
954
955/// Calibrated softmax probability of `id` under `logits` (the confidence on
956/// the emitted token) — the confidence signal, cheap from logits already
957/// computed for sampling. `temp` is the calibration temperature (B1):
958/// softmax(logits / temp); 1.0 = raw.
959#[cfg_attr(not(test), allow(dead_code))]
960fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
961    let t = if temp > 1e-3 { temp } else { 1.0 };
962    let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
963    let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
964    if sum > 0.0 {
965        (((logits[id as usize] - max) / t).exp()) / sum
966    } else {
967        0.0
968    }
969}
970
971/// prefill-GEMM enabled? (CMF_PREFILL=seq — emergency fallback to the
972/// sequential path.)
973fn prefill_batched() -> bool {
974    std::env::var("CMF_PREFILL")
975        .map(|v| v != "seq")
976        .unwrap_or(true)
977}
978
979/// Decide the graph NLL route without conflating graph quality with the
980/// optional native-Metal fused head. A hidden-state graph remains a valid
981/// quality route on Vulkan/Wgpu; only native Metal requires graph logits.
982#[inline]
983fn nll_graph_policy(
984    unmasked: bool,
985    prefer_graph: bool,
986    native_metal: bool,
987) -> (bool, bool) {
988    let graph_quality = unmasked && prefer_graph;
989    let fused_head_quality = graph_quality && native_metal;
990    (graph_quality, fused_head_quality)
991}
992
993/// Input to the layer-major batched span walk: token ids (embeds itself,
994/// full-stack and coordinator prefill) or ready boundary hiddens (the
995/// network worker's side of a split).
996#[derive(Clone, Copy)]
997enum PrefillIn<'a> {
998    Ids(&'a [u32]),
999    Hidden(&'a [f32]),
1000}
1001
1002/// The batched prefill walks `weights.layers`. Architectures that load
1003/// their own stack (gemma-3n's AltUp replicas, DeepSeek-V4's hyper-
1004/// connections) leave that empty and must go position by position — asking
1005/// otherwise indexes an empty vector, which is a panic rather than a
1006/// fallback. Every call site goes through here so the next such
1007/// architecture is one line, not four.
1008impl Pipeline {
1009    fn can_prefill_batched(&self) -> bool {
1010        #[cfg(test)]
1011        let force_serial = self.nll_test_force_serial;
1012        #[cfg(not(test))]
1013        let force_serial = false;
1014        prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1015    }
1016
1017    /// The backend's automatic capacity split for a mapped transformer.
1018    /// Kept as a method so prefill and decode use the exact same boundary.
1019    fn automatic_gpu_prefix(&self) -> Option<usize> {
1020        let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1021        crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1022    }
1023}
1024
1025/// Prefill chunk (positions per batched pass). On macOS the AMX GEMM
1026/// path wants tall panels — M=48 starves the matrix units (ggml uses
1027/// ubatch 512); elsewhere the historical 48 stays. CMF_PREFILL_CHUNK
1028/// overrides. Pub: the network split MUST chunk identically to the
1029/// local path — panel width reorders float accumulation, so a different
1030/// chunk is a different (equally valid) generation.
1031pub fn prefill_chunk() -> usize {
1032    if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
1033        .ok()
1034        .and_then(|v| v.parse::<usize>().ok())
1035    {
1036        return n.max(1);
1037    }
1038    if cfg!(target_os = "macos") {
1039        512
1040    } else if cfg!(target_arch = "aarch64") {
1041        // Mobile: big enough to feed the batched attend (gate b ≥ 32)
1042        // and the blocked SDOT GEMM without the memory of 512.
1043        256
1044    } else {
1045        48
1046    }
1047}
1048
1049/// Number of prompt rows that have a real teacher-forced next-token pair in a
1050/// prefill span.  The final prompt row has no successor token, so it must not
1051/// be handed to the MTP warm-up.  Keeping this arithmetic in one helper makes
1052/// the full-chunk and tail-chunk boundaries explicit for both the graph and
1053/// CPU implementations.
1054#[inline]
1055fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1056    if end <= start || start >= input_len {
1057        return 0;
1058    }
1059    let rows = (end.min(input_len) - start).min(input_len - start);
1060    if end < input_len {
1061        rows
1062    } else {
1063        rows.saturating_sub(1)
1064    }
1065}
1066
1067/// Callback for streaming tokens. Return `false` to cancel.
1068pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1069
1070impl Pipeline {
1071    /// Clear all per-sequence state, including backend device mirrors.
1072    ///
1073    /// The host KV/history buffers are only half of the request lifecycle on
1074    /// wgpu: GDN/O(1) state and cached graph bind groups are keyed by the
1075    /// pipeline id and otherwise survive a pooled request.  Keep every fresh
1076    /// sequence entry point on this one reset path so a new request cannot
1077    /// inherit the prior request's device state.
1078    fn clear_sequence_state(&mut self) {
1079        // a replay still writing the GDN owners must land before they are
1080        // cleared or reallocated (the device holds raw pointers to them)
1081        #[cfg(target_os = "macos")]
1082        let _ = crate::gpu_metal::wait_replay();
1083        self.kv_cache.clear();
1084        self.kv_history.clear();
1085        if let Some(b) = &mut self.dsv41 {
1086            b.3.clear();
1087        }
1088        crate::gpu::graph_kv_reset(self.graph_kv_id);
1089        // MTP is detached from `self` for the duration of generation, so its
1090        // device mirror is not covered by the trunk reset above.  Reset the
1091        // derived id as well: a failed/aborted warm-up must never leave a
1092        // mirror that a later request can mistake for a current MTP cache.
1093        crate::gpu::graph_kv_reset(self.mtp_kv_id());
1094    }
1095
1096    /// Finish a generation lifecycle after the MTP/router owners were
1097    /// detached.  Every terminal path must put those owners back before the
1098    /// pooled pipeline can serve another request.  Graph side channels and
1099    /// device mirrors are cleared on errors and cancellations; a successful
1100    /// generation keeps its decode-ready host cache for KV reuse.
1101    fn finish_generation(
1102        &mut self,
1103        mtp: &mut Option<MtpModule>,
1104        router: &mut Option<crate::swarm::DynRouter>,
1105        clear_sequence: bool,
1106    ) {
1107        // A dynamic route may have switched the overlay before the terminal
1108        // path. Restore the backbone while the detached router is still
1109        // available, because set_active_skill also owns the overlay reset.
1110        if router.is_some() {
1111            let _ = self.set_active_skill(None);
1112        }
1113        // The last speculative round's replay may still be in flight on
1114        // the second queue: whoever reads the host cache after generate()
1115        // returns (session export, the network split's KV wire, a KV
1116        // reuse) must see the final states.
1117        // A replay that failed leaves the GDN owners half-written: fail
1118        // closed and drop the sequence instead of handing the cache on.
1119        #[cfg(target_os = "macos")]
1120        let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1121        if clear_sequence {
1122            self.clear_sequence_state();
1123            if let Some(m) = mtp.as_mut() {
1124                // The MTP owner is detached while generation runs, so the
1125                // trunk reset above cannot clear its host cache.  Drop its
1126                // partial rows before reattaching it to the pooled pipeline;
1127                // the next request must start from the same empty anchor on
1128                // CPU and on the device mirror.
1129                m.kv.clear();
1130            }
1131            if let Some(m) = self.mtp.as_mut() {
1132                // A non-speculative request leaves the configured MTP owner
1133                // attached.  Clear that dormant cache too when a shared
1134                // generation failure/cancellation resets the sequence.
1135                m.kv.clear();
1136            }
1137        }
1138        self.graph_want_logits = false;
1139        self.graph_head_required = false;
1140        self.graph_logits = None;
1141        self.graph_failed
1142            .store(false, std::sync::atomic::Ordering::Relaxed);
1143        self.cancel
1144            .store(false, std::sync::atomic::Ordering::Relaxed);
1145        self.dyn_router = router.take().or(self.dyn_router.take());
1146        self.mtp = mtp.take().or(self.mtp.take());
1147        self.mtp_graph_mode = None;
1148        self.spec_forced = None;
1149    }
1150
1151    /// Consume a graph failure reported by a forward that returns only a
1152    /// hidden vector.  `forward_ids` is a public Result API, so it must not
1153    /// turn the graph's zero hidden sentinel into a valid lm_head result.
1154    fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1155        if self
1156            .graph_failed
1157            .swap(false, std::sync::atomic::Ordering::Relaxed)
1158        {
1159            self.cancel
1160                .store(false, std::sync::atomic::Ordering::Relaxed);
1161            self.clear_sequence_state();
1162            self.graph_logits = None;
1163            self.graph_want_logits = false;
1164            self.graph_head_required = false;
1165            return Err(format!("GPU graph failed during {phase} at position {pos}"));
1166        }
1167        Ok(())
1168    }
1169
1170    #[cfg(target_os = "macos")]
1171    fn fail_metal_graph(&mut self, reason: &str) {
1172        crate::pipeline::METAL_GRAPH_ERRORS
1173            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1174        self.clear_sequence_state();
1175        self.graph_logits = None;
1176        self.graph_failed
1177            .store(true, std::sync::atomic::Ordering::Relaxed);
1178        self.cancel
1179            .store(true, std::sync::atomic::Ordering::Relaxed);
1180        tracing::error!("native Metal TokenGraph failed closed: {reason}");
1181    }
1182
1183    /// Start an NLL/PPL request with all graph side channels in a known
1184    /// state.  A graph failure also raises the cooperative cancel bit; it is
1185    /// consumed here and that graph-induced bit is cleared so an independent
1186    /// request can be reused.  A caller-owned cancellation remains intact.
1187    fn nll_begin(&mut self) -> Result<(), String> {
1188        if self
1189            .graph_failed
1190            .swap(false, std::sync::atomic::Ordering::Relaxed)
1191        {
1192            self.cancel
1193                .store(false, std::sync::atomic::Ordering::Relaxed);
1194            self.clear_sequence_state();
1195            self.graph_logits = None;
1196            self.graph_want_logits = false;
1197            self.graph_head_required = false;
1198            return Err("GPU graph failed before NLL scoring".to_string());
1199        }
1200        self.clear_sequence_state();
1201        self.graph_logits = None;
1202        self.graph_want_logits = false;
1203        self.graph_head_required = false;
1204        Ok(())
1205    }
1206
1207    /// End an NLL/PPL request, including the side channels that are not part
1208    /// of the host KV cache.  This is intentionally explicit instead of
1209    /// relying on a tuple/sentinel return: callers must see every failure.
1210    fn nll_end(&mut self) {
1211        self.clear_sequence_state();
1212        self.graph_logits = None;
1213        self.graph_want_logits = false;
1214        self.graph_head_required = false;
1215        self.graph_failed
1216            .store(false, std::sync::atomic::Ordering::Relaxed);
1217    }
1218
1219    /// Check the graph failure channel at a scoring boundary and leave the
1220    /// pipeline reusable when the device path failed.
1221    fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1222        #[cfg(test)]
1223        if self.nll_test_fail_at == Some(pos) {
1224            self.nll_test_fail_at = None;
1225            self.graph_failed
1226                .store(true, std::sync::atomic::Ordering::Relaxed);
1227            self.cancel
1228                .store(true, std::sync::atomic::Ordering::Relaxed);
1229        }
1230        if self
1231            .graph_failed
1232            .swap(false, std::sync::atomic::Ordering::Relaxed)
1233        {
1234            self.cancel
1235                .store(false, std::sync::atomic::Ordering::Relaxed);
1236            self.clear_sequence_state();
1237            self.graph_logits = None;
1238            self.graph_want_logits = false;
1239            return Err(format!(
1240                "GPU graph failed during NLL {phase} at position {pos}"
1241            ));
1242        }
1243        Ok(())
1244    }
1245
1246    /// Map a virtual layer index to its physical weight index.
1247    /// Looped Transformer (Nanbeige 4.2): 22 physical layers × 2 loops = 44 virtual;
1248    /// virtual layer 23 maps back to physical layer 1 (23 % 22 = 1).
1249    #[inline]
1250    pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1251        virtual_idx % self.physical_layers
1252    }
1253
1254    /// True when `virtual_idx` is the last layer of a loop iteration
1255    /// (used for loop_final_norm insertion).
1256    #[inline]
1257    pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1258        self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1259    }
1260
1261    /// Build a pipeline from parts (used by the loader and tests).
1262    #[allow(clippy::too_many_arguments)]
1263
1264    /// Whole-block q1 token graph on the GPU (macOS/Metal): the run of
1265    /// consecutive q1 layers — GDN *and* full attention — starting at
1266    /// `start` executes as few command buffers as the CPU truly needs.
1267    /// Hidden stays device-resident across every layer; the only syncs
1268    /// are before each CPU attend (it needs q/k/v and owns the KV
1269    /// cache) and the final hidden readback. Recurrent states
1270    /// round-trip through shared memory (the CPU stays their owner, so
1271    /// every other path remains coherent). Returns the first layer
1272    /// index NOT covered (== `start` → refused, caller falls through
1273    /// to the per-layer CPU path).
1274    /// Should prefill run position-by-position through the GPU token
1275    /// graph instead of the batched CPU chunk-GEMM? True for q1 GDN
1276    /// hybrids on native Metal: their chunk prefill is walled by the
1277    /// sequential scalar recurrence, so the graph's decode rate wins.
1278    /// NOT for Looped Transformers, despite the per-chunk loop_final_norm
1279    /// sync: the chunk-GEMM amortizes each weight over the whole chunk,
1280    /// which the per-position graph cannot (Nanbeige 4.2 on M4, 512-token
1281    /// prompt: 85 tok/s chunked vs 14 through the graph).
1282    #[cfg(target_os = "macos")]
1283    fn graph_prefill_preferred(&self) -> bool {
1284        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1285        if !crate::gpu::enabled_here()
1286            || !graph_force
1287            || std::env::var("CMF_GPU_BLOCK")
1288                .map(|v| v == "0")
1289                .unwrap_or(false)
1290            // CMF_PREFILL_GRAPH=0: the chunked prefill (GEMM projections,
1291            // CPU recurrence) instead of the per-position token graph.
1292            || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1293        {
1294            return false;
1295        }
1296        self.weights
1297            .layers
1298            .iter()
1299            .any(|lw| {
1300                matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1301            })
1302    }
1303
1304    #[cfg(not(target_os = "macos"))]
1305    fn graph_prefill_preferred(&self) -> bool {
1306        // Discrete-GPU wgpu whole-token graph: GDN layers carry recurrent state
1307        // (conv ring + delta-rule S) resident on the GPU. A batched CPU prefill
1308        // builds that state on the CPU only, leaving the GPU buffers zeroed at
1309        // decode → garbage. Route GDN-hybrid prefill through the graph one
1310        // position at a time so the resident state is seeded exactly as decode
1311        // will read it. Pure-attention models keep the batched CPU prefill (its
1312        // KV mirror re-syncs from the CPU cache, so no seeding gap).
1313        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1314        if !graph_on || !crate::gpu::enabled_here() {
1315            return false;
1316        }
1317        // The descriptor-aware Prism graph now carries both the FWHT/affine
1318        // transforms and resident GDN state, so it is also the exact prefill
1319        // path for this model.  Keeping it here (rather than falling through
1320        // to the CPU chunk walk) is required for a long prompt to seed the
1321        // same device state that decode consumes.
1322        // O(1) needs the CPU prefill: the q-trace that seals the Nyström
1323        // skeleton is recorded there and nowhere else. The GDN half of
1324        // the hybrid loses nothing — the graph's first decode creates
1325        // its (ring, S) entries seeded from `cpu_state`, the same
1326        // handoff every graph run relies on when the entry is fresh.
1327        // Without this line the two designs collide on hybrids and o1
1328        // never becomes graph-portable: prefill through the graph
1329        // records no trace, so views stay None forever.
1330        if self.o1_active() {
1331            return false;
1332        }
1333        if self
1334            .weights
1335            .layers
1336            .iter()
1337            .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1338        {
1339            return true;
1340        }
1341        // MoE models too: the chunked CPU prefill runs every expert on the
1342        // host (Hy-MT2-30B-A3B on a Xeon: 8 tok/s of ingest against 53 of
1343        // graph decode), while the token graph — and the batched graph under
1344        // CMF_BATCH_K — keep the experts resident. Full attention in the
1345        // graph writes the KV mirror that decode reads, exactly as it does
1346        // for the hybrids' attention layers. Only when the whole stack is
1347        // resident: with a device prefix the per-position walk finishes
1348        // every token on the host, and the chunked prefill (GEMMs on the
1349        // card, the expert loop batched on the host) is the faster ingest
1350        // (the 8 GB ladder point: 7 tok/s chunked against ~1 walked).
1351        self.weights
1352            .layers
1353            .iter()
1354            .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1355            && self.automatic_gpu_prefix().is_none()
1356    }
1357
1358    #[cfg(target_os = "macos")]
1359    fn q1_graph_gpu(
1360        &mut self,
1361        start: usize,
1362        upto: Option<usize>,
1363        position: usize,
1364        h: &mut [f32],
1365    ) -> usize {
1366        let _mt0 = std::time::Instant::now(); // CMF_METAL_HOSTPROF
1367        use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1368        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1369        if self.attn_softcap > 0.0 // capped scores: no graph kernel — CPU path
1370            || !crate::gpu::enabled_here()
1371            || !graph_force
1372            || std::env::var("CMF_GPU_BLOCK")
1373                .map(|v| v == "0")
1374                .unwrap_or(false)
1375        {
1376            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1377                eprintln!(
1378                    "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
1379                    self.attn_softcap > 0.0,
1380                    crate::gpu::enabled_here(),
1381                    graph_force,
1382                );
1383            }
1384            if self.graph_head_required {
1385                self.fail_metal_graph("native graph front gate refused");
1386            }
1387            return start;
1388        }
1389        // The graph encodes SiLU FFN and full-context attention with an
1390        // explicit model scale. Architectures with sliding windows,
1391        // sandwich norms or non-SiLU FFNs still fall back to the CPU path.
1392        if self.swa.is_some()
1393            || self.global_attn.is_some()
1394            || self.attention_heads_per_layer.is_some()
1395            || self.attn_v_norm
1396            || self.weights.layers.iter().any(|lw| {
1397                lw.attn_out_norm.is_some()
1398                    || lw.ffn_out_norm.is_some()
1399                    || lw.layer_scale.is_some()
1400                    || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1401            })
1402        {
1403            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1404                eprintln!(
1405                    "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1406                    self.swa.is_some(),
1407                    self.global_attn.is_some(),
1408                    self.attention_heads_per_layer.is_some(),
1409                    self.attn_v_norm,
1410                    (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1411                );
1412            }
1413            if self.graph_head_required {
1414                self.fail_metal_graph("native graph architecture gate refused");
1415            }
1416            return start;
1417        }
1418        // Looped Transformer: the graph covers ALL loop iterations;
1419        // encode_loop_norm is inserted on-device at each boundary.
1420        let limit = upto
1421            .map(|u| u + 1)
1422            .unwrap_or(self.num_layers)
1423            .min(self.num_layers);
1424
1425        enum Item<'a> {
1426            Gdn {
1427                run: Vec<GdnGpuLayer<'a>>,
1428                first: usize,
1429            },
1430            Attn {
1431                l: AttnGpuLayer<'a>,
1432                li: usize,
1433                q_norm: Option<&'a [f32]>,
1434                k_norm: Option<&'a [f32]>,
1435                output_gate: bool,
1436                bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1437                /// Attend on the device too (no sync): F32 KV, no
1438                /// o1/bias, dims inside the kernels' contract.
1439                full_gpu: bool,
1440            },
1441        }
1442
1443        // Device-attend KERNEL contract, shared by every Full layer. The
1444        // hd>128 default-off POLICY is applied after the scan: it was
1445        // measured on dense models, and a MoE plan inverts it — with the
1446        // experts on device each CPU-attend sandwich costs a
1447        // commit+wait, ~30 submits/token (W2 on M4: 14.7 tok/s
1448        // sandwiched vs 27.1 device-attend vs 18.8 pure CPU).
1449        let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1450        let attend_contract = attend_mode != "0"
1451            && attend_mode != "off"
1452            && self.head_dim % 4 == 0
1453            && self.head_dim <= 256
1454            && self.rotary_dim >= 2
1455            && self.rotary_dim <= self.head_dim
1456            && (self.rotary_dim / 2) % 32 == 0
1457            && self.num_kv_heads > 0
1458            && self.num_heads % self.num_kv_heads == 0;
1459
1460        let mut plan: Vec<Item> = Vec::new();
1461        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1462        // Break-reason diagnostics ride the same env as the plan summary.
1463        let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1464        let mut scan = start;
1465        while scan < limit {
1466            let lw = &self.weights.layers[self.phys_layer(scan)];
1467            let ffn = match &lw.ffn {
1468                FfnKind::Dense(d) if d.segs.is_empty() => {
1469                    let (Some(g), Some(u), Some(dn)) = (
1470                        d.gate_proj.metal_graph_parts(),
1471                        d.up_proj.metal_graph_parts(),
1472                        d.down_proj.metal_graph_parts(),
1473                    ) else {
1474                        if block_diag {
1475                            eprintln!(
1476                                "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1477                            );
1478                        }
1479                        break;
1480                    };
1481                    MetalFfn::Dense {
1482                        gate: g,
1483                        up: u,
1484                        down: dn,
1485                    }
1486                }
1487                FfnKind::Moe(m) => {
1488                    let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1489                        if block_diag {
1490                            eprintln!(
1491                                "block-graph: L{scan} MoE outside the graph contract — run ends"
1492                            );
1493                        }
1494                        break;
1495                    };
1496                    if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1497                        model_ref.get_or_insert_with(|| model.clone());
1498                    }
1499                    MetalFfn::Moe(moe)
1500                }
1501                _ => {
1502                    if block_diag {
1503                        eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1504                    }
1505                    break;
1506                }
1507            };
1508            match &lw.attn {
1509                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1510                    let parts = (
1511                        w.in_proj_qkv.metal_graph_parts(),
1512                        w.in_proj_z.metal_graph_parts(),
1513                        w.in_proj_a.f32_parts(),
1514                        w.in_proj_b.f32_parts(),
1515                        w.out_proj.metal_graph_parts(),
1516                    );
1517                    let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1518                        if block_diag {
1519                            eprintln!(
1520                                "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1521                                w.in_proj_qkv.metal_graph_parts().is_some(),
1522                                w.in_proj_z.metal_graph_parts().is_some(),
1523                                w.in_proj_a.f32_parts().is_some(),
1524                                w.in_proj_b.f32_parts().is_some(),
1525                                w.out_proj.metal_graph_parts().is_some(),
1526                            );
1527                        }
1528                        break;
1529                    };
1530                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1531                        model_ref.get_or_insert_with(|| model.clone());
1532                    }
1533                    let gl = GdnGpuLayer {
1534                        attn_norm: &lw.input_norm,
1535                        post_norm: &lw.post_norm,
1536                        qkv,
1537                        z,
1538                        a,
1539                        b,
1540                        out,
1541                        ffn,
1542                        conv1d: &w.conv1d,
1543                        a_log: &w.a_log,
1544                        dt_bias: &w.dt_bias,
1545                        gnorm: &w.norm,
1546                    };
1547                    match plan.last_mut() {
1548                        Some(Item::Gdn { run, .. }) => run.push(gl),
1549                        _ => plan.push(Item::Gdn {
1550                            run: vec![gl],
1551                            first: scan,
1552                        }),
1553                    }
1554                }
1555                AttnKind::Full {
1556                    wq,
1557                    wk,
1558                    wv,
1559                    wo,
1560                    q_norm,
1561                    k_norm,
1562                    output_gate,
1563                    softplus_gate: None,
1564                    bias,
1565                } if !self.kv_cache.layers[scan].o1_sealed()
1566                    // Sealed o1 stays plannable when the Metal o1 port
1567                    // is on: full_gpu attends through the device state,
1568                    // and any refusal falls to the sandwich, whose CPU
1569                    // core routes sealed layers through the nystrom step.
1570                    || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1571                {
1572                    let parts = (
1573                        wq.metal_graph_parts(),
1574                        wk.metal_graph_parts(),
1575                        wv.metal_graph_parts(),
1576                        wo.metal_graph_parts(),
1577                    );
1578                    let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1579                        break;
1580                    };
1581                    if let QTensor::Mapped { model, .. } = wq {
1582                        model_ref.get_or_insert_with(|| model.clone());
1583                    }
1584                    let cache = &self.kv_cache.layers[scan];
1585                    // O(1) layer on Metal: the device attends through the
1586                    // sealed Nystrom state (opt-in while the port proves
1587                    // itself). Unsealed -> sandwich path = the CPU o1 step.
1588                    let o1_metal = cache.o1.is_some()
1589                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1590                        && cache.o1_views().is_some();
1591                    let full_gpu = attend_contract
1592                        && cache.mode == crate::kv_cache::KvMode::F32
1593                        && (cache.o1.is_none() || o1_metal)
1594                        && bias.is_none()
1595                        && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1596                        && pk.1 == self.num_kv_heads * self.head_dim
1597                        && pv.1 == self.num_kv_heads * self.head_dim
1598                        && po.2 == self.num_heads * self.head_dim;
1599                    plan.push(Item::Attn {
1600                        l: AttnGpuLayer {
1601                            attn_norm: &lw.input_norm,
1602                            post_norm: &lw.post_norm,
1603                            wq: pq,
1604                            wk: pk,
1605                            wv: pv,
1606                            wo: po,
1607                            ffn,
1608                        },
1609                        li: scan,
1610                        q_norm: q_norm.as_deref(),
1611                        k_norm: k_norm.as_deref(),
1612                        output_gate: *output_gate,
1613                        bias: bias
1614                            .as_ref()
1615                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1616                        full_gpu,
1617                    });
1618                }
1619                _ => break,
1620            }
1621            scan += 1;
1622        }
1623        let Some(model) = model_ref else {
1624            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1625                eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1626            }
1627            if self.graph_head_required {
1628                self.fail_metal_graph("native graph has no mapped model reference");
1629            }
1630            return start;
1631        };
1632        if plan.is_empty() {
1633            if std::env::var("CMF_GRAPH_DBG").is_ok() {
1634                eprintln!("q1-graph: empty plan at layer {start}");
1635            }
1636            if self.graph_head_required {
1637                self.fail_metal_graph("native graph plan is empty");
1638            }
1639            return start;
1640        }
1641        let has_moe = plan.iter().any(|it| match it {
1642            Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1643            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1644        });
1645        let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1646        let dev_attend = attend_contract
1647            && (self.head_dim <= 128
1648                || has_moe
1649                // A GDN hybrid attends on a quarter of its layers: the
1650                // hd>128 caution was measured on pure-dense models where
1651                // gqa_attend dominates, and on Qwen3.8-27B (hd 256, 48
1652                // GDN + 16 attn) the sandwich costs 2x the whole decode
1653                // (1.2 vs 2.21 tok/s measured before the arena fix).
1654                || (self.head_dim <= 256 && has_gdn)
1655                || attend_mode == "force"
1656                || attend_mode == "256");
1657        if !dev_attend {
1658            for it in &mut plan {
1659                if let Item::Attn { li, full_gpu, .. } = it {
1660                    // The hd>128 policy is about gqa_attend; an o1 layer
1661                    // attends through its own kernel set.
1662                    let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1663                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1664                    if !keep_o1 {
1665                        *full_gpu = false;
1666                    }
1667                }
1668            }
1669        }
1670        if std::env::var("CMF_GRAPH_DBG").is_ok() {
1671            use std::sync::atomic::{AtomicBool, Ordering};
1672            static SAID: AtomicBool = AtomicBool::new(false);
1673            if !SAID.swap(true, Ordering::Relaxed) {
1674                let fg = plan
1675                    .iter()
1676                    .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1677                    .count();
1678                let att = plan
1679                    .iter()
1680                    .filter(|it| matches!(it, Item::Attn { .. }))
1681                    .count();
1682                eprintln!(
1683                    "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1684                    plan.len(),
1685                    self.head_dim,
1686                    self.rotary_dim,
1687                    self.num_kv_heads,
1688                    self.num_heads,
1689                );
1690            }
1691        }
1692        let dims = GraphDims {
1693            hidden: self.hidden_size,
1694            eps: self.rms_eps as f32,
1695            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1696        };
1697        let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1698            if self.graph_head_required {
1699                self.fail_metal_graph("native TokenGraph allocation refused");
1700            }
1701            return start;
1702        };
1703        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1704            nv: cfg.num_v_heads,
1705            nk: cfg.num_k_heads,
1706            dk: cfg.key_head_dim,
1707            dv: cfg.value_head_dim,
1708            kk: cfg.conv_kernel,
1709            hidden: self.hidden_size,
1710            inter: self.intermediate_size,
1711            c_dim: cfg.conv_dim(),
1712            eps: cfg.rms_eps as f32,
1713            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1714        });
1715        // Validate the whole plan BEFORE encoding anything: after the
1716        // first sync a refused layer would leave the token
1717        // half-executed, so truncate to the provably encodable prefix.
1718        let mut valid = 0usize;
1719        let mut end = start;
1720        crate::gpu::stageprof(1, _mt0.elapsed()); // конец планирования
1721        if std::env::var("CMF_PLAN_DUMP").is_ok() {
1722            static ONCE: std::sync::Once = std::sync::Once::new();
1723            ONCE.call_once(|| {
1724                for it in &plan {
1725                    match it {
1726                        Item::Gdn { first, run } => {
1727                            eprintln!("plan: Gdn first={first} len={}", run.len())
1728                        }
1729                        Item::Attn { li, full_gpu, .. } => {
1730                            eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1731                        }
1732                    }
1733                }
1734            });
1735        }
1736        for item in &plan {
1737            let ok = match item {
1738                Item::Gdn { run, .. } => gcfg
1739                    .as_ref()
1740                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1741                    .unwrap_or(false),
1742                Item::Attn { l, .. } => graph.attn_ok(l),
1743            };
1744            if !ok {
1745                if block_diag {
1746                    eprintln!(
1747                        "block-graph: plan item {} ({}) failed graph preflight",
1748                        valid,
1749                        match item {
1750                            Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1751                            Item::Attn { li, .. } => format!("Attn L{li}"),
1752                        }
1753                    );
1754                }
1755                break;
1756            }
1757            valid += 1;
1758            end += match item {
1759                Item::Gdn { run, .. } => run.len(),
1760                Item::Attn { .. } => 1,
1761            };
1762        }
1763        plan.truncate(valid);
1764        if plan.is_empty() {
1765            if self.graph_head_required {
1766                self.fail_metal_graph("native graph preflight produced no valid items");
1767            }
1768            return start;
1769        }
1770
1771        if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
1772            self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
1773            return start;
1774        }
1775
1776        let inv_freq = self.inv_freq.clone();
1777        let pool = self.pool.clone();
1778        let (nh, nkv, hd, hs, rd, eps) = (
1779            self.num_heads,
1780            self.num_kv_heads,
1781            self.head_dim,
1782            self.hidden_size,
1783            self.rotary_dim,
1784            self.rms_eps,
1785        );
1786        let norm_style = self.norm_style;
1787        let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1788        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1789        let kv_id = self.graph_kv_id;
1790        // GDN runs whose states await readback after the next sync
1791        // (device-attended layers add no sync, so several may stack).
1792        let mut pending: Vec<(usize, usize)> = Vec::new();
1793        // Device-attended layers: their K/V/imp are pulled from the
1794        // mirror after the final sync.
1795        let mut dev_attn: Vec<usize> = Vec::new();
1796        for item in &plan {
1797            let _xt0 = std::time::Instant::now();
1798            let _xkind: u32 = match item {
1799                Item::Gdn { .. } => 2,
1800                Item::Attn { .. } => 3,
1801            };
1802            // Looped Transformer: insert on-device norm at loop boundaries.
1803            if self.loop_final_norm {
1804                let item_start = match item {
1805                    Item::Gdn { first, .. } => *first,
1806                    Item::Attn { li, .. } => *li,
1807                };
1808                if item_start > start && self.is_loop_end(item_start - 1) {
1809                    graph.encode_loop_norm(&self.weights.final_norm);
1810                }
1811            }
1812            match item {
1813                Item::Gdn { run, first } => {
1814                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1815                        if l.linear_state.len() != want {
1816                            l.linear_state = vec![0f32; want];
1817                        }
1818                    }
1819                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1820                        .iter()
1821                        .map(|l| l.linear_state.as_slice())
1822                        .collect();
1823                    let _ig = std::time::Instant::now();
1824                    if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1825                        // Unreachable: the plan was validated above.
1826                        tracing::error!("q1 graph: GDN run refused after validation");
1827                        return start;
1828                    }
1829                    // Early commit: the GPU starts the run while the
1830                    // CPU encodes the next layer (nothing to wait on).
1831                    graph.commit_kind = 2;
1832                    graph.commit();
1833                    crate::gpu::stageprof(0, _ig.elapsed());
1834                    pending.push((*first, run.len()));
1835                }
1836                Item::Attn {
1837                    l,
1838                    li,
1839                    q_norm,
1840                    k_norm,
1841                    output_gate,
1842                    bias,
1843                    full_gpu,
1844                } => {
1845                    let _ia = std::time::Instant::now();
1846                    // ── Fully device-resident attention: no sync at all.
1847                    if *full_gpu {
1848                        let cache = &self.kv_cache.layers[*li];
1849                        let o1p = if cache.o1.is_some() {
1850                            match cache.o1_views() {
1851                                Some(views) => Some(crate::gpu::O1AttnParams {
1852                                    views,
1853                                    epoch: self.o1_epoch,
1854                                }),
1855                                // Sealed state gone mid-run: sandwich.
1856                                None => None,
1857                            }
1858                        } else {
1859                            None
1860                        };
1861                        let o1_layer = cache.o1.is_some();
1862                        if o1_layer && o1p.is_none() {
1863                            // fall to the sandwich (CPU o1 step)
1864                        }
1865                        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1866                        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1867                        let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1868                        let p = crate::gpu::AttnDeviceParams {
1869                            kv_id,
1870                            layer: *li,
1871                            nh,
1872                            nkv,
1873                            hd,
1874                            rd,
1875                            position,
1876                            scale: self.attn_scale,
1877                            eps: eps as f32,
1878                            gemma,
1879                            late_qk_norm: self.qk_norm_after_rope,
1880                            output_gate: *output_gate,
1881                            q_norm: *q_norm,
1882                            k_norm: *k_norm,
1883                            inv_freq: &inv_freq,
1884                            cpu_k,
1885                            cpu_v,
1886                            cpu_stored,
1887                            o1: o1p,
1888                        };
1889                        let o1_bad = o1_layer && p.o1.is_none();
1890                        if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1891                        {
1892                            // o1 layers leave no mirror row to pull.
1893                            if p.o1.is_none() {
1894                                dev_attn.push(*li);
1895                            }
1896                            graph.commit_kind = 3;
1897                            graph.commit();
1898                            // The footer below is skipped by `continue`:
1899                            // account the device-attn item here or its
1900                            // cost hides from the stage profile entirely.
1901                            crate::gpu::stageprof(_xkind, _xt0.elapsed());
1902                            continue;
1903                        }
1904                        // Mirror refused (nothing encoded) → sandwich.
1905                    }
1906                    graph.encode_attn_prefix(l);
1907                    if let Err(err) = graph.sync_checked() {
1908                        self.fail_metal_graph(&err);
1909                        return start;
1910                    }
1911                    if !pending.is_empty() {
1912                        let idxs: Vec<usize> =
1913                            pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1914                        let mut outs: Vec<&mut [f32]> = self
1915                            .kv_cache
1916                            .layers
1917                            .iter_mut()
1918                            .enumerate()
1919                            .filter(|(i, _)| idxs.binary_search(i).is_ok())
1920                            .map(|(_, s)| s.linear_state.as_mut_slice())
1921                            .collect();
1922                        graph.read_states(&mut outs);
1923                    }
1924                    let mut q_raw = attention::take_buf(l.wq.1);
1925                    let mut k = attention::take_buf(l.wk.1);
1926                    let mut v = attention::take_buf(l.wv.1);
1927                    graph.read_qkv(&mut q_raw, &mut k, &mut v);
1928                    let cfg = QwenAttnCfg {
1929                        num_heads: nh,
1930                        num_kv_heads: nkv,
1931                        head_dim: hd,
1932                        hidden_size: hs,
1933                        position,
1934                        inv_freq: &inv_freq,
1935                        rotary_dim: rd,
1936                        scale: self.attn_scale,
1937                        softcap: self.attn_softcap,
1938                        window: None,
1939                        v_norm: false,
1940                        qk_norm_after_rope: self.qk_norm_after_rope,
1941                        q_norm: *q_norm,
1942                        k_norm: *k_norm,
1943                        output_gate: *output_gate,
1944                        softplus_gate: None,
1945                        rope_scale: 1.0,
1946                        bias: *bias,
1947                        rms_eps: eps,
1948                        norm_style,
1949                        pool: pool.as_deref(),
1950                    };
1951                    // CMF_ATTN_ORACLE=1: diff the device attend against
1952                    // this CPU attend on identical inputs (bring-up).
1953                    let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1954                        || std::env::var("CMF_ATTN_DUMP").is_ok();
1955                    let _ = full_gpu;
1956                    let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1957                    let mut ao = attention::qwen_attention_core(
1958                        q_raw,
1959                        k,
1960                        v,
1961                        &mut self.kv_cache.layers[*li],
1962                        &cfg,
1963                    );
1964                    // CMF_ATTN_DUMP=<dir>: this token's rope'd Q and the layer's whole
1965                    // K/V cache as raw f32 (offline attention-statistics probes:
1966                    // block bounds, mass concentration). Needs CMF_GPU_ATTEND=0.
1967                    if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1968                        if let Some((qr0, k0, v0)) = oracle_in.clone() {
1969                            let (cq, _cg, _ck, _cv) =
1970                                attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1971                            let cache = &self.kv_cache.layers[*li];
1972                            let n = cache.head_keys(0).len() / hd;
1973                            let mut bytes: Vec<u8> = Vec::new();
1974                            for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1975                                bytes.extend_from_slice(&v.to_le_bytes());
1976                            }
1977                            for v in &cq {
1978                                bytes.extend_from_slice(&v.to_le_bytes());
1979                            }
1980                            for g in 0..nkv {
1981                                for v in cache.head_keys(g) {
1982                                    bytes.extend_from_slice(&v.to_le_bytes());
1983                                }
1984                            }
1985                            for g in 0..nkv {
1986                                for v in cache.head_values(g) {
1987                                    bytes.extend_from_slice(&v.to_le_bytes());
1988                                }
1989                            }
1990                            let _ =
1991                                std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1992                        }
1993                    }
1994                    if let Some((qr0, k0, v0)) =
1995                        oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
1996                    {
1997                        let (cq, _cg, ck, cv) =
1998                            attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1999                        let mut h_now = vec![0f32; hs];
2000                        graph.read_h(&mut h_now);
2001                        let cache = &self.kv_cache.layers[*li];
2002                        let n_after = cache.head_keys(0).len() / hd;
2003                        // A sealed O(1) cache may have no dense current-row
2004                        // entry. The oracle is a debug probe, so let it see
2005                        // zero stored exact rows instead of underflowing.
2006                        let stored = n_after.saturating_sub(1);
2007                        let cpu_k: Vec<&[f32]> = (0..nkv)
2008                            .map(|g| &cache.head_keys(g)[..stored * hd])
2009                            .collect();
2010                        let cpu_v: Vec<&[f32]> = (0..nkv)
2011                            .map(|g| &cache.head_values(g)[..stored * hd])
2012                            .collect();
2013                        let p = crate::gpu::AttnDeviceParams {
2014                            kv_id,
2015                            layer: *li,
2016                            nh,
2017                            nkv,
2018                            hd,
2019                            rd,
2020                            position,
2021                            scale: self.attn_scale,
2022                            eps: eps as f32,
2023                            gemma,
2024                            late_qk_norm: self.qk_norm_after_rope,
2025                            output_gate: *output_gate,
2026                            q_norm: *q_norm,
2027                            k_norm: *k_norm,
2028                            inv_freq: &inv_freq,
2029                            cpu_k,
2030                            cpu_v,
2031                            cpu_stored: stored,
2032                            o1: None,
2033                        };
2034                        if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
2035                            let md = |a: &[f32], b: &[f32]| {
2036                                a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
2037                            };
2038                            let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
2039                            eprintln!(
2040                                "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}",
2041                                nn(&cq),
2042                                md(&cq, &dq),
2043                                nn(&ck),
2044                                md(&ck, &dk),
2045                                nn(&cv),
2046                                md(&cv, &dv),
2047                                nn(&ao),
2048                                md(&ao, &dao)
2049                            );
2050                        } else {
2051                            eprintln!("attn-oracle L{li}: device probe declined");
2052                        }
2053                    }
2054                    graph.encode_attn_suffix(l, &ao);
2055                    // Early commit: the GPU starts O+FFN while the CPU
2056                    // encodes the following GDN run / attention prefix.
2057                    graph.commit();
2058                    attention::recycle_buf(&mut ao);
2059                }
2060            }
2061
2062            crate::gpu::stageprof(_xkind, _xt0.elapsed());
2063        }
2064        // Ride the final norm + lm_head in the same command buffer when
2065        // this run reaches the model's end and the caller wants logits:
2066        // the separate per-op lm_head submit (a full round trip) folds
2067        // into the sync that already happens here.
2068        let mut lm_rows = None;
2069        if self.graph_want_logits
2070            && upto.is_none()
2071            && end == self.num_layers
2072            && std::env::var("CMF_GPU_LMHEAD")
2073                .map(|v| v != "0")
2074                .unwrap_or(true)
2075        {
2076            if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
2077                if graph.lm_head_ok(lm) {
2078                    graph.encode_lm_head(&self.weights.final_norm, lm);
2079                    lm_rows = Some(lm.1);
2080                }
2081            }
2082        }
2083        if self.graph_head_required && lm_rows.is_none() {
2084            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2085            self.fail_metal_graph("fused graph head was requested but not encodable");
2086            return start;
2087        }
2088        let _sy0 = std::time::Instant::now();
2089        if let Err(err) = graph.sync_checked() {
2090            self.fail_metal_graph(&err);
2091            return start;
2092        }
2093        let _rs0 = std::time::Instant::now();
2094        if !pending.is_empty() {
2095            let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
2096            let mut outs: Vec<&mut [f32]> = self
2097                .kv_cache
2098                .layers
2099                .iter_mut()
2100                .enumerate()
2101                .filter(|(i, _)| idxs.binary_search(i).is_ok())
2102                .map(|(_, s)| s.linear_state.as_mut_slice())
2103                .collect();
2104            graph.read_states(&mut outs);
2105        }
2106        if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
2107            use std::sync::atomic::{AtomicU64, Ordering};
2108            static SY: AtomicU64 = AtomicU64::new(0);
2109            static RS: AtomicU64 = AtomicU64::new(0);
2110            static N: AtomicU64 = AtomicU64::new(0);
2111            SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
2112            RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
2113            let n = N.fetch_add(1, Ordering::Relaxed) + 1;
2114            if n % 100 == 0 {
2115                eprintln!(
2116                    "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
2117                    SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
2118                    RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
2119                );
2120            }
2121        }
2122        if let Some(rows) = lm_rows {
2123            crate::gpu::hostprof_encode_done(_mt0);
2124            let mut lg = attention::take_buf(rows.min(self.vocab_size));
2125            graph.read_logits(&mut lg);
2126            crate::gpu::hostprof_total(_mt0);
2127            lg.resize(self.vocab_size, 0.0);
2128            if let Some(c) = self.final_softcap {
2129                for l in lg.iter_mut() {
2130                    *l = c * (*l / c).tanh();
2131                }
2132            }
2133            self.graph_logits = Some(lg);
2134        }
2135        graph.read_h(h);
2136        if self.graph_head_required && self.graph_logits.is_none() {
2137            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2138            self.fail_metal_graph("fused graph head completed without logits readback");
2139            return start;
2140        }
2141        METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2142        METAL_GRAPH_LAYERS.fetch_add(
2143            end.saturating_sub(start) as u64,
2144            std::sync::atomic::Ordering::Relaxed,
2145        );
2146        if self.graph_head_required {
2147            METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2148        }
2149        // Device-attended layers: replay the CPU bookkeeping — append
2150        // the mirror's new K/V row (rope'd on the GPU) into the owner
2151        // cache, then bank this token's attention-importance mass.
2152        for li in dev_attn {
2153            let mut krow = attention::take_buf(nkv * hd);
2154            let mut vrow = attention::take_buf(nkv * hd);
2155            if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
2156                let cache = &mut self.kv_cache.layers[li];
2157                cache.append(&krow, &vrow, &[]);
2158                let n = cache.seq_len;
2159                let mut imp = attention::take_buf(n);
2160                crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
2161                cache.accumulate_imp(&imp);
2162                attention::recycle_buf(&mut imp);
2163            }
2164            attention::recycle_buf(&mut krow);
2165            attention::recycle_buf(&mut vrow);
2166        }
2167        end
2168    }
2169
2170    pub fn new(
2171        tokenizer: Tokenizer,
2172        weights: PipelineWeights,
2173        hidden_size: usize,
2174        intermediate_size: usize,
2175        num_heads: usize,
2176        num_kv_heads: usize,
2177        head_dim: usize,
2178        num_layers: usize,
2179        physical_layers: usize,
2180        loop_final_norm: bool,
2181        vocab_size: usize,
2182        rms_eps: f64,
2183        rope_base: f32,
2184        norm_style: NormStyle,
2185        max_seq_len: usize,
2186        sampler_config: SamplerConfig,
2187    ) -> Self {
2188        let rng = match sampler_config.seed {
2189            Some(s) => SplitMix64::new(s),
2190            None => SplitMix64::from_entropy(),
2191        };
2192        let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
2193        let pool = Pool::from_env();
2194        if let Some(p) = &pool {
2195            tracing::info!("worker pool: {} threads", p.n_workers());
2196        }
2197        Self {
2198            gpu_plan: None,
2199            tokenizer: std::sync::Arc::new(tokenizer),
2200            kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
2201            sampler_config,
2202            weights,
2203            hidden_size,
2204            intermediate_size,
2205            num_heads,
2206            num_kv_heads,
2207            head_dim,
2208            num_layers,
2209            physical_layers,
2210            loop_final_norm,
2211            vocab_size,
2212            rms_eps,
2213            rope_base,
2214            norm_style,
2215            rotary_dim: head_dim,
2216            attention_heads_per_layer: None,
2217            vmf_cfg: None,
2218            gdn_cfg: None,
2219            kda_cfg: None,
2220            g3n: None,
2221            dsv4: None,
2222            dsv41: None,
2223            dsv41_vision: None,
2224            dsv41_prefill: None,
2225            qwen4_exp: None,
2226            dsv4_mtp: Vec::new(),
2227            dspark: None,
2228            dspark_pending: Vec::new(),
2229            dspark_hist: Vec::new(),
2230            dspark_real: Vec::new(),
2231            dspark_trunk_picks: Vec::new(),
2232            dspark_exp: Vec::new(),
2233            dspark_draft_ns: 0,
2234            logit_multiplier: None,
2235            cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2236            graph_failed: std::sync::atomic::AtomicBool::new(false),
2237            kv_history: Vec::new(),
2238            short_conv_cfg: None,
2239            mtp: None,
2240            speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
2241            ignore_eos: false,
2242            draft_full_streak: 0,
2243            spec_k_adapt: None,
2244            spec_acc_ewma: 0.7,
2245            rng,
2246            sampler_scratch: SamplerScratch::default(),
2247            spec_forced: None,
2248            spec_q: Vec::new(),
2249            spec_p: Vec::new(),
2250            spec_res: Vec::new(),
2251            spec_qs: Vec::new(),
2252            spec_ps: Vec::new(),
2253            spec_ress: Vec::new(),
2254            mtp_graph_mode: None,
2255            #[cfg(target_os = "macos")]
2256            metal_verify: None,
2257            inv_freq,
2258            ws: ForwardScratch::new(hidden_size),
2259            pool,
2260            model: None,
2261            dyn_force_f32: false,
2262            dyn_skill_layers: Vec::new(),
2263            dyn_active: None,
2264            dyn_blend_loaded: false,
2265            dyn_phi_layer: None,
2266            dyn_phi_ema: Vec::new(),
2267            dyn_phi_seen: 0,
2268            dyn_router: None,
2269            o1_cfg: None,
2270            o1_epoch: 0,
2271            o1_flags: Vec::new(),
2272            trace: false,
2273            calib_temp: 1.0,
2274            confidence_on: true,
2275            embed_multiplier: 1.0,
2276            attn_scale: 1.0 / (head_dim as f32).sqrt(),
2277            swa: None,
2278            sliding_layers: None,
2279            inv_freq_local: None,
2280            rotary_dim_local: None,
2281            rope_scale: 1.0,
2282            rope_scale_local: 1.0,
2283            global_attn: None,
2284            inv_freq_global: None,
2285            attn_v_norm: false,
2286            qk_norm_after_rope: false,
2287            final_softcap: None,
2288            head_clusters: None,
2289            attn_softcap: 0.0,
2290            graph_want_logits: false,
2291            graph_head_required: false,
2292            graph_logits: None,
2293            graph_kv_id: {
2294                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2295                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2296            },
2297            #[cfg(test)]
2298            nll_test_fail_at: None,
2299            #[cfg(test)]
2300            nll_test_force_serial: false,
2301        }
2302    }
2303
2304    /// Enable/disable per-layer O(1) Nyström attention. Only Full
2305    /// layers are eligible (a linear layer keeps its own operator).
2306    /// Applies to generation (`generate*`/`forward_ids`): the prompt
2307    /// pass stays exact, then the state seals after prefill or at the
2308    /// deferred skeleton-safe boundary for short prompts; decode runs on
2309    /// the O(1) state. Teacher-forced scoring (`ppl_ids`) intentionally
2310    /// stays exact.
2311    pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
2312        if let Some(c) = &cfg {
2313            if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
2314                tracing::error!(
2315                    "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
2316                    c.w,
2317                    c.sink
2318                );
2319                self.o1_flags.clear();
2320                self.o1_cfg = None;
2321                return;
2322            }
2323        }
2324        self.o1_flags = match &cfg {
2325            Some(c) => {
2326                let mut flags = c.layer_flags(self.num_layers);
2327                for (li, f) in flags.iter_mut().enumerate() {
2328                    if *f
2329                        && !matches!(
2330                            self.weights.layers[self.phys_layer(li)].attn,
2331                            AttnKind::Full { .. }
2332                        )
2333                    {
2334                        *f = false;
2335                    }
2336                }
2337                flags
2338            }
2339            None => Vec::new(),
2340        };
2341        if let Some(c) = &cfg {
2342            let n = self.o1_flags.iter().filter(|&&f| f).count();
2343            tracing::info!(
2344                "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2345                self.num_layers,
2346                c.m,
2347                c.w,
2348                c.sink,
2349                c.rect
2350            );
2351        }
2352        self.o1_cfg = cfg;
2353    }
2354
2355    /// True when at least one layer runs the O(1) kernel.
2356    pub fn o1_active(&self) -> bool {
2357        self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2358    }
2359
2360    /// Whether generation's prompt ingest is routed through the whole-token
2361    /// graph.  The bench uses this to label the measured generation prefill
2362    /// honestly; keep the predicate in Pipeline so CLI labels cannot drift
2363    /// from the production route.
2364    /// Positions per batched-graph submit for the prompt: `CMF_BATCH_K`
2365    /// when set (0 = one position at a time through the token graph),
2366    /// otherwise 32 on a discrete card whose prompt takes the graph route.
2367    /// The batched graph read a 2048-token prompt at 53 tok/s against 28.5
2368    /// one position at a time on an RTX PRO 4000 (Qwen3.8-27B q4tp: TTFT
2369    /// 39 s against 72), and its states are the speculative verify's,
2370    /// measured identical to the plain path. macOS keeps its own arm.
2371    pub fn generation_batch_k(&self) -> usize {
2372        if let Some(k) = std::env::var("CMF_BATCH_K")
2373            .ok()
2374            .and_then(|v| v.parse::<usize>().ok())
2375        {
2376            return k;
2377        }
2378        #[cfg(not(target_os = "macos"))]
2379        if self.graph_prefill_preferred() && !self.o1_active() {
2380            return 32;
2381        }
2382        0
2383    }
2384
2385    pub fn generation_graph_prefill(&self) -> bool {
2386        let graph = self.graph_prefill_preferred();
2387        // On wgpu, an active MTP head now consumes the trunk's graph batches
2388        // and warms its own block from those returned rows.  The selected
2389        // generation measurement is therefore the batched path, even though
2390        // the underlying GDN model still satisfies the graph-prefill
2391        // predicate.  Keep the CLI label tied to the actual route.  Native
2392        // Metal has a separate prefill-batch arm and retains its historical
2393        // label here.
2394        // A batched prompt (`generation_batch_k` > 0) is the batched graph
2395        // for every model on the graph route, not only those with an MTP
2396        // head — the label follows the route.
2397        #[cfg(not(target_os = "macos"))]
2398        if graph
2399            && self.generation_batch_k() > 0
2400            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2401        {
2402            return false;
2403        }
2404        graph
2405    }
2406
2407    /// Device-side O(1) mirrors currently uploaded for this pipeline's
2408    /// sequence.  The count/bytes are zero before seal or after a fresh
2409    /// reset; callers use this to distinguish logical host state from the
2410    /// GPU allocation that actually serves decode.
2411    pub fn o1_device_stats(&self) -> (usize, u64) {
2412        crate::gpu::o1_device_stats(self.graph_kv_id)
2413    }
2414
2415    /// Arm query collection on the o1 layers (fresh prompt pass).
2416    /// Reset the o1 layers to Collecting for a fresh sequence. Pub for the
2417    /// network split: each side runs the o1 lifecycle over ITS OWN layers
2418    /// (begin before prefill, seal at the prefill barrier).
2419    pub fn o1_begin(&mut self) {
2420        self.o1_begin_with_prefix(None);
2421    }
2422
2423    /// Arm collection and optionally request a positive calibration prefix.
2424    /// The effective barrier is always at least the skeleton-safe floor, so
2425    /// a short requested prefix cannot create an exact-only runtime state.
2426    pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2427        if let Some(c) = &self.o1_cfg {
2428            let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2429            let boundary = requested_prefix.map(|p| {
2430                p.max(
2431                    crate::nystrom::o1_deferred_boundary(w, sink)
2432                        .expect("o1 config boundary validated in set_o1"),
2433                )
2434            });
2435            for (li, &f) in self.o1_flags.iter().enumerate() {
2436                if f {
2437                    self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2438                }
2439            }
2440        }
2441    }
2442
2443    /// Effective deferred boundary for a positive prefix request.
2444    fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2445        self.o1_cfg.as_ref().and_then(|c| {
2446            crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2447                .map(|floor| requested_prefix.max(floor))
2448        })
2449    }
2450
2451    fn o1_note_transition(&mut self) {
2452        // Drain every layer's one-shot bit before publishing one pipeline
2453        // epoch. `any()` would short-circuit on the first layer and leak the
2454        // remaining bits into later forwards, causing one epoch per layer.
2455        let mut transitioned = false;
2456        for (li, &flagged) in self.o1_flags.iter().enumerate() {
2457            if flagged {
2458                transitioned |= self.kv_cache.layers[li].take_o1_transition();
2459            }
2460        }
2461        if transitioned {
2462            self.o1_epoch = self.o1_epoch.wrapping_add(1);
2463        }
2464    }
2465
2466    fn o1_pending(&self) -> bool {
2467        self.o1_flags.iter().enumerate().any(|(li, &f)| {
2468            f && self.kv_cache.layers[li].seq_len > 0
2469                && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2470        })
2471    }
2472
2473    fn o1_fail(&mut self, err: String) {
2474        tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2475        self.clear_sequence_state();
2476        self.graph_failed
2477            .store(true, std::sync::atomic::Ordering::Relaxed);
2478        self.cancel
2479            .store(true, std::sync::atomic::Ordering::Relaxed);
2480    }
2481
2482    /// Seal participating layers while retaining the exact state when the
2483    /// prompt is below the deferred boundary. A split worker may have
2484    /// collecting layers outside its owned span; zero-depth layers remain
2485    /// armed and are intentionally skipped until their peer runs them.
2486    pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2487        if self.o1_cfg.is_none() {
2488            return Ok(false);
2489        }
2490        let mut participating = false;
2491        for li in 0..self.num_layers {
2492            if !self.o1_flags.get(li).copied().unwrap_or(false) {
2493                continue;
2494            }
2495            if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2496                return Err(err);
2497            }
2498            if self.kv_cache.layers[li].seq_len == 0 {
2499                continue;
2500            }
2501            participating = true;
2502            let num_heads = self.layer_num_heads(li);
2503            self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2504        }
2505        self.o1_note_transition();
2506        for li in 0..self.num_layers {
2507            if self.o1_flags.get(li).copied().unwrap_or(false) {
2508                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2509                    return Err(err);
2510                }
2511            }
2512        }
2513        Ok(participating
2514            && (0..self.num_layers).all(|li| {
2515                !self.o1_flags.get(li).copied().unwrap_or(false)
2516                    || self.kv_cache.layers[li].seq_len == 0
2517                    || self.kv_cache.layers[li].o1_sealed()
2518            }))
2519    }
2520
2521    /// Complete a deferred boundary after a full position/span forward.
2522    /// This is the pipeline owner for epoch publication and failure cleanup.
2523    fn o1_progress(&mut self) {
2524        if !self.o1_active() {
2525            return;
2526        }
2527        for li in 0..self.num_layers {
2528            if self.o1_flags.get(li).copied().unwrap_or(false) {
2529                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2530                    self.o1_fail(err);
2531                    return;
2532                }
2533            }
2534        }
2535        // A qwen_attention row can seal in the middle of a complete layer
2536        // walk. Consume its transition even though the pending boundary has
2537        // already disappeared from the cache.
2538        self.o1_note_transition();
2539        if !self.o1_pending() {
2540            return;
2541        }
2542        if let Err(err) = self.o1_seal_checked() {
2543            self.o1_fail(err);
2544        }
2545    }
2546
2547    /// Turn a deferred O(1) failure raised by a hidden-only forward into the
2548    /// Result error its public batch/span caller must return. The failure
2549    /// path already cleared host/device sequence state; consume only the
2550    /// side-channel marker here and leave the pipeline reusable.
2551    fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
2552        if self
2553            .graph_failed
2554            .swap(false, std::sync::atomic::Ordering::Relaxed)
2555        {
2556            self.cancel
2557                .store(false, std::sync::atomic::Ordering::Relaxed);
2558            self.clear_sequence_state();
2559            return Err(format!("{phase}: deferred O(1) transition failed"));
2560        }
2561        Ok(())
2562    }
2563
2564    /// Freeze landmarks + skeleton state after the prompt pass and drop
2565    /// the o1 layers' full KV; decode then runs `step()` per token.
2566    /// Pub for the network split (see `o1_begin`).
2567    pub fn o1_seal(&mut self) {
2568        if let Err(err) = self.o1_seal_checked() {
2569            self.o1_fail(err);
2570        }
2571    }
2572
2573    /// Enable/disable the structured per-token telemetry trace (B4).
2574    pub fn set_trace(&mut self, on: bool) {
2575        self.trace = on;
2576    }
2577
2578    /// Replace all request-scoped sampler options and reset the random stream.
2579    /// This is required for deterministic `seed` semantics in pooled servers.
2580    pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2581        self.rng = match config.seed {
2582            Some(seed) => SplitMix64::new(seed),
2583            None => SplitMix64::from_entropy(),
2584        };
2585        self.sampler_config = config;
2586    }
2587
2588    /// Toggle the per-token confidence reduction (a full-vocab
2589    /// softmax each token). `bench --core` turns it off so the timed
2590    /// loop matches llama-bench's core contract; the result's
2591    /// `confidence` vec is empty while off.
2592    pub fn set_confidence(&mut self, on: bool) {
2593        self.confidence_on = on;
2594    }
2595
2596    /// Set the confidence-calibration temperature (B1). Values ≤0 are
2597    /// clamped to raw (1.0).
2598    pub fn set_calib_temp(&mut self, t: f32) {
2599        self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2600    }
2601
2602    /// The active calibration temperature (1.0 = raw probability).
2603    pub fn calib_temp(&self) -> f32 {
2604        self.calib_temp
2605    }
2606
2607    /// Partial rotary (Qwen3.5): rotate only the first `rotary_dim` dims;
2608    /// the frequency table is rebuilt over the rotary dims.
2609    pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2610        self.rotary_dim = rotary_dim.min(self.head_dim);
2611        self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2612    }
2613
2614    fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2615        QwenAttnCfg {
2616            num_heads: self.num_heads,
2617            num_kv_heads: self.num_kv_heads,
2618            head_dim: self.head_dim,
2619            hidden_size: self.hidden_size,
2620            position,
2621            inv_freq: &self.inv_freq,
2622            rotary_dim: self.rotary_dim,
2623            scale: self.attn_scale,
2624            softcap: self.attn_softcap,
2625            window: None,
2626            v_norm: false,
2627            qk_norm_after_rope: self.qk_norm_after_rope,
2628            q_norm: None,
2629            k_norm: None,
2630            output_gate: false,
2631            softplus_gate: None,
2632            rope_scale: self.rope_scale,
2633            bias: None,
2634            rms_eps: self.rms_eps,
2635            norm_style: self.norm_style,
2636            pool: self.pool.as_deref(),
2637        }
2638    }
2639
2640    /// Generate text from a plain-text prompt. Streams tokens via `on_token`.
2641    pub fn generate(
2642        &mut self,
2643        prompt: &str,
2644        max_tokens: usize,
2645        task_mask: Option<&TaskMask>,
2646        on_token: Option<TokenCallback>,
2647    ) -> Result<GenerateResult, String> {
2648        let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2649        self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2650    }
2651
2652    /// Generate from a V4.1 multimodal prompt prepared by the vision module.
2653    /// Vision rows are encoded once and fed through the same bounded token walk as text.
2654    pub fn generate_from_vl(
2655        &mut self,
2656        input: &crate::dsv41_vision::PreparedVlInputs,
2657        max_tokens: usize,
2658        task_mask: Option<&TaskMask>,
2659        on_token: Option<TokenCallback>,
2660    ) -> Result<GenerateResult, String> {
2661        let Some(dsv41) = &self.dsv41 else {
2662            return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
2663        };
2664        if input.token_ids.is_empty() {
2665            return Err("empty V4.1 multimodal prompt".into());
2666        }
2667        if input.token_types.len() != input.token_ids.len() {
2668            return Err(format!(
2669                "V4.1 token type count {} != token count {}",
2670                input.token_types.len(),
2671                input.token_ids.len()
2672            ));
2673        }
2674        let dim = dsv41.2.dim;
2675        let mut embeddings = vec![None; input.token_ids.len()];
2676        let mut participates = vec![true; input.token_ids.len()];
2677        if !input.images.is_empty() {
2678            let vision = self
2679                .dsv41_vision
2680                .as_ref()
2681                .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
2682            for image in &input.images {
2683                let end = image.start.saturating_add(image.types.len());
2684                if end > input.token_ids.len() {
2685                    return Err(format!(
2686                        "V4.1 image span {}..{} exceeds prompt length {}",
2687                        image.start,
2688                        end,
2689                        input.token_ids.len()
2690                    ));
2691                }
2692                let mut span = vec![0.0f32; image.types.len() * dim];
2693                vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
2694                for (offset, &kind) in image.types.iter().enumerate() {
2695                    let pos = image.start + offset;
2696                    if input.token_types[pos] != kind {
2697                        return Err(format!(
2698                            "V4.1 image type mismatch at position {pos}: {} != {kind}",
2699                            input.token_types[pos]
2700                        ));
2701                    }
2702                    embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
2703                    participates[pos] = false;
2704                }
2705            }
2706        }
2707        for (pos, &kind) in input.token_types.iter().enumerate() {
2708            if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
2709                return Err(format!("V4.1 text position {pos} has an image embedding"));
2710            }
2711            if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
2712                return Err(format!("V4.1 image position {pos} has no image embedding"));
2713            }
2714        }
2715        self.dsv41_prefill = Some((embeddings, participates));
2716        let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
2717        self.dsv41_prefill = None;
2718        result
2719    }
2720
2721    /// `None` when the mask forbids nothing (see `TaskMask::fully_open`).
2722    fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
2723        m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
2724    }
2725
2726    /// Generate from prepared token ids (e.g. a chat template).
2727    ///
2728    /// With an MTP head, greedy generation without a task mask takes the
2729    /// speculative path: the MTP module drafts the token after next and
2730    /// the main model verifies both in one fused two-position forward
2731    /// (weights streamed once). The output is EXACTLY the vanilla greedy
2732    /// sequence — a rejected draft is rolled back — MTP only buys speed.
2733    pub fn generate_from_ids(
2734        &mut self,
2735        input_ids: &[u32],
2736        max_tokens: usize,
2737        task_mask: Option<&TaskMask>,
2738        mut on_token: Option<TokenCallback>,
2739    ) -> Result<GenerateResult, String> {
2740        if std::env::var("CMF_TRACE_H").is_ok() {
2741            eprintln!("input_ids: {input_ids:?}");
2742        }
2743        if input_ids.is_empty() {
2744            return Err("empty prompt: nothing to generate from".to_string());
2745        }
2746        // A prior graph failure is terminal for that sequence but must not
2747        // poison the next independent request.  Keep this flag separate from
2748        // the externally-owned cooperative cancel bit.
2749        self.graph_failed
2750            .store(false, std::sync::atomic::Ordering::Relaxed);
2751        // A mask that forbids nothing still costs every fused path and
2752        // whole-token graph, all of which are gated on `is_none()`. A
2753        // narrowed file whose one segment is always on carries exactly
2754        // such a mask — drop it here rather than pay 5x for a no-op.
2755        let task_mask = self.drop_open_mask(task_mask);
2756
2757        // Cross-turn KV reuse: a chat app resends the whole history
2758        // every turn; when the new ids strictly EXTEND what the cache
2759        // already holds, prefill only the tail — turn latency stays
2760        // proportional to the new text instead of the whole session.
2761        // Extension-only (no rollback), so it is exact for every layer
2762        // kind including recurrent state; MTP/o1/task-mask runs keep
2763        // the fresh-sequence path. CMF_KV_REUSE=0 disables.
2764        let reuse_from = {
2765            let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
2766            let h = &self.kv_history;
2767            if on
2768                && task_mask.is_none()
2769                && self.mtp.is_none()
2770                && self.o1_cfg.is_none()
2771                && self.dsv41.is_none()
2772                && !h.is_empty()
2773                && h.len() < input_ids.len()
2774                && input_ids[..h.len()] == h[..]
2775            {
2776                h.len()
2777            } else {
2778                0
2779            }
2780        };
2781        if reuse_from == 0 {
2782            // Fresh sequence — the cache holds absolute positions.
2783            self.clear_sequence_state();
2784        } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
2785            eprintln!(
2786                "kv-reuse: {} of {} prompt positions already cached",
2787                reuse_from,
2788                input_ids.len()
2789            );
2790        }
2791        crate::gpu::graph_race_begin_generation();
2792        // Optional bounded calibration prefix. Keep the requested value
2793        // even when it is longer than the prompt; the collecting layer will
2794        // defer at the effective boundary and remain exact for short input.
2795        let o1_prefill = if self.o1_active() && task_mask.is_none() {
2796            std::env::var("CMF_O1_PREFILL")
2797                .ok()
2798                .and_then(|v| v.parse::<usize>().ok())
2799                .filter(|&p| p > 0)
2800        } else {
2801            None
2802        };
2803        if task_mask.is_none() {
2804            self.o1_begin_with_prefix(o1_prefill);
2805        }
2806
2807        // Speculative decode is off under o1: a rejected draft can't be
2808        // rolled back out of the far accumulators / ring window (the
2809        // Nyström insertion is irreversible by design).
2810        // The wgpu token graph owns a device K/V mirror that speculative
2811        // rollback would desync — the two are mutually exclusive.
2812        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2813        // Graph speculative decode (`CMF_GRAPH_SPEC=1`): the MTP head
2814        // drafts, ONE batched graph submit verifies the whole chain.
2815        //
2816        // It now PAYS on Qwen3.6-27B / RTX 5090 — 51.1 tok/s against a
2817        // plain 49.4 at k=3, medians of three, 89% of drafts accepted,
2818        // and the greedy continuation is byte-identical to the plain
2819        // path. That took the batch matvec sharing its nibble unpack
2820        // across the batch (`CMF_MV_BK=2`); before it, the same round
2821        // measured 43.6, an 11% LOSS, which is what the earlier note
2822        // here described.
2823        //
2824        // Still opt-in. One model's win is not a default: the verify
2825        // rides `gdn_spec_restore` and a batched frame whose numerics
2826        // are the batch kernels', and that has to be shown on more than
2827        // one architecture before every greedy decode takes it.
2828        // Greedy (with or without penalties) verifies by argmax equality.
2829        // Sampling (temperature > 0) can go through speculative SAMPLING —
2830        // draft from the MTP head's own post-chain distribution, accept
2831        // with min(1, p/q), correct from max(0, p − q); the emitted stream
2832        // is distributed exactly as the plain sampler's — but it is
2833        // OPT-IN (`CMF_GRAPH_SPEC_SAMPLE=1`): measured on Qwen3.8-27B /
2834        // RTX 5090 at the instruct row (0.7 / 0.80 / 20 / presence 1.5)
2835        // it decoded 19-22 tok/s against a plain 40 — nine post-chain
2836        // distributions a round plus a lower acceptance than greedy's,
2837        // against a verify that costs 2.7 single tokens. The greedy arms
2838        // pay +10%; the sampling arm needs a cheaper verify first.
2839        // Native Metal HAS that verify: its eight-row tile is flat in b,
2840        // so a round costs ~1.9 plain tokens and the sampling arm pays at
2841        // 2.3 accepted per round — measured on Qwen3.8-27B q4tp / M4 at
2842        // the CLI defaults (0.7 / rep 1.1 / top-k 40, seed 42), a code
2843        // prompt: 9.0 tok/s against a plain 5.4 in the same window, and
2844        // the per-round watchdog turns it off where prose loses. So on
2845        // Metal the sampling arm is ON (`CMF_GRAPH_SPEC_SAMPLE=0` opts out)
2846        // — but only for a config the SPARSE chain serves (a top-k within
2847        // `sparse_ok`): without it a round builds nine 248k-float
2848        // distributions on the host, which is the 5090's measured loss and
2849        // not a cost the round-token proxy below can see. A top-k-less
2850        // sampling config keeps the plain path unless asked for by name.
2851        #[cfg(target_os = "macos")]
2852        let metal_graph = crate::gpu::q1_force()
2853            && crate::gpu::enabled_here()
2854            && std::env::var("CMF_GPU_BLOCK")
2855                .map(|v| v != "0")
2856                .unwrap_or(true);
2857        #[cfg(not(target_os = "macos"))]
2858        let metal_graph = false;
2859        let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
2860        // A round whose cost is the MEASURED one: greedy (argmax rows), or
2861        // sampling through the sparse chain. Anything else pays the dense
2862        // chain's host time, which no proxy can price.
2863        let spec_cheap_round = self.sampler_config.temperature < 1e-6
2864            || sampler::sparse_ok(&self.sampler_config);
2865        let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2866            || match spec_sample_env.as_deref() {
2867                Some("1") => true,
2868                Some(_) => false,
2869                None => metal_graph && spec_cheap_round,
2870            };
2871        // ON by default for greedy on the wgpu graph: with the draft on
2872        // the graph and the verify bit-exact, it measured 58.7 tok/s
2873        // against a plain 48.1 on Qwen3.8-27B q4tp / RTX 5090 (k=4) and
2874        // 51.1 against 49.4 on Qwen3.6-27B, and a round that stops
2875        // paying turns itself off below (acceptance watchdog).
2876        // `CMF_GRAPH_SPEC=0` disables; `=1` was the old opt-in spelling.
2877        // …but only where the batched verify has its register-blocked
2878        // kernel: q4tp dense FFNs (graph kind 6). q4t and q8_2f verify
2879        // through tile GEMMs today and measured a LOSS (q8_2f 22 against
2880        // 29 tok/s), the 2-bit plane the same; those stay opt-in
2881        // (`CMF_GRAPH_SPEC=1`).
2882        // …at least in nine dense FFNs of ten: a healed file carries its
2883        // last two layers at q8_2f, and two tile-GEMM verifies among 64 do
2884        // not change the arithmetic (measured: the healed q4tp file
2885        // decodes at the plain file's rate and would otherwise sit out).
2886        let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2887        for lw in &self.weights.layers {
2888            if let FfnKind::Dense(d) = &lw.ffn {
2889                dense_n += 1;
2890                if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2891                    && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2892                    && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2893                {
2894                    dense_q4tp += 1;
2895                }
2896            }
2897        }
2898        let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2899        // Penalties break the draft head's agreement with the trunk (a
2900        // 1.1 repetition penalty measured 2 of 16 accepted): not by
2901        // default there either — off Metal that rule is untouched, and
2902        // suppressed ids keep counting as a penalty there, because no
2903        // measurement on a discrete card says otherwise.
2904        //
2905        // On native Metal the penalized arms DO pay: the draft applies
2906        // the same penalty and the verify scores the penalized rows
2907        // exactly (`greedy_pen`, the plain loop's arithmetic), so the
2908        // text is the plain path's and only the round's shape changes.
2909        // Measured on this M4 — see the report for the interleaved run.
2910        let penalized = !metal_graph
2911            && (self.sampler_config.repetition_penalty != 1.0
2912                || self.sampler_config.presence_penalty != 0.0
2913                || !self.sampler_config.suppress_tokens.is_empty());
2914        // …and not on wgpu-over-Metal: the batched verify graph there
2915        // returned 0 accepted drafts and garbage text on a GDN hybrid
2916        // (16.08, Qwen3.5-0.8B) while Vulkan is bit-exact; the Mac's
2917        // default backend is native Metal without a batch graph anyway.
2918        #[cfg(feature = "gpu")]
2919        let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2920        #[cfg(not(feature = "gpu"))]
2921        let metal_wgpu = false;
2922        let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2923        let spec_wanted = match spec_env.as_deref() {
2924            Some("0") => false,
2925            Some(_) => {
2926                if metal_wgpu {
2927                    tracing::warn!(
2928                        "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2929                         verified on this backend (garbage measured on Qwen3.5-0.8B)"
2930                    );
2931                }
2932                true
2933            }
2934            None => spec_default_ok && !penalized && !metal_wgpu,
2935        };
2936        // Native Metal: the b-row verify graph (`try_batch_graph_metal`)
2937        // stands where the wgpu batch graph stands on discrete cards
2938        // (`metal_graph`, above).
2939        let graph_spec = self.speculative
2940            && (graph_on || metal_graph)
2941            && self.mtp.is_some()
2942            && task_mask.is_none()
2943            && !self.o1_active()
2944            && spec_sampling_ok
2945            && spec_wanted;
2946        // Native Metal: say the route ONCE (RUST_LOG=info), so a user can
2947        // confirm the fast path without setting a single flag — every
2948        // knob below defaults to the measured-best value on the M4.
2949        #[cfg(target_os = "macos")]
2950        if metal_graph {
2951            static SAID: std::sync::Once = std::sync::Once::new();
2952            SAID.call_once(|| {
2953                let spec = if graph_spec {
2954                    let k = std::env::var("CMF_GRAPH_SPEC_K")
2955                        .ok()
2956                        .and_then(|v| v.parse::<usize>().ok())
2957                        .filter(|&v| (1..=8).contains(&v))
2958                        .unwrap_or(7);
2959                    let arm = if self.sampler_config.temperature < 1e-6 {
2960                        "greedy"
2961                    } else {
2962                        "sampling"
2963                    };
2964                    format!(
2965                        "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
2966                        Self::draft_vocab_rows(usize::MAX)
2967                    )
2968                } else if !self.speculative {
2969                    "spec off (CMF_MTP=0)".to_string()
2970                } else if self.mtp.is_none() {
2971                    "spec off (no MTP head)".to_string()
2972                } else if !spec_sampling_ok {
2973                    if spec_cheap_round {
2974                        "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
2975                    } else {
2976                        "spec off (sampling without a top-k: the dense chain \
2977                         costs more than it saves)"
2978                            .to_string()
2979                    }
2980                } else if !spec_wanted {
2981                    "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
2982                } else if task_mask.is_some() {
2983                    "spec off (task mask)".to_string()
2984                } else {
2985                    "spec off (O(1) attention)".to_string()
2986                };
2987                let on = |var: &str| {
2988                    if std::env::var(var).as_deref() == Ok("0") {
2989                        "off"
2990                    } else {
2991                        "on"
2992                    }
2993                };
2994                tracing::info!(
2995                    "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
2996                     MTP graph {}, attend {}, probe {}",
2997                    if crate::gpu_metal::state4_on() { "on" } else { "off" },
2998                    if crate::gpu_metal::async_replay_on() { "on" } else { "off" },
2999                    on("CMF_METAL_PREFILL"),
3000                    on("CMF_MTP_GRAPH"),
3001                    std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
3002                    if crate::gpu::probe_enabled() { "bypassed (q1 force)" } else { "off" },
3003                );
3004            });
3005        }
3006        // GDN hybrids sit the fused-pair speculation out by default: the
3007        // recurrence is sequential, so the pair lane cannot parallelize
3008        // (the bench's own Pair line reads fused 1.28x TWO singles on the
3009        // 35B) and the draft's full-vocab head rides on top — measured 2x
3010        // SLOWER end to end (16.1 vs 32.4 tok/s on the 48-core stand).
3011        // CMF_MTP=1 forces it back for study.
3012        let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
3013        let spec_active = self.speculative
3014            && self.mtp.is_some()
3015            && task_mask.is_none()
3016            && !self.o1_active()
3017            && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
3018        // The MTP module is detached during generation so its mutable
3019        // state does not fight the borrow on `self`.
3020        let mut mtp = if spec_active { self.mtp.take() } else { None };
3021        if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
3022            eprintln!(
3023                "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
3024                mtp.is_some(),
3025                self.speculative,
3026                self.sampler_config.temperature < 1e-6,
3027            );
3028        }
3029        if let Some(m) = &mut mtp {
3030            m.kv.clear();
3031            // The MTP block's own device mirror starts over with its cache.
3032            crate::gpu::graph_kv_reset(self.mtp_kv_id());
3033            self.mtp_graph_mode = None;
3034        }
3035        // Dynamic router detached during decode (same borrow trick as MTP).
3036        // Speculative decode and dynamic routing are mutually exclusive
3037        // for now — the fused-pair path doesn't carry per-token φ.
3038        let mut router = if mtp.is_none() {
3039            self.dyn_router.take()
3040        } else {
3041            None
3042        };
3043        if let Some(r) = &mut router {
3044            r.reset(); // active=backbone, matching a fresh overlay
3045            self.dyn_phi_seen = 0; // fresh φ EMA per generation
3046            let _ = self.set_active_skill(None);
3047        }
3048
3049        let mut all_ids = input_ids.to_vec();
3050        let mut generated = 0usize;
3051        let mut finish_reason = "max_tokens".to_string();
3052        let mut drafted = 0usize;
3053        let mut accepted = 0usize;
3054        // DeepSeek-V4's draft quality is strongly content-dependent.  Two
3055        // consecutive paid rounds with no extra token put it on a bounded
3056        // cooldown; predictable text keeps batching, ordinary prose falls
3057        // back to the exact walk instead of paying a slow draft forever.
3058        // Local to one generation so one difficult request cannot poison the
3059        // next one, and deliberately automatic — this is not a user knob.
3060        let mut dsv4_spec_bad = 0usize;
3061        let mut dsv4_spec_retry_at = 0usize;
3062        let mut confidence: Vec<f32> = Vec::new();
3063        let trace_on = self.trace;
3064        let calib_temp = self.calib_temp;
3065        let mut traces: Vec<TokenTrace> = Vec::new();
3066
3067        // ── Prefill: forward each prompt token once, KEEP the last hidden.
3068        //    Dense prefill runs in fused pairs (weights streamed once per
3069        //    two positions — bit-identical to sequential, proven by the
3070        //    pair tests). With MTP: warm the draft head on
3071        //    (hidden_p, token_{p+1}) pairs.
3072        let mut hidden = vec![0.0f32; self.hidden_size];
3073        let mut pos = reuse_from;
3074        // lm_head-in-graph is only sound when the very next logits
3075        // consumer is this loop's own (MTP and skill routing interleave
3076        // other forwards / can swap lm_head between forward and sample).
3077        // CMF_GPU_LMHEAD=0 keeps lm_head off the graph: the token reads back
3078        // the 8 KB hidden instead of ~1 MB of logits, and the head runs on
3079        // the host. A probe for how much of the graph's fixed per-token cost
3080        // is the logits readback (the layer sweep puts that fixed part at
3081        // 3.88 ms of an 18.5 ms frame).
3082        let fuse_lm = mtp.is_none()
3083            && router.is_none()
3084            && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
3085        self.graph_logits = None;
3086        self.graph_want_logits = false;
3087        let _tpf = std::time::Instant::now();
3088        let batch_k = self.generation_batch_k();
3089        // DeepSeek-V4 owns a separate hyper-connection stack. Route it
3090        // before the generic prefill choices: those correctly reject an
3091        // empty `weights.layers`, but their final per-position fallback used
3092        // to consume the whole prompt before `dsv4::forward_chunk` could see
3093        // it. The batch implementation therefore existed without a live
3094        // production entry point.
3095        //
3096        // Bounded chunks preserve cancellation responsiveness. Only the
3097        // prompt's final chunk asks for logits; every earlier head projection
3098        // would produce 129 280 values that no caller reads.
3099        while self.qwen4_exp.is_some()
3100            && mtp.is_none()
3101            && pos < input_ids.len()
3102            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3103        {
3104            let token_id = input_ids[pos];
3105            let want_logits = pos + 1 == input_ids.len();
3106            let mut lg = Vec::new();
3107            if let Some(b) = &mut self.qwen4_exp {
3108                crate::qwen4_exp::forward_token(
3109                    &b.0,
3110                    &b.1,
3111                    &b.2,
3112                    &mut b.3,
3113                    token_id,
3114                    pos,
3115                    &self.inv_freq,
3116                    self.pool.as_deref(),
3117                    &mut lg,
3118                    want_logits,
3119                );
3120            }
3121            if want_logits {
3122                self.graph_logits = Some(lg);
3123            }
3124            pos += 1;
3125            hidden.fill(0.0);
3126        }
3127        while self.dsv4.is_some()
3128            && mtp.is_none()
3129            && pos < input_ids.len()
3130            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3131        {
3132            let end = (pos + prefill_chunk()).min(input_ids.len());
3133            let ids: Vec<u32> = input_ids[pos..end].to_vec();
3134            let mut lg = Vec::new();
3135            if let Some(b) = &mut self.dsv4 {
3136                let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
3137                crate::dsv4::forward_chunk(
3138                    g,
3139                    layers,
3140                    &cfg,
3141                    st,
3142                    &ids,
3143                    pos,
3144                    &self.inv_freq,
3145                    self.pool.as_deref(),
3146                    &mut lg,
3147                    end == input_ids.len(),
3148                );
3149            }
3150            if end == input_ids.len() {
3151                self.graph_logits = Some(lg);
3152            }
3153            pos = end;
3154            hidden = vec![0.0; self.hidden_size];
3155        }
3156        let dsv41_prefill = self.dsv41_prefill.take();
3157        while self.dsv41.is_some()
3158            && mtp.is_none()
3159            && pos < input_ids.len()
3160            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3161        {
3162            let end = (pos + prefill_chunk()).min(input_ids.len());
3163            let ids: Vec<u32> = input_ids[pos..end].to_vec();
3164            let mut lg = Vec::new();
3165            if let Some(b) = &mut self.dsv41 {
3166                let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
3167                if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
3168                    crate::dsv41::forward_chunk_masked_with_embeddings(
3169                        g,
3170                        layers,
3171                        cfg,
3172                        st,
3173                        &ids,
3174                        pos,
3175                        &embeddings[pos..end],
3176                        &participates[pos..end],
3177                        self.pool.as_deref(),
3178                        &mut lg,
3179                    );
3180                } else {
3181                    crate::dsv41::forward_chunk(
3182                        g,
3183                        layers,
3184                        cfg,
3185                        st,
3186                        &ids,
3187                        pos,
3188                        self.pool.as_deref(),
3189                        &mut lg,
3190                    );
3191                }
3192            }
3193            if end == input_ids.len() {
3194                self.graph_logits = Some(lg);
3195            }
3196            pos = end;
3197            hidden = vec![0.0; self.hidden_size];
3198        }
3199        // With dynamic routing, prefill sequentially so the φ hook fires
3200        // over the PROMPT — the router enters decode with a warm φ (the
3201        // fused-pair path skips the per-layer φ capture). o1 layers
3202        // collect their query trace in both the single and pair paths.
3203        let dyn_prefill = router.is_some();
3204        // Optional bounded calibration prefix for generation.  The normal
3205        // O(1) path seals after the full prompt; this explicit knob instead
3206        // runs only the requested prefix through exact attention, seals the
3207        // Nyström state, and streams the rest of the prompt through the same
3208        // O(1) step used by decode.  It keeps the O(1) layers' Q trace and
3209        // temporary full KV bounded by the prefix while leaving the default
3210        // full-prompt quality profile untouched.
3211        let o1_prefill_limit = o1_prefill
3212            .and_then(|requested| self.o1_effective_boundary(requested))
3213            .map(|boundary| boundary.min(input_ids.len()));
3214        let mut o1_sealed = false;
3215        if let Some(limit) = o1_prefill_limit {
3216            // Reuse the exact batched prefix machinery when available; it
3217            // records the same per-position Q trace as the full prefill.
3218            if self.can_prefill_batched() && limit > 2 {
3219                let chunk = prefill_chunk();
3220                let hs = self.hidden_size;
3221                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3222                    let end = (pos + chunk).min(limit);
3223                    let hb = self.prefill_batch(&input_ids[pos..end], pos);
3224                    hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3225                    pos = end;
3226                }
3227            } else {
3228                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3229                    hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
3230                    pos += 1;
3231                }
3232            }
3233            if pos >= limit {
3234                o1_sealed = match self.o1_seal_checked() {
3235                    Ok(sealed) => sealed,
3236                    Err(err) => {
3237                        self.finish_generation(&mut mtp, &mut router, true);
3238                        return Err(err);
3239                    }
3240                };
3241                tracing::info!(
3242                    "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
3243                    o1_prefill.unwrap_or(0),
3244                    self.o1_effective_boundary(o1_prefill.unwrap_or(0))
3245                        .unwrap_or(limit),
3246                    limit,
3247                    input_ids.len()
3248                );
3249            }
3250        }
3251        // q1 hybrids on Metal: the per-position GPU token graph beats
3252        // the CPU chunk-GEMM (whose wall is the sequential scalar GDN
3253        // recurrence), so prefill goes position-by-position through the
3254        // same graph as decode. Pure-attention models keep the batched
3255        // path — there the chunk-GEMM amortization wins.
3256        let graph_prefill = self.graph_prefill_preferred();
3257        // Native Metal, q4tp GDN hybrids: the prompt through the b-row
3258        // rows graph — projections as GEMMs over up to 512 positions, the
3259        // GDN recurrence in registers on the device, K/V rows appended by
3260        // the chunk — instead of one token-graph submit per position (the
3261        // 27B: 8 tok/s → GEMM-bound). The MTP warm-up rows come out of one
3262        // batched run of the block per chunk. Any refusal leaves the rest
3263        // of the prompt to the sequential paths below.
3264        #[cfg(target_os = "macos")]
3265        if task_mask.is_none()
3266            && !dyn_prefill
3267            && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
3268            && crate::gpu::enabled_here()
3269            && self.gdn_cfg.is_some()
3270            && self.g3n.is_none()
3271            && input_ids.len() > 8
3272            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3273            && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
3274        {
3275            let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
3276                .ok()
3277                .and_then(|v| v.parse().ok())
3278                .filter(|&v| (16..=512).contains(&v))
3279                .unwrap_or(256);
3280            let hs = self.hidden_size;
3281            let _tp = std::time::Instant::now();
3282            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3283                let end = (pos + chunk).min(input_ids.len());
3284                let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
3285                    MetalPrefillOutcome::Completed(hb) => hb,
3286                    MetalPrefillOutcome::Declined => break,
3287                    MetalPrefillOutcome::Failed => {
3288                        self.finish_generation(&mut mtp, &mut router, true);
3289                        return Err("ordinary Metal prefill failed after admission".into());
3290                    }
3291                };
3292                if let Some(m) = &mut mtp {
3293                    let n_pairs = if end < input_ids.len() {
3294                        end - pos
3295                    } else {
3296                        end - pos - 1
3297                    };
3298                    if n_pairs > 0 {
3299                        let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
3300                            .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
3301                            .collect();
3302                        if !self.mtp_warm_batch_metal(m, &pairs, pos) {
3303                            for (j, (h, t)) in pairs.iter().enumerate() {
3304                                let h = h.to_vec();
3305                                let _ = self.mtp_step(m, &h, *t, pos + j);
3306                            }
3307                        }
3308                    }
3309                }
3310                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3311                pos = end;
3312            }
3313            if std::env::var("CMF_PREFILL_PROF").is_ok() {
3314                eprintln!(
3315                    "metal-prefill: {} of {} tokens in {:.1} ms",
3316                    pos,
3317                    input_ids.len(),
3318                    _tp.elapsed().as_secs_f64() * 1e3
3319                );
3320            }
3321        }
3322        if task_mask.is_none()
3323            && !dyn_prefill
3324            && !graph_prefill
3325            && self.can_prefill_batched()
3326            && self.g3n.is_none()
3327            && o1_prefill.is_none()
3328            && input_ids.len() > 2
3329        {
3330            // Production prefill = the same chunked prefill-GEMM that
3331            // bench/PPL measure (roadmap §3 P0: generation used to warm
3332            // the prompt with the slower pair path — the published
3333            // prefill number didn't match real TTFT). MTP warm-up reads
3334            // each position's hidden straight from the chunk result.
3335            let chunk = prefill_chunk();
3336            let hs = self.hidden_size;
3337            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3338                let end = (pos + chunk).min(input_ids.len());
3339                let hb = self.prefill_batch(&input_ids[pos..end], pos);
3340                if let Some(m) = &mut mtp {
3341                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3342                        .ok()
3343                        .and_then(|v| v.parse().ok())
3344                        .unwrap_or(0);
3345                    for p in pos..end {
3346                        if p + 1 < input_ids.len() {
3347                            if probe >= 1 && p + 2 < input_ids.len() {
3348                                // Teacher-forced chain acceptance (see the
3349                                // tail loop's twin): the warm-up row stays,
3350                                // the chain's rows roll back.
3351                                let (d1, mut hx) = self.mtp_step_h(
3352                                    m,
3353                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
3354                                    input_ids[p + 1],
3355                                    p,
3356                                );
3357                                let mut ok = d1 == input_ids[p + 2];
3358                                Self::chain_probe_note(0, ok);
3359                                let mut d_prev = d1;
3360                                let mut extra = 0usize;
3361                                for j in 1..probe {
3362                                    if p + 2 + j >= input_ids.len() {
3363                                        break;
3364                                    }
3365                                    let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
3366                                    extra += 1;
3367                                    ok = ok && dj == input_ids[p + 2 + j];
3368                                    Self::chain_probe_note(j, ok);
3369                                    d_prev = dj;
3370                                    hx = hj;
3371                                }
3372                                m.kv.truncate_last(extra);
3373                            } else {
3374                                let _ = self.mtp_step(
3375                                    m,
3376                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
3377                                    input_ids[p + 1],
3378                                    p,
3379                                );
3380                            }
3381                        }
3382                    }
3383                }
3384                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3385                pos = end;
3386            }
3387        }
3388        let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
3389        if task_mask.is_none()
3390            && !dyn_prefill
3391            && !graph_prefill
3392            && !pair_off
3393            && self.pair_supported()
3394            && o1_prefill.is_none()
3395        {
3396            while pos + 1 < input_ids.len()
3397                && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3398            {
3399                let e1 = self.embed_single(input_ids[pos]);
3400                let e2 = self.embed_single(input_ids[pos + 1]);
3401                let (h1, h2) = self.forward_pair(&e1, &e2, pos);
3402                // Both prefill tokens are real → commit lane-2 states.
3403                self.commit_linear_scratch();
3404                if let Some(m) = &mut mtp {
3405                    let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
3406                    if pos + 2 < input_ids.len() {
3407                        let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3408                            .ok()
3409                            .and_then(|v| v.parse().ok())
3410                            .unwrap_or(0);
3411                        if probe >= 1 && pos + 3 < input_ids.len() {
3412                            // Same teacher-forced chain table as the tail
3413                            // loop below, fed from the pair path that owns
3414                            // most prefill positions.
3415                            let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
3416                            let mut ok = d1 == input_ids[pos + 3];
3417                            Self::chain_probe_note(0, ok);
3418                            let mut d_prev = d1;
3419                            let mut extra = 0usize;
3420                            for j in 1..probe {
3421                                if pos + 3 + j >= input_ids.len() {
3422                                    break;
3423                                }
3424                                let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
3425                                extra += 1;
3426                                ok = ok && dj == input_ids[pos + 3 + j];
3427                                Self::chain_probe_note(j, ok);
3428                                d_prev = dj;
3429                                hx = hj;
3430                            }
3431                            m.kv.truncate_last(extra);
3432                        } else {
3433                            let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
3434                        }
3435                    }
3436                }
3437                hidden = h2;
3438                pos += 2;
3439            }
3440        }
3441        // Batched GPU prefill for the wgpu decode graph (GDN hybrids): K prompt
3442        // positions per submit — projections/FFN as GEMMs (weight once per K),
3443        // attention/GDN looped inside — instead of one whole-graph submit per
3444        // position. Falls through to the per-position graph on any refusal.
3445        // Batched prefill is opt-in (CMF_BATCH_K>0). Default 0 = per-position
3446        // graph prefill. (Steady-state decode is provably identical either way —
3447        // token-graph submit and lm_head both unchanged — so this only trades
3448        // prefill wall.)
3449        // A bounded O(1) prefix is the one post-seal prompt interval: only
3450        // admit its batch when the device O(1) route is explicitly enabled and
3451        // every sealed layer exposes a portable view. The same batch size and
3452        // refusal behavior remain the ordinary controls/comparator.
3453        let o1_batch_ready = o1_sealed
3454            && o1_prefill.is_some()
3455            && mtp.is_none()
3456            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
3457            && (0..self.num_layers).all(|li| {
3458                let cache = &self.kv_cache.layers[self.phys_layer(li)];
3459                cache.o1.is_none() || cache.o1_views().is_some()
3460            });
3461        // The ordinary graph-prefill route can share each completed trunk
3462        // chunk with an attached MTP head.  Keep chain probing on its
3463        // established per-position path: the probe deliberately needs every
3464        // teacher-forced draft row and its rollback table.
3465        let mtp_batch_prefill = mtp.is_some()
3466            && graph_prefill
3467            && task_mask.is_none()
3468            && !dyn_prefill
3469            && !self.o1_active()
3470            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
3471        if batch_k > 0
3472            && (graph_prefill || o1_batch_ready)
3473            && task_mask.is_none()
3474            && (!self.o1_active() || o1_batch_ready)
3475            && (mtp.is_none() || mtp_batch_prefill)
3476            && !dyn_prefill
3477            && pos + 1 < input_ids.len()
3478        {
3479            let hs = self.hidden_size;
3480            let chunk = batch_k;
3481            while pos < input_ids.len() {
3482                let end = (pos + chunk).min(input_ids.len());
3483                let bk = end - pos;
3484                let mut hiddens = vec![0f32; bk * hs];
3485                for (j, &id) in input_ids[pos..end].iter().enumerate() {
3486                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3487                }
3488                let positions: Vec<usize> = (pos..end).collect();
3489                let t_chunk = std::time::Instant::now();
3490                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3491                let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
3492                if std::env::var("CMF_GRAPH_PROF").is_ok() {
3493                    let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3494                    eprintln!(
3495                        "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3496                        if o1_batch_ready {
3497                            "o1"
3498                        } else if mtp_batch_prefill {
3499                            "ordinary_mtp"
3500                        } else {
3501                            "ordinary"
3502                        },
3503                        bk as f64 / (ms / 1000.0)
3504                    );
3505                }
3506                {
3507                    use std::sync::atomic::{AtomicBool, Ordering};
3508                    static SAID: AtomicBool = AtomicBool::new(false);
3509                    if !SAID.swap(true, Ordering::Relaxed) {
3510                        if ok_b {
3511                            tracing::info!(
3512                                "batched prefill: ACTIVE mode={} (k={bk})",
3513                                if o1_batch_ready {
3514                                    "o1"
3515                                } else if mtp_batch_prefill {
3516                                    "ordinary_mtp"
3517                                } else {
3518                                    "ordinary"
3519                                }
3520                            );
3521                        } else {
3522                            tracing::warn!("batched prefill {:?} — per-position graph", outcome);
3523                        }
3524                    }
3525                }
3526                if ok_b {
3527                    if mtp_batch_prefill {
3528                        let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
3529                        if n_pairs > 0 {
3530                            // `hiddens` is owned by this chunk, so materialize
3531                            // row slices before borrowing the detached MTP
3532                            // module.  The last prompt row has no successor;
3533                            // the helper above is the single source of that
3534                            // boundary rule.
3535                            let rows: Vec<Vec<f32>> = (0..n_pairs)
3536                                .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
3537                                .collect();
3538                            let pairs: Vec<(&[f32], u32)> = rows
3539                                .iter()
3540                                .enumerate()
3541                                .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
3542                                .collect();
3543                            if std::env::var("CMF_GRAPH_PROF").is_ok() {
3544                                eprintln!(
3545                                    "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
3546                                    pos,
3547                                    n_pairs,
3548                                    pos + n_pairs - 1,
3549                                );
3550                            }
3551                            let warm_error = if let Some(m) = mtp.as_mut() {
3552                                self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
3553                            } else {
3554                                None
3555                            };
3556                            if let Some(err) = warm_error {
3557                                // The trunk batch was already admitted.  A
3558                                // failed MTP warm-up therefore clears both
3559                                // mirrors and exits; continuing would pair a
3560                                // current trunk state with a stale MTP cache.
3561                                self.finish_generation(&mut mtp, &mut router, true);
3562                                return Err(err.to_string());
3563                            }
3564                        }
3565                    }
3566                    hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
3567                    pos = end;
3568                } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
3569                    // A failed batch may have advanced a device recurrent
3570                    // state (ordinary GDN or sealed O(1)). A CPU fallback
3571                    // would then observe stale accumulators, so clear the
3572                    // request state and make the failure explicit.
3573                    self.finish_generation(&mut mtp, &mut router, true);
3574                    return Err(if o1_batch_ready {
3575                        "sealed O(1) batch graph failed after admission".to_string()
3576                    } else {
3577                        "ordinary recurrent batch graph failed after admission".to_string()
3578                    });
3579                } else {
3580                    break; // unsupported → per-position graph handles the rest
3581                }
3582            }
3583        }
3584        while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3585            self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
3586            hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
3587            if let Some(m) = &mut mtp {
3588                if pos + 1 < input_ids.len() {
3589                    // `CMF_MTP_CHAIN_PROBE=k`: teacher-forced acceptance of a
3590                    // CHAINED draft — iterate the head on its own hidden k
3591                    // deep and score every depth against the prompt's real
3592                    // continuation. The economics of a k-token speculative
3593                    // round stand or fall on this table.
3594                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3595                        .ok()
3596                        .and_then(|v| v.parse().ok())
3597                        .unwrap_or(0);
3598                    if probe >= 1 && pos + 2 < input_ids.len() {
3599                        let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
3600                        let mut ok = d1 == input_ids[pos + 2];
3601                        Self::chain_probe_note(0, ok);
3602                        let mut d_prev = d1;
3603                        let mut extra = 0usize;
3604                        for j in 1..probe {
3605                            if pos + 2 + j >= input_ids.len() {
3606                                break;
3607                            }
3608                            let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
3609                            extra += 1;
3610                            ok = ok && dj == input_ids[pos + 2 + j];
3611                            Self::chain_probe_note(j, ok);
3612                            d_prev = dj;
3613                            hx = hj;
3614                        }
3615                        // The chain's rows are speculation, not the prompt —
3616                        // keep only the warmup row the plain path would add.
3617                        m.kv.truncate_last(extra);
3618                    } else {
3619                        let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
3620                    }
3621                }
3622            }
3623            pos += 1;
3624        }
3625        if std::env::var("CMF_PREFILL_PROF").is_ok() {
3626            eprintln!(
3627                "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
3628                input_ids.len(),
3629                _tpf.elapsed().as_secs_f64() * 1000.0
3630            );
3631        }
3632        if self
3633            .graph_failed
3634            .swap(false, std::sync::atomic::Ordering::Relaxed)
3635        {
3636            // MTP is detached for speculative generation.  Restore the
3637            // module before returning the terminal graph error; otherwise a
3638            // failed request would silently remove the head from a pooled
3639            // pipeline and the next request would lose its configured route.
3640            self.finish_generation(&mut mtp, &mut router, true);
3641            return Err("GPU token graph failed during prefill".to_string());
3642        }
3643        // Cancelled mid-prefill: the cache holds a partial prompt —
3644        // drop the reuse history and return an empty generation.
3645        if self
3646            .cancel
3647            .swap(false, std::sync::atomic::Ordering::Relaxed)
3648        {
3649            // A cancelled prefill can already have advanced the device
3650            // mirror. Drop the whole partial sequence so a pooled pipeline
3651            // cannot carry that state into its next request.
3652            self.finish_generation(&mut mtp, &mut router, true);
3653            return Ok(GenerateResult {
3654                text: String::new(),
3655                token_ids: Vec::new(),
3656                prompt_tokens: input_ids.len(),
3657                tokens_generated: 0,
3658                finish_reason: "cancelled".to_string(),
3659                mtp_drafted: 0,
3660                mtp_accepted: 0,
3661                token_confidence: Vec::new(),
3662                traces: Vec::new(),
3663            });
3664        }
3665
3666        // Prompt absorbed → freeze the o1 layers' skeletons; from here
3667        // every decode step on those layers is O(W + m·dv + m²).
3668        if !o1_sealed {
3669            match self.o1_seal_checked() {
3670                Ok(_) => {}
3671                Err(err) => {
3672                    self.finish_generation(&mut mtp, &mut router, true);
3673                    return Err(err);
3674                }
3675            }
3676        }
3677
3678        // Commit one token: push, check EOS, stream. Returns false = stop.
3679        macro_rules! commit {
3680            ($id:expr) => {{
3681                all_ids.push($id);
3682                generated += 1;
3683                self.note_draft_id($id);
3684                if self.tokenizer.is_eos($id) && !self.ignore_eos {
3685                    finish_reason = "stop".to_string();
3686                    false
3687                } else {
3688                    let token_text = self.tokenizer.decode_token($id);
3689                    let mut go = true;
3690                    if let Some(ref mut cb) = on_token {
3691                        if !cb(&token_text) {
3692                            finish_reason = "cancelled".to_string();
3693                            go = false;
3694                        }
3695                    }
3696                    go
3697                }
3698            }};
3699        }
3700
3701        // Speculation is decided by MEASUREMENT, not by an acceptance
3702        // model. A k=4 round costs ~3.8 plain tokens on the 5090 (draft
3703        // 6.6 + verify 66.6 + commit 4.8 ms against a 20.6 ms token), so it
3704        // pays only when the head lands ~2.8 of 4 — predictable text (code,
3705        // structured output) does, free prose often does not, and the
3706        // ratio at which the two cross depends on the card and the context
3707        // depth. So: four speculative rounds timed, then eight plain
3708        // tokens timed, and the faster arm runs until a re-check 256
3709        // tokens later (context growth moves the balance). The trial
3710        // costs at most a few tokens of the slower arm per 256.
3711        let mut spec_trial = SpecTrial::Spec {
3712            t0: std::time::Instant::now(),
3713            gen0: generated,
3714            rounds: 0,
3715        };
3716        // The token-count proxy prices a round at ~1.9 plain tokens. That
3717        // holds for the Metal rounds whose cost was measured — greedy and
3718        // the sparse sampling chain — so an expensive round (the dense
3719        // chain, reachable only by `CMF_GRAPH_SPEC_SAMPLE=1`) still times
3720        // the plain path before it decides.
3721        let mut spec_mon = SpecMon {
3722            metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
3723            ..SpecMon::default()
3724        };
3725        let mut spec_watchdog_off = false;
3726        // CMF_GRAPH_SPEC_TIME: the round walls so far (round 1 excluded —
3727        // it pays the scratch), for the outlier test on each new one
3728        let mut spec_walls: Vec<f32> = Vec::new();
3729        // ... and the end of the last round: the host time between rounds
3730        // (token commits, streaming, the loop top) is printed at level 2
3731        let mut spec_round_end: Option<std::time::Instant> = None;
3732        // ── Decode ──
3733        let mut next_pos = input_ids.len();
3734        'decode: while generated < max_tokens {
3735            if self
3736                .graph_failed
3737                .swap(false, std::sync::atomic::Ordering::Relaxed)
3738            {
3739                // Keep the detached MTP module attached after a terminal
3740                // graph error so the pipeline can be reused for a fresh
3741                // sequence.  `clear_sequence_state` only clears mirrors and
3742                // host KV; it cannot recover a module dropped here.
3743                self.finish_generation(&mut mtp, &mut router, true);
3744                return Err("GPU token graph failed during decode".to_string());
3745            }
3746            if self
3747                .cancel
3748                .swap(false, std::sync::atomic::Ordering::Relaxed)
3749            {
3750                finish_reason = "cancelled".to_string();
3751                break 'decode;
3752            }
3753            // A rejected speculative draft already drew this position's
3754            // token from the residual distribution (graph_spec_step); it
3755            // is committed as-is — sampling again from the row's logits
3756            // would bias the stream toward the target's mode.
3757            let forced = self.spec_forced.take();
3758            let mut logits = match (forced, self.graph_logits.take()) {
3759                (Some(_), _) => Vec::new(),
3760                (None, Some(lg)) => lg,
3761                (None, None) => {
3762                    inference::rms_norm_into(
3763                        &hidden,
3764                        &self.weights.final_norm,
3765                        self.rms_eps,
3766                        self.norm_style,
3767                        &mut self.ws.n1,
3768                    );
3769                    self.lm_head_forward(&self.ws.n1)
3770                }
3771            };
3772            // CMF_LOGIT_DUMP=<path>: the first decode step's hidden + logits
3773            // as raw f32 (hidden first) — cross-backend numerics diffing.
3774            if generated
3775                == std::env::var("CMF_LOGIT_DUMP_STEP")
3776                    .ok()
3777                    .and_then(|v| v.parse().ok())
3778                    .unwrap_or(0)
3779            {
3780                if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
3781                    let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
3782                    for v in hidden.iter().chain(logits.iter()) {
3783                        bytes.extend_from_slice(&v.to_le_bytes());
3784                    }
3785                    if let Err(e) = std::fs::write(&path, &bytes) {
3786                        eprintln!("logit dump: failed to write {path}: {e}");
3787                        self.finish_generation(&mut mtp, &mut router, true);
3788                        return Err(format!("logit dump write failed: {e}"));
3789                    }
3790                }
3791            }
3792            let t_next = match forced {
3793                Some(c) => c,
3794                None => sampler::sample_with_scratch_pool(
3795                    &logits,
3796                    &self.sampler_config,
3797                    &all_ids,
3798                    &mut self.rng,
3799                    &mut self.sampler_scratch,
3800                    self.pool.as_deref(),
3801                ),
3802            };
3803            if self.confidence_on {
3804                confidence.push(if logits.is_empty() {
3805                    0.0
3806                } else {
3807                    sampler::top1_prob_pool(
3808                        self.pool.as_deref(),
3809                        &mut self.sampler_scratch,
3810                        &logits,
3811                        t_next,
3812                        calib_temp,
3813                    )
3814                });
3815            }
3816            if !logits.is_empty() {
3817                attention::recycle_buf(&mut logits);
3818            }
3819            if trace_on {
3820                // active_skill = the overlay in force while this token was
3821                // generated; recon/switched are filled after the post-emit
3822                // routing eval below (freshest coherence for this token).
3823                let skill = router.as_ref().and_then(|r| r.active_id());
3824                traces.push(TokenTrace {
3825                    t: generated,
3826                    token_id: t_next,
3827                    confidence: confidence.last().copied().unwrap_or(0.0),
3828                    active_skill: skill,
3829                    recon: None,
3830                    switched: false,
3831                });
3832            }
3833            if !commit!(t_next) {
3834                break 'decode;
3835            }
3836            if generated >= max_tokens {
3837                break 'decode;
3838            }
3839
3840            if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
3841                // Say it ONCE, loudly: past this point the model keeps
3842                // talking but has lost half its context, and on a GDN
3843                // hybrid the graph's device state goes stale on top. The
3844                // Qwen3.8 bring-up spent a day reading this cliff as
3845                // three different model bugs.
3846                static SAID: std::sync::Once = std::sync::Once::new();
3847                SAID.call_once(|| {
3848                    tracing::warn!(
3849                        "KV cache full at {} positions — evicting half; quality \
3850                         will degrade. Raise CMF_MAX_SEQ.",
3851                        self.kv_cache.max_seq_len,
3852                    );
3853                });
3854                let keep = (self.kv_cache.max_seq_len / 2).max(1);
3855                self.kv_cache.evict(keep);
3856            }
3857
3858            // Advance the speculation trial: plain-phase accounting and
3859            // the periodic re-check happen here, on every token.
3860            if graph_spec {
3861                match spec_trial {
3862                    SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
3863                        spec_mon.plain_ms =
3864                            t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
3865                        let keep = spec_mon.pays();
3866                        tracing::info!(
3867                            "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3868                            spec_mon.tokens,
3869                            spec_mon.round_ms,
3870                            spec_mon.plain_ms,
3871                            if keep { "speculating" } else { "plain" }
3872                        );
3873                        spec_mon.fails = 0;
3874                        spec_trial = SpecTrial::Decided {
3875                            spec: keep,
3876                            recheck_at: if keep { usize::MAX } else { generated + 128 },
3877                        };
3878                    }
3879                    SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
3880                        spec_mon.n = 0;
3881                        spec_trial = SpecTrial::Spec {
3882                            t0: std::time::Instant::now(),
3883                            gen0: generated,
3884                            rounds: 0,
3885                        };
3886                    }
3887                    _ => {}
3888                }
3889                spec_watchdog_off = matches!(
3890                    spec_trial,
3891                    SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
3892                );
3893            }
3894            match &mut mtp {
3895                // ── Graph speculation: chain-draft, batch-verify on device ──
3896                #[cfg(feature = "gpu")]
3897                Some(m)
3898                    if graph_spec
3899                        && !spec_watchdog_off
3900                        && generated + 1 < max_tokens
3901                        && next_pos > 0 =>
3902                {
3903                    let t_round = std::time::Instant::now();
3904                    if spec_time_level() >= 2 {
3905                        if let Some(t) = spec_round_end.take() {
3906                            eprintln!(
3907                                "spec-gap {:.2} ms (host between rounds)",
3908                                t.elapsed().as_secs_f64() * 1e3
3909                            );
3910                        }
3911                    }
3912                    spec_stamps_begin();
3913                    // device buffers allocated during this round: a
3914                    // first-touch Shared allocation is zero-filled inside
3915                    // the command buffer that uses it, which is what the
3916                    // long outlier rounds were
3917                    #[cfg(target_os = "macos")]
3918                    let allocs0 = crate::gpu_metal::IO_BUF_ALLOCS
3919                        .load(std::sync::atomic::Ordering::Relaxed);
3920                    #[cfg(not(target_os = "macos"))]
3921                    let allocs0 = 0u64;
3922                    if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
3923                        m,
3924                        &hidden,
3925                        t_next,
3926                        next_pos,
3927                        &mut drafted,
3928                        &mut accepted,
3929                        &mut all_ids,
3930                        max_tokens - generated,
3931                    ) {
3932                        next_pos = n_pos;
3933                        hidden = new_h;
3934                        let level = spec_time_level();
3935                        if level > 0 {
3936                            let wall = t_round.elapsed().as_secs_f32() * 1e3;
3937                            let stamps = spec_stamps_take();
3938                            // the running median of the rounds before this
3939                            // one (round 1 pays the scratch: not a sample)
3940                            let median = if spec_walls.len() >= 3 {
3941                                let mut s = spec_walls.clone();
3942                                s.sort_by(|a, b| a.partial_cmp(b).unwrap());
3943                                Some(s[s.len() / 2])
3944                            } else {
3945                                None
3946                            };
3947                            let outlier = median.is_some_and(|m| wall > 1.4 * m);
3948                            #[cfg(target_os = "macos")]
3949                            let allocs = crate::gpu_metal::IO_BUF_ALLOCS
3950                                .load(std::sync::atomic::Ordering::Relaxed)
3951                                - allocs0;
3952                            #[cfg(not(target_os = "macos"))]
3953                            let allocs = allocs0;
3954                            eprintln!(
3955                                "spec-round wall {wall:.1} ms → {} tokens{}{}",
3956                                extra.len() + 1,
3957                                if allocs > 0 {
3958                                    format!(" [{allocs} new device buffers]")
3959                                } else {
3960                                    String::new()
3961                                },
3962                                match (outlier, median) {
3963                                    (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
3964                                    _ => String::new(),
3965                                }
3966                            );
3967                            if level >= 2 || outlier {
3968                                let sum: f32 = stamps.iter().map(|s| s.1).sum();
3969                                eprintln!(
3970                                    "spec-stamps: {}| untracked {:.1}",
3971                                    spec_stamps_format(&stamps),
3972                                    wall - sum
3973                                );
3974                            }
3975                            if spec_mon.n >= 1 {
3976                                spec_walls.push(wall);
3977                            }
3978                        }
3979                        // One speculative round done: the monitor counts it
3980                        // (round 1 untimed — it pays the batch scratch and
3981                        // the draft mirror), and the trial advances.
3982                        spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
3983                        // the round's tokens land in `generated` below; the
3984                        // plain phase must start counting AFTER them
3985                        spec_trial = Self::spec_trial_round(
3986                            spec_trial,
3987                            &mut spec_mon,
3988                            generated + extra.len() + 1,
3989                        );
3990                        let mut stopped = false;
3991                        for &id in &extra {
3992                            if self.confidence_on {
3993                                confidence.push(0.0);
3994                            }
3995                            if !commit!(id) {
3996                                stopped = true;
3997                                break;
3998                            }
3999                        }
4000                        if stopped {
4001                            break 'decode;
4002                        }
4003                        if spec_time_level() >= 2 {
4004                            spec_round_end = Some(std::time::Instant::now());
4005                        }
4006                        continue 'decode;
4007                    }
4008                    if self
4009                        .graph_failed
4010                        .swap(false, std::sync::atomic::Ordering::Relaxed)
4011                    {
4012                        // `graph_spec_step` may have detached MTP while a
4013                        // warm-up was in flight.  Do not reinterpret its
4014                        // terminal device failure as a plain decode step;
4015                        // restore the head, clear both mirrors, and surface
4016                        // one explicit error to the caller.
4017                        self.finish_generation(&mut mtp, &mut router, true);
4018                        return Err("GPU MTP graph failed during speculative decode".to_string());
4019                    }
4020                    // Declined (batch graph refused): plain forward below —
4021                    // and a round that produced one token for the trial's
4022                    // ledger, so a graph that keeps refusing is measured out
4023                    // like a head that keeps missing (it was spinning
4024                    // forever on a file whose batch graph declines).
4025                    // A declined round is not a cheap one-token round — it
4026                    // is a verify that does not exist for this file (a
4027                    // healed q8_2f tail measured 760 drafts, 0 accepted, 33
4028                    // against 48.8 tok/s while the monitor called the draft
4029                    // alone "paying"). Count it as the losing streak in one.
4030                    spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
4031                    spec_mon.tokens = 0.0;
4032                    spec_mon.fails = 3;
4033                    spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
4034                    hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
4035                    next_pos += 1;
4036                    continue 'decode;
4037                }
4038                // ── Speculative: draft t+2, verify in a fused pair ──
4039                Some(m) if !graph_spec && generated + 1 < max_tokens => {
4040                    let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
4041                    drafted += 1;
4042                    let emb1 = self.embed_single(t_next);
4043                    let emb2 = self.embed_single(draft);
4044                    let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
4045
4046                    inference::rms_norm_into(
4047                        &h1,
4048                        &self.weights.final_norm,
4049                        self.rms_eps,
4050                        self.norm_style,
4051                        &mut self.ws.n1,
4052                    );
4053                    let mut logits1 = self.lm_head_forward(&self.ws.n1);
4054                    let t_after = sampler::sample_with_scratch_pool(
4055                        &logits1,
4056                        &self.sampler_config,
4057                        &all_ids,
4058                        &mut self.rng,
4059                        &mut self.sampler_scratch,
4060                        self.pool.as_deref(),
4061                    );
4062                    if self.confidence_on {
4063                        confidence.push(sampler::top1_prob_pool(
4064                            self.pool.as_deref(),
4065                            &mut self.sampler_scratch,
4066                            &logits1,
4067                            t_after,
4068                            calib_temp,
4069                        ));
4070                    }
4071                    attention::recycle_buf(&mut logits1);
4072                    if trace_on {
4073                        // Speculative decode is mutually exclusive with
4074                        // dynamic routing (router is None here) — no skill.
4075                        traces.push(TokenTrace {
4076                            t: generated,
4077                            token_id: t_after,
4078                            confidence: confidence.last().copied().unwrap_or(0.0),
4079                            active_skill: None,
4080                            recon: None,
4081                            switched: false,
4082                        });
4083                    }
4084                    let stop = !commit!(t_after);
4085
4086                    if t_after == draft {
4087                        accepted += 1;
4088                        self.commit_linear_scratch();
4089                        let _ = self.mtp_step(m, &h1, t_after, next_pos);
4090                        hidden = h2;
4091                        next_pos += 2;
4092                    } else {
4093                        // The draft lane is wrong: roll its KV entry back.
4094                        for layer in &mut self.kv_cache.layers {
4095                            layer.truncate_last(1);
4096                        }
4097                        if !stop {
4098                            let _ = self.mtp_step(m, &h1, t_after, next_pos);
4099                            hidden = self.forward_layers(
4100                                &self.embed_single(t_after),
4101                                next_pos + 1,
4102                                None,
4103                            );
4104                        }
4105                        next_pos += 2;
4106                    }
4107                    if stop {
4108                        break 'decode;
4109                    }
4110                }
4111                // ── Vanilla: forward the sampled token ──
4112                _ => {
4113                    // ── DeepSeek-V4 speculative decode (CMF_DSV4_SPEC=1):
4114                    // draft five on the card, verify batched, commit the
4115                    // accepted prefix. Greedy only; a rejected token's state
4116                    // is restored and replayed, so output equals the walk. ──
4117                    #[cfg(feature = "gpu")]
4118                    if Self::dsv4_spec_on() && self.dsv4.is_some() {
4119                        static SAID: std::sync::Once = std::sync::Once::new();
4120                        SAID.call_once(|| {
4121                            eprintln!(
4122                                "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
4123                                !self.dsv4_mtp.is_empty(),
4124                                task_mask.is_none(),
4125                                router.is_none(),
4126                                !trace_on,
4127                                self.sampler_config.temperature < 1e-6,
4128                                self.sampler_config.repetition_penalty == 1.0,
4129                            );
4130                        });
4131                    }
4132                    #[cfg(feature = "gpu")]
4133                    if Self::dsv4_spec_on()
4134                        && self.dsv4.is_some()
4135                        && !self.dsv4_mtp.is_empty()
4136                        && task_mask.is_none()
4137                        && router.is_none()
4138                        && !trace_on
4139                        && self.sampler_config.temperature < 1e-6
4140                        && self.sampler_config.repetition_penalty == 1.0
4141                        && generated + 1 < max_tokens
4142                        && all_ids.len() >= 2
4143                        && generated >= dsv4_spec_retry_at
4144                    {
4145                        let tip_token = all_ids[all_ids.len() - 2];
4146                        let drafted0 = drafted;
4147                        let round = self.dsv4_spec_step(
4148                            tip_token,
4149                            t_next,
4150                            next_pos,
4151                            max_tokens.saturating_sub(generated),
4152                            &mut drafted,
4153                            &mut accepted,
4154                        );
4155                        if drafted > drafted0 {
4156                            let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
4157                            if useful {
4158                                dsv4_spec_bad = 0;
4159                            } else {
4160                                dsv4_spec_bad += 1;
4161                                if dsv4_spec_bad >= 2 {
4162                                    dsv4_spec_bad = 0;
4163                                    dsv4_spec_retry_at = generated.saturating_add(32);
4164                                    tracing::info!(
4165                                        "dsv4: draft не окупился дважды — точный walk на 32 токена"
4166                                    );
4167                                }
4168                            }
4169                        }
4170                        if let Some((extra, n_pos)) = round {
4171                            next_pos = n_pos;
4172                            let mut stopped = false;
4173                            for &id in &extra {
4174                                if self.confidence_on {
4175                                    confidence.push(0.0);
4176                                }
4177                                if !commit!(id) {
4178                                    stopped = true;
4179                                    break;
4180                                }
4181                            }
4182                            if stopped {
4183                                break 'decode;
4184                            }
4185                            continue 'decode;
4186                        }
4187                    }
4188                    self.graph_want_logits = fuse_lm;
4189                    // Greedy burst (CMF_MULTISTEP, default 8, 1 = off): while
4190                    // nothing observes per-token state — pure argmax sampling,
4191                    // no router/trace/confidence/mask — decode k tokens per
4192                    // submit and commit them wholesale. The trailing normal
4193                    // forward leaves logits for the loop top, as always.
4194                    let mut t_fwd = t_next;
4195                    let pure_greedy = self.sampler_config.temperature < 1e-6
4196                        && self.sampler_config.repetition_penalty == 1.0
4197                        && self.sampler_config.suppress_tokens.is_empty();
4198                    // Off by default: at every k the burst measured at or
4199                    // below the plain path on this graph shape (k=1 loses
4200                    // the argmax dispatches vs a 1 MB readback, k>=8 loses
4201                    // inter-step drains vs the saved sync). Experimental.
4202                    let burst_k = std::env::var("CMF_MULTISTEP")
4203                        .ok()
4204                        .and_then(|v| v.parse::<usize>().ok())
4205                        .unwrap_or(0);
4206                    if pure_greedy
4207                        && burst_k >= 1
4208                        && fuse_lm
4209                        && task_mask.is_none()
4210                        && router.is_none()
4211                        && !trace_on
4212                        && !self.confidence_on
4213                    {
4214                        let mut stopped = false;
4215                        loop {
4216                            let room = max_tokens.saturating_sub(generated);
4217                            if room <= 2 {
4218                                break;
4219                            }
4220                            let k = burst_k.min(room - 1);
4221                            if k < 1 {
4222                                break;
4223                            }
4224                            let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
4225                                if self
4226                                    .graph_failed
4227                                    .swap(false, std::sync::atomic::Ordering::Relaxed)
4228                                {
4229                                    self.finish_generation(&mut mtp, &mut router, true);
4230                                    return Err(
4231                                        "GPU token graph failed during greedy burst".to_string()
4232                                    );
4233                                }
4234                                break;
4235                            };
4236                            next_pos += k;
4237                            for &id in &ids {
4238                                if !commit!(id) {
4239                                    stopped = true;
4240                                    break;
4241                                }
4242                            }
4243                            if stopped {
4244                                break;
4245                            }
4246                            t_fwd = *ids.last().unwrap();
4247                        }
4248                        if stopped {
4249                            break 'decode;
4250                        }
4251                    }
4252                    // Metal: keep the draft head's cache in step through
4253                    // the trial's plain phase and a paused speculation —
4254                    // the pair (hidden, t_fwd) at next_pos−1, the step the
4255                    // round's draft 0 would take. Without it the head's
4256                    // cache lagged the trunk by every plain token for the
4257                    // rest of the generation: the batched warm-up declined
4258                    // every later round and its rows went one by one (a
4259                    // whole MTP step per accepted token), and the drafts
4260                    // attended a context with those tokens missing.
4261                    #[cfg(target_os = "macos")]
4262                    if graph_spec
4263                        && spec_watchdog_off
4264                        && next_pos > 0
4265                        && self.mtp_graph_mode == Some(true)
4266                        && crate::gpu::q1_force()
4267                    {
4268                        if let Some(m) = mtp.as_mut() {
4269                            let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
4270                        }
4271                    }
4272                    hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
4273                    next_pos += 1;
4274                    // Dynamic routing: the forward updated φ; ask the
4275                    // router whether to switch skills before the next token.
4276                    if let Some(r) = &mut router {
4277                        let phi = self.dyn_phi_ema.clone();
4278                        let decision = r.step(&phi, generated);
4279                        if let Some(new_active) = decision {
4280                            let _ = self.set_active_skill(new_active);
4281                        }
4282                        // Backfill this token's coherence + switch flag from
4283                        // the just-run eval (freshest measured values).
4284                        if trace_on {
4285                            if let Some(last) = traces.last_mut() {
4286                                let e = r.last_best_e();
4287                                last.recon = e.is_finite().then_some(e);
4288                                last.switched = decision.is_some();
4289                            }
4290                        }
4291                    }
4292                }
4293            }
4294        }
4295
4296        let cancelled = finish_reason == "cancelled";
4297        self.finish_generation(&mut mtp, &mut router, cancelled);
4298
4299        let output_ids = &all_ids[input_ids.len()..];
4300        // Forwarded = prompt + all generated but the LAST sampled token
4301        // (emitted without being fed back). Exact only without MTP —
4302        // reuse is gated off when MTP is active.
4303        let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
4304        if cancelled {
4305            self.kv_history.clear();
4306        } else {
4307            self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
4308        }
4309        confidence.truncate(output_ids.len()); // guard against any overshoot
4310        traces.truncate(output_ids.len());
4311        Ok(GenerateResult {
4312            text: self.tokenizer.decode(output_ids),
4313            token_ids: output_ids.to_vec(),
4314            prompt_tokens: input_ids.len(),
4315            tokens_generated: generated,
4316            finish_reason,
4317            mtp_drafted: drafted,
4318            mtp_accepted: accepted,
4319            token_confidence: confidence,
4320            traces,
4321        })
4322    }
4323
4324    /// One MTP step: feed `(hidden_p, token_{p+1})` into the draft head,
4325    /// advance its KV cache at position `p`, return the drafted token
4326    /// for position `p+2`.
4327    fn mtp_step(
4328        &mut self,
4329        m: &mut MtpModule,
4330        hidden: &[f32],
4331        next_token: u32,
4332        position: usize,
4333    ) -> u32 {
4334        self.mtp_step_h(m, hidden, next_token, position).0
4335    }
4336
4337    /// Tally for `CMF_MTP_CHAIN_PROBE`: per depth, how often the CHAIN is
4338    /// still an exact prefix of the real continuation. Printed every 128
4339    /// depth-0 samples so a killed run still shows its table.
4340    fn chain_probe_note(depth: usize, prefix_ok: bool) {
4341        use std::sync::Mutex;
4342        static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
4343        let mut t = T.lock().unwrap();
4344        if t.len() <= depth {
4345            t.resize(depth + 1, (0, 0));
4346        }
4347        t[depth].0 += 1;
4348        t[depth].1 += prefix_ok as u64;
4349        if depth == 0 && t[0].0 % 128 == 0 {
4350            let line: Vec<String> = t
4351                .iter()
4352                .enumerate()
4353                .map(|(d, (n, k))| {
4354                    format!(
4355                        "d{}={:.0}%({n})",
4356                        d + 1,
4357                        100.0 * *k as f64 / (*n).max(1) as f64
4358                    )
4359                })
4360                .collect();
4361            eprintln!("mtp-chain: {}", line.join(" "));
4362        }
4363    }
4364
4365    /// `mtp_step` that also hands back the block's own output hidden — the
4366    /// state a CHAINED draft feeds the next step, the way a multi-token
4367    /// speculative round iterates the head on itself.
4368    /// One MTP block step from (trunk hidden, token): the head's LOGITS
4369    /// and the block's own hidden for chaining. The draft is argmax of the
4370    /// logits on the greedy path and a draw from their post-chain
4371    /// distribution on the sampling path.
4372    fn mtp_step_hl(
4373        &mut self,
4374        m: &mut MtpModule,
4375        hidden: &[f32],
4376        next_token: u32,
4377        position: usize,
4378    ) -> (Vec<f32>, Vec<f32>) {
4379        // The graph arm: the MTP block as a one-layer token graph with the
4380        // head fused — device attention over the block's own KV mirror,
4381        // one submit for block + head, hidden and logits back together.
4382        // Decided once per generation (see `mtp_graph_mode`).
4383        #[cfg(target_os = "macos")]
4384        if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
4385            if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
4386                self.mtp_graph_mode = Some(true);
4387                return r;
4388            }
4389            if self.mtp_graph_mode == Some(true) {
4390                tracing::error!("mtp Metal graph failed after admission");
4391                self.clear_sequence_state();
4392                self.graph_failed
4393                    .store(true, std::sync::atomic::Ordering::Relaxed);
4394                self.cancel
4395                    .store(true, std::sync::atomic::Ordering::Relaxed);
4396                return (Vec::new(), Vec::new());
4397            }
4398            self.mtp_graph_mode = Some(false);
4399        }
4400        #[cfg(feature = "gpu")]
4401        if self.mtp_graph_mode != Some(false) {
4402            if !self.mtp_graph_ok(m) {
4403                if self.mtp_graph_mode == Some(true) {
4404                    // A mirror was already admitted, so a capability change
4405                    // cannot safely switch this request to the stale CPU
4406                    // cache.  Keep the same terminal contract as a failed
4407                    // token graph.
4408                    tracing::error!("mtp graph became unavailable after admission");
4409                    self.clear_sequence_state();
4410                    self.graph_failed
4411                        .store(true, std::sync::atomic::Ordering::Relaxed);
4412                    self.cancel
4413                        .store(true, std::sync::atomic::Ordering::Relaxed);
4414                    return (Vec::new(), Vec::new());
4415                }
4416                self.mtp_graph_mode = Some(false);
4417            } else {
4418                if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
4419                    self.mtp_graph_mode = Some(true);
4420                    return r;
4421                }
4422                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4423                    // A token graph can have admitted a persistent MTP/GDN
4424                    // mirror before its readback failed.  The CPU MTP cache
4425                    // is not a valid continuation in that state; leave the
4426                    // flag set so the generation caller returns through its
4427                    // terminal error path instead of silently switching
4428                    // arithmetic.
4429                    return (Vec::new(), Vec::new());
4430                }
4431                // `mtp_graph_ok` was true, so a None here means a refusal or
4432                // failure after graph admission.  Do not fall through to a
4433                // CPU cache whose rows may lag the device mirror.
4434                tracing::error!("mtp graph failed or declined after admission");
4435                self.clear_sequence_state();
4436                self.graph_failed
4437                    .store(true, std::sync::atomic::Ordering::Relaxed);
4438                self.cancel
4439                    .store(true, std::sync::atomic::Ordering::Relaxed);
4440                return (Vec::new(), Vec::new());
4441            }
4442        }
4443        // fc concat order is [enorm(embed); hnorm(hidden)] — EMBEDDING
4444        // FIRST. Verified by the oracle (converter/mtp_oracle.py):
4445        // [emb;hid] → 45.8% acceptance, [hid;emb] → 0.00%.
4446        let e = self.embed_single(next_token);
4447        let mut cat = vec![0.0f32; 2 * self.hidden_size];
4448        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4449        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4450        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4451        let mut x = vec![0.0f32; self.hidden_size];
4452        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4453
4454        // One standard transformer block over the MTP's own cache.
4455        let lw = &m.layer;
4456        inference::rms_norm_into(
4457            &x,
4458            &lw.input_norm,
4459            self.rms_eps,
4460            self.norm_style,
4461            &mut self.ws.n1,
4462        );
4463        let attn = match &lw.attn {
4464            // MLA models carry no MTP head; this path cannot see them.
4465            AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4466            AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4467            AttnKind::Full {
4468                wq,
4469                wk,
4470                wv,
4471                wo,
4472                q_norm,
4473                k_norm,
4474                output_gate,
4475                softplus_gate,
4476                bias,
4477            } => {
4478                let mut cfg = self.attn_cfg(position);
4479                cfg.q_norm = q_norm.as_deref();
4480                cfg.k_norm = k_norm.as_deref();
4481                cfg.output_gate = *output_gate;
4482                cfg.softplus_gate = softplus_gate
4483                    .as_ref()
4484                    .map(|(gate, per_head)| (gate, *per_head));
4485                cfg.bias = bias
4486                    .as_ref()
4487                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4488                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4489            }
4490            AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
4491                unreachable!("MTP block is full attention")
4492            }
4493        };
4494        for (i, &a) in attn.iter().enumerate() {
4495            x[i] += a;
4496        }
4497        inference::rms_norm_into(
4498            &x,
4499            &lw.post_norm,
4500            self.rms_eps,
4501            self.norm_style,
4502            &mut self.ws.p1,
4503        );
4504        let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
4505        for (i, &f) in ffn.iter().enumerate() {
4506            x[i] += f;
4507        }
4508
4509        inference::rms_norm_into(
4510            &x,
4511            &m.final_norm,
4512            self.rms_eps,
4513            self.norm_style,
4514            &mut self.ws.n1,
4515        );
4516        let lg = self.lm_head_forward(&self.ws.n1);
4517        (lg, x)
4518    }
4519
4520    /// `mtp_step_hl` reduced to the greedy draft: argmax of the head.
4521    fn mtp_step_h(
4522        &mut self,
4523        m: &mut MtpModule,
4524        hidden: &[f32],
4525        next_token: u32,
4526        position: usize,
4527    ) -> (u32, Vec<f32>) {
4528        let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
4529        let draft = sampler::argmax(&lg);
4530        attention::recycle_buf(&mut lg);
4531        (draft, x)
4532    }
4533
4534    /// One speculative round for the trial: rounds 1..5 of a `Spec` phase
4535    /// advance it (the monitor already averaged this round); after five,
4536    /// the plain phase runs (once — a known plain rate decides at once);
4537    /// a decided speculation keeps re-checking the rule every round and
4538    /// stops after four losing rounds in a row.
4539    fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
4540        match trial {
4541            SpecTrial::Spec { t0, gen0, rounds } => {
4542                let rounds = rounds + 1;
4543                if rounds >= 5 {
4544                    if mon.plain_ms > 0.0 {
4545                        let keep = mon.pays();
4546                        mon.fails = 0;
4547                        tracing::info!(
4548                            "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4549                            mon.tokens,
4550                            mon.round_ms,
4551                            mon.plain_ms,
4552                            if keep { "speculating" } else { "plain" }
4553                        );
4554                        SpecTrial::Decided {
4555                            spec: keep,
4556                            recheck_at: if keep { usize::MAX } else { generated + 128 },
4557                        }
4558                    } else if mon.pays() {
4559                        // Metal: the rounds land enough tokens each that no
4560                        // plain measurement is needed — keep speculating,
4561                        // and re-check every round (a losing streak sends
4562                        // the loop to the plain phase, below).
4563                        mon.fails = 0;
4564                        tracing::info!(
4565                            "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
4566                            mon.tokens,
4567                            mon.round_ms,
4568                        );
4569                        SpecTrial::Decided {
4570                            spec: true,
4571                            recheck_at: usize::MAX,
4572                        }
4573                    } else {
4574                        SpecTrial::Plain {
4575                            t0: std::time::Instant::now(),
4576                            gen0: generated,
4577                        }
4578                    }
4579                } else {
4580                    SpecTrial::Spec { t0, gen0, rounds }
4581                }
4582            }
4583            SpecTrial::Decided { spec: true, .. } => {
4584                if mon.pays() {
4585                    mon.fails = 0;
4586                    trial
4587                } else {
4588                    mon.fails += 1;
4589                    if mon.fails >= 4 {
4590                        if mon.plain_ms <= 0.0 {
4591                            // Metal, plain never timed: four doubtful rounds
4592                            // buy the (bounded) plain measurement, and the
4593                            // exact rule decides from it.
4594                            tracing::info!(
4595                                "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
4596                                mon.tokens,
4597                                mon.round_ms,
4598                            );
4599                            return SpecTrial::Plain {
4600                                t0: std::time::Instant::now(),
4601                                gen0: generated,
4602                            };
4603                        }
4604                        tracing::info!(
4605                            "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
4606                            mon.tokens,
4607                            mon.round_ms,
4608                            mon.plain_ms
4609                        );
4610                        SpecTrial::Decided {
4611                            spec: false,
4612                            recheck_at: generated + 128,
4613                        }
4614                    } else {
4615                        trial
4616                    }
4617                }
4618            }
4619            other => other,
4620        }
4621    }
4622
4623    /// The MTP block's device-mirror id: the trunk's id with a high bit,
4624    /// so the (kv_id, layer) mirror keys never collide.
4625    fn mtp_kv_id(&self) -> u64 {
4626        self.graph_kv_id | (1u64 << 40)
4627    }
4628
4629    /// The MTP block's mirror layer index: 0 — its own kv_id keeps it
4630    /// apart from the trunk, and the BATCH graph (the warm-up path) keys
4631    /// its mirrors at layer 0 with no base of its own, so the draft's
4632    /// token graph must key the same slot.
4633    const MTP_LAYER_BASE: usize = 0;
4634
4635    /// The wgpu MTP draft writes speculative rows straight into its device
4636    /// mirror while the CPU owner retains only the real prompt/decode anchor.
4637    /// After verification, move that mirror cursor back to the anchor before
4638    /// replaying accepted pairs.  The next graph append then sees the same
4639    /// contiguous position as the CPU/Metal path without uploading stale
4640    /// speculative rows.
4641    #[cfg(feature = "gpu")]
4642    fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
4643        self.mtp_graph_mode != Some(true)
4644            || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
4645    }
4646
4647    /// A speculative verify graph appends the full `k+1` trunk rows before
4648    /// the acceptance count is known.  GDN state already has a snapshot
4649    /// restore; Full-attention mirrors need the matching logical cursor
4650    /// rewind so the next graph call does not reject an ahead-of-position KV
4651    /// cache after a partial acceptance.
4652    #[cfg(feature = "gpu")]
4653    fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
4654        let mut ok = true;
4655        let mut expected = false;
4656        for li in 0..self.num_layers {
4657            if matches!(
4658                self.weights.layers[self.phys_layer(li)].attn,
4659                AttnKind::Full { .. }
4660            ) {
4661                expected = true;
4662                ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
4663            }
4664        }
4665        !expected || ok
4666    }
4667
4668    /// Count the recurrent layers participating in the trunk verify graph.
4669    /// Snapshot restore is all-or-nothing across that set; deriving the count
4670    /// from the model keeps the restore contract valid for looped models too.
4671    fn graph_gdn_layer_count(&self) -> usize {
4672        (0..self.num_layers)
4673            .filter(|&li| {
4674                matches!(
4675                    &self.weights.layers[self.phys_layer(li)].attn,
4676                    AttnKind::LinearGdn(_)
4677                )
4678            })
4679            .count()
4680    }
4681
4682    /// The block's input from (trunk hidden, token): eh_proj · [enorm(e);
4683    /// hnorm(h)] — the same arithmetic the per-op path starts with.
4684    fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
4685        let e = self.embed_single(next_token);
4686        let mut cat = vec![0.0f32; 2 * self.hidden_size];
4687        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4688        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4689        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4690        let mut x = vec![0.0f32; self.hidden_size];
4691        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4692        x
4693    }
4694
4695    /// Is the MTP block graphable at all (device up, full attention
4696    /// without softplus, dense FFN)? The plan itself is built per call.
4697    #[cfg(feature = "gpu")]
4698    fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
4699        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
4700            return false;
4701        }
4702        if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
4703            || !crate::gpu::enabled_here()
4704            || self.attn_softcap > 0.0
4705            || self.attention_heads_per_layer.is_some()
4706        {
4707            return false;
4708        }
4709        matches!(
4710            &m.layer.attn,
4711            AttnKind::Full {
4712                softplus_gate: None,
4713                ..
4714            }
4715        ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
4716    }
4717
4718    /// Full MTP token-graph eligibility, including the fused lm-head and all
4719    /// block projection weights.  Keep this distinct from the block-only
4720    /// check: prompt warm-up does not need the head, while a draft step does.
4721    #[cfg(feature = "gpu")]
4722    fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
4723        if !self.mtp_block_graph_ok(m) {
4724            return false;
4725        }
4726        let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
4727            return false;
4728        };
4729        let FfnKind::Dense(d) = &m.layer.ffn else {
4730            return false;
4731        };
4732        d.segs.is_empty()
4733            && wq.graph_weight().is_some()
4734            && wk.graph_weight().is_some()
4735            && wv.graph_weight().is_some()
4736            && wo.graph_weight().is_some()
4737            && d.gate_proj.graph_weight().is_some()
4738            && d.up_proj.graph_weight().is_some()
4739            && d.down_proj.graph_weight().is_some()
4740            && self.weights.lm_head.graph_weight().is_some()
4741    }
4742
4743    /// One MTP block step on the wgpu token graph: block + fused head in
4744    /// one submit, the block hidden and the logits read back together.
4745    /// None = the graph cannot take this block (softplus gate, non-dense
4746    /// FFN, unquantized head, no device) — the caller keeps the per-op
4747    /// path for the whole generation.
4748    #[cfg(feature = "gpu")]
4749    fn mtp_step_graph(
4750        &mut self,
4751        m: &mut MtpModule,
4752        hidden: &[f32],
4753        next_token: u32,
4754        position: usize,
4755    ) -> Option<(Vec<f32>, Vec<f32>)> {
4756        if !self.mtp_graph_ok(m) {
4757            return None;
4758        }
4759        let lw = &m.layer;
4760        let AttnKind::Full {
4761            wq,
4762            wk,
4763            wv,
4764            wo,
4765            q_norm,
4766            k_norm,
4767            output_gate,
4768            softplus_gate,
4769            bias,
4770        } = &lw.attn
4771        else {
4772            return None;
4773        };
4774        if softplus_gate.is_some() {
4775            return None;
4776        }
4777        let FfnKind::Dense(d) = &lw.ffn else {
4778            return None;
4779        };
4780        if !d.segs.is_empty() {
4781            return None; // tube layers run on the segmented path
4782        }
4783        // The block's input first: it borrows `self` mutably (embed scratch,
4784        // pool), the plan below borrows the weights immutably.
4785        let mut x = self.mtp_block_input(m, hidden, next_token);
4786        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4787            let (_, i, kind, rs) = t.graph_weight()?;
4788            Some(crate::gpu::GraphW {
4789                idx: i,
4790                kind,
4791                row_scale: rs,
4792                data: &[],
4793                prism: crate::gpu::GraphPrismOp::None,
4794                affine: false,
4795            })
4796        }
4797        let (model, _, _, _) = wq.graph_weight()?;
4798        let model = model.clone();
4799        let (lm_gw, lm_rows) = {
4800            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4801            // The draft's head over the CMF_DRAFT_VOCAB shortlist (the same
4802            // cut the native Metal draft takes): 662 MB a step on Qwen3.8
4803            // becomes 170 MB at 65536; the verify keeps the full head.
4804            let rows = if kind == 6 {
4805                self.draft_head_rows(self.weights.lm_head.rows())
4806            } else {
4807                self.weights.lm_head.rows()
4808            };
4809            (
4810                crate::gpu::GraphW {
4811                    idx: i,
4812                    kind,
4813                    row_scale: rs,
4814                    data: &[],
4815                    prism: crate::gpu::GraphPrismOp::None,
4816                    affine: false,
4817                },
4818                rows,
4819            )
4820        };
4821        let layer = crate::gpu::GraphLayer {
4822            input_norm: &lw.input_norm,
4823            attn: crate::gpu::GraphAttn::Full {
4824                wq: gw(wq)?,
4825                wk: gw(wk)?,
4826                wv: gw(wv)?,
4827                wo: gw(wo)?,
4828                q_norm: q_norm.as_deref(),
4829                k_norm: k_norm.as_deref(),
4830                late_qk_norm: self.qk_norm_after_rope,
4831                bias: bias
4832                    .as_ref()
4833                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4834                output_gate: *output_gate,
4835                cpu_k: m.kv.k_heads(),
4836                cpu_v: m.kv.v_heads(),
4837            },
4838            post_norm: &lw.post_norm,
4839            ffn: crate::gpu::GraphFfn::Dense {
4840                gate: gw(&d.gate_proj)?,
4841                up: gw(&d.up_proj)?,
4842                down: gw(&d.down_proj)?,
4843            },
4844        };
4845        let nh = self.num_heads;
4846        let (nkv, hd, rd) = self.layer_geom(0);
4847        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4848        let mut logits = Vec::new();
4849        let ok = crate::gpu::forward_token_graph(
4850            &model,
4851            self.mtp_kv_id(),
4852            std::slice::from_ref(&layer),
4853            &[None],
4854            self.o1_epoch,
4855            &self.inv_freq,
4856            &mut x,
4857            nh,
4858            nkv,
4859            hd,
4860            self.attn_scale,
4861            rd,
4862            self.hidden_size,
4863            self.intermediate_size,
4864            position,
4865            self.kv_cache.max_seq_len,
4866            gemma,
4867            self.rms_eps as f32,
4868            Some((&lm_gw, lm_rows)),
4869            &m.final_norm,
4870            &mut logits,
4871            &[],
4872            1,
4873            None,
4874            None,
4875            None,
4876            Self::MTP_LAYER_BASE,
4877            true,
4878        );
4879        match ok {
4880            crate::gpu::TokenGraphOutcome::Completed => {}
4881            crate::gpu::TokenGraphOutcome::Declined => return None,
4882            crate::gpu::TokenGraphOutcome::Failed => {
4883                // The backend has already admitted persistent state.  Keep
4884                // this distinct from a capability refusal so the caller
4885                // cannot switch to the stale CPU MTP cache.
4886                self.clear_sequence_state();
4887                self.graph_failed
4888                    .store(true, std::sync::atomic::Ordering::Relaxed);
4889                self.cancel
4890                    .store(true, std::sync::atomic::Ordering::Relaxed);
4891                return None;
4892            }
4893        }
4894        logits.resize(self.vocab_size, 0.0);
4895        Some((logits, x))
4896    }
4897
4898    /// The warm-ups of one speculative round on the device: every accepted
4899    /// (hidden, token) pair as ONE batched graph run over the MTP block
4900    /// (no head) — its kv_append lands the pairs in the block's mirror.
4901    /// `pairs` are consecutive positions from `first_pos`.  The tri-state
4902    /// result is intentional: a refusal before admission may use the
4903    /// per-row/CPU route, while a failure after admission must terminate the
4904    /// sequence rather than fall through to a stale CPU cache.
4905    #[cfg(feature = "gpu")]
4906    fn mtp_warm_graph(
4907        &mut self,
4908        m: &mut MtpModule,
4909        pairs: &[(&[f32], u32)],
4910        first_pos: usize,
4911    ) -> crate::gpu::BatchGraphOutcome {
4912        if pairs.is_empty() {
4913            return crate::gpu::BatchGraphOutcome::Completed;
4914        }
4915        if !self.mtp_block_graph_ok(m) {
4916            return crate::gpu::BatchGraphOutcome::Declined;
4917        }
4918        let hs = self.hidden_size;
4919        // Block inputs for every pair (eh_proj on the per-op path, one
4920        // matvec each — the plan's own prologue).
4921        let mut hiddens = Vec::with_capacity(pairs.len() * hs);
4922        for (h, t) in pairs {
4923            hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
4924        }
4925        let lw = &m.layer;
4926        let AttnKind::Full {
4927            wq,
4928            wk,
4929            wv,
4930            wo,
4931            q_norm,
4932            k_norm,
4933            output_gate,
4934            bias,
4935            ..
4936        } = &lw.attn
4937        else {
4938            return crate::gpu::BatchGraphOutcome::Declined;
4939        };
4940        let FfnKind::Dense(d) = &lw.ffn else {
4941            return crate::gpu::BatchGraphOutcome::Declined;
4942        };
4943        if !d.segs.is_empty() {
4944            return crate::gpu::BatchGraphOutcome::Declined; // tube layers run on the segmented path
4945        }
4946        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4947            let (_, i, kind, rs) = t.graph_weight()?;
4948            Some(crate::gpu::GraphW {
4949                idx: i,
4950                kind,
4951                row_scale: rs,
4952                data: &[],
4953                prism: crate::gpu::GraphPrismOp::None,
4954                affine: false,
4955            })
4956        }
4957        let Some((model, _, _, _)) = wq.graph_weight() else {
4958            return crate::gpu::BatchGraphOutcome::Declined;
4959        };
4960        let model = model.clone();
4961        let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
4962            gw(wq),
4963            gw(wk),
4964            gw(wv),
4965            gw(wo),
4966            gw(&d.gate_proj),
4967            gw(&d.up_proj),
4968            gw(&d.down_proj),
4969        ) else {
4970            return crate::gpu::BatchGraphOutcome::Declined;
4971        };
4972        let layer = crate::gpu::GraphLayer {
4973            input_norm: &lw.input_norm,
4974            attn: crate::gpu::GraphAttn::Full {
4975                wq: gwq,
4976                wk: gwk,
4977                wv: gwv,
4978                wo: gwo,
4979                q_norm: q_norm.as_deref(),
4980                k_norm: k_norm.as_deref(),
4981                late_qk_norm: self.qk_norm_after_rope,
4982                bias: bias
4983                    .as_ref()
4984                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4985                output_gate: *output_gate,
4986                cpu_k: m.kv.k_heads(),
4987                cpu_v: m.kv.v_heads(),
4988            },
4989            post_norm: &lw.post_norm,
4990            ffn: crate::gpu::GraphFfn::Dense {
4991                gate: gg,
4992                up: gu,
4993                down: gd,
4994            },
4995        };
4996        let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
4997        let nh = self.num_heads;
4998        let (nkv, hd, rd) = self.layer_geom(0);
4999        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5000        crate::gpu::forward_batch_graph(
5001            &model,
5002            self.mtp_kv_id(),
5003            std::slice::from_ref(&layer),
5004            &self.inv_freq,
5005            &mut hiddens,
5006            nh,
5007            nkv,
5008            hd,
5009            rd,
5010            hs,
5011            self.intermediate_size,
5012            &positions,
5013            self.kv_cache.max_seq_len,
5014            gemma,
5015            self.rms_eps as f32,
5016            self.attn_scale,
5017            pairs.len(),
5018            &[],
5019            0,
5020            None,
5021        )
5022    }
5023
5024    /// Complete an MTP warm-up after the batched graph has refused.  A
5025    /// graphable block is retried one row at a time; once any device row has
5026    /// been admitted, a CPU fallback would observe a stale mirror, so every
5027    /// token-graph refusal is terminal.  If the block is not graphable and no
5028    /// mirror exists yet, warming on the CPU is safe and records the CPU mode
5029    /// for the rest of the generation.
5030    #[cfg(feature = "gpu")]
5031    fn mtp_warm_graph_fallback(
5032        &mut self,
5033        m: &mut MtpModule,
5034        pairs: &[(&[f32], u32)],
5035        first_pos: usize,
5036    ) -> bool {
5037        if pairs.is_empty() {
5038            return true;
5039        }
5040        let graphable = self.mtp_block_graph_ok(m);
5041        if !graphable {
5042            // A previously admitted mirror cannot be made coherent by
5043            // appending to the host cache.  The caller turns this into a
5044            // terminal generation error and clears both mirrors.
5045            if self.mtp_graph_mode == Some(true) {
5046                return false;
5047            }
5048            self.mtp_graph_mode = Some(false);
5049            for (j, (h, t)) in pairs.iter().enumerate() {
5050                self.mtp_warm(m, h, *t, first_pos + j);
5051            }
5052            return true;
5053        }
5054
5055        // The batch refusal is recoverable only through the same device
5056        // state.  Keep rows owned until each token graph has completed; a
5057        // None is treated as unsafe because the token-graph API deliberately
5058        // collapses its backend refusal/failure into that result.
5059        for (j, (h, t)) in pairs.iter().enumerate() {
5060            if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
5061                return false;
5062            }
5063        }
5064        self.mtp_graph_mode = Some(true);
5065        true
5066    }
5067
5068    /// Warm a contiguous set of MTP pairs using the existing graph seam, with
5069    /// an all-or-nothing error contract for callers that already admitted the
5070    /// trunk batch.  The non-GPU build keeps the same pair accounting while
5071    /// using the established CPU warm path.
5072    #[cfg(feature = "gpu")]
5073    fn mtp_warm_prefill_pairs(
5074        &mut self,
5075        m: &mut MtpModule,
5076        pairs: &[(&[f32], u32)],
5077        first_pos: usize,
5078    ) -> Result<(), &'static str> {
5079        // Keep unsupported token-graph heads on the established CPU MTP
5080        // route before admitting any block mirror.  Once a device mirror is
5081        // active, the same condition is terminal because CPU rows cannot
5082        // repair its state.
5083        if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
5084            if self.mtp_graph_mode == Some(true) {
5085                return Err("MTP token graph became unavailable after admission");
5086            }
5087            self.mtp_graph_mode = Some(false);
5088            for (j, (h, t)) in pairs.iter().enumerate() {
5089                self.mtp_warm(m, h, *t, first_pos + j);
5090            }
5091            return Ok(());
5092        }
5093        match self.mtp_warm_graph(m, pairs, first_pos) {
5094            crate::gpu::BatchGraphOutcome::Completed => {
5095                if !pairs.is_empty() {
5096                    self.mtp_graph_mode = Some(true);
5097                }
5098                Ok(())
5099            }
5100            crate::gpu::BatchGraphOutcome::Declined => {
5101                if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
5102                    Ok(())
5103                } else {
5104                    Err("MTP warm-up fallback failed after device admission")
5105                }
5106            }
5107            crate::gpu::BatchGraphOutcome::Failed => {
5108                Err("MTP warm batch graph failed after admission")
5109            }
5110        }
5111    }
5112
5113    #[cfg(not(feature = "gpu"))]
5114    fn mtp_warm_prefill_pairs(
5115        &mut self,
5116        m: &mut MtpModule,
5117        pairs: &[(&[f32], u32)],
5118        first_pos: usize,
5119    ) -> Result<(), &'static str> {
5120        for (j, (h, t)) in pairs.iter().enumerate() {
5121            self.mtp_warm(m, h, *t, first_pos + j);
5122        }
5123        Ok(())
5124    }
5125
5126    /// The MTP block alone — advance its KV with a (hidden, token) pair the
5127    /// verify just proved, without paying the head. What keeps the draft's
5128    /// attention context warm between speculative rounds.
5129    fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
5130        let e = self.embed_single(next_token);
5131        let mut cat = vec![0.0f32; 2 * self.hidden_size];
5132        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
5133        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
5134        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
5135        let mut x = vec![0.0f32; self.hidden_size];
5136        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
5137        inference::rms_norm_into(
5138            &x,
5139            &m.layer.input_norm,
5140            self.rms_eps,
5141            self.norm_style,
5142            &mut self.ws.n1,
5143        );
5144        let attn = match &m.layer.attn {
5145            AttnKind::Full {
5146                wq,
5147                wk,
5148                wv,
5149                wo,
5150                q_norm,
5151                k_norm,
5152                output_gate,
5153                softplus_gate,
5154                bias,
5155            } => {
5156                let mut cfg = self.attn_cfg(position);
5157                cfg.q_norm = q_norm.as_deref();
5158                cfg.k_norm = k_norm.as_deref();
5159                cfg.output_gate = *output_gate;
5160                cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
5161                cfg.bias = bias
5162                    .as_ref()
5163                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
5164                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
5165            }
5166            _ => return,
5167        };
5168        let _ = attn;
5169    }
5170
5171    /// Speculative decode ON the wgpu whole-token graph: draft k with the
5172    /// MTP head, verify all of them plus the tip in ONE batched graph
5173    /// submit whose tail folds the head, commit the accepted prefix and
5174    /// roll the GDN state back to the last real position. Greedy only —
5175    /// output equals the plain graph's token for token, the way the DSV4
5176    /// verify equals the walk.
5177    #[cfg(feature = "gpu")]
5178    #[allow(clippy::too_many_arguments)]
5179    fn graph_spec_step(
5180        &mut self,
5181        m: &mut MtpModule,
5182        hidden: &[f32],
5183        t_next: u32,
5184        next_pos: usize,
5185        drafted: &mut usize,
5186        accepted: &mut usize,
5187        // The committed stream (prompt + generated so far, `t_next`
5188        // included): the sampler chain's penalties read it, and the
5189        // sampling arm extends it with the drafts position by position.
5190        all_ids: &mut Vec<u32>,
5191        // Tokens left before `max_tokens`. A round commits up to k
5192        // accepted drafts, and those positions are already in the cache,
5193        // so the depth is capped here — trimming the output afterwards
5194        // would leave cache rows the committed stream does not have.
5195        room: usize,
5196    ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
5197        // 3 is the measured optimum on Qwen3.6-27B / RTX 5090 (medians
5198        // of three, greedy): 51.1 tok/s against a plain 49.4, where k=2
5199        // gives 46.1, k=4 50.0, k=5 47.4, k=6 45.2. Acceptance is 89-91%
5200        // throughout — what turns the curve over is the verify, which
5201        // costs ~7.4 ms per extra position, and the draft ~3 ms a step.
5202        // 4 since the draft moved onto the graph (Qwen3.8-27B / 5090:
5203        // k=3 51.2, k=4 51.8 with the per-op draft; the graph draft
5204        // halves the draft cost, so the extra draft is cheaper still).
5205        // 5 with the int8 verify (the default: measured 76.5 against
5206        // k=4's 72-74 and k=6's 74 on the 5090), 4 with the f32 one.
5207        #[cfg(target_os = "macos")]
5208        let metal_native = crate::gpu::q1_force();
5209        #[cfg(not(target_os = "macos"))]
5210        let metal_native = false;
5211        #[cfg(feature = "gpu")]
5212        let k_default = if metal_native {
5213            // the Metal verify's GEMM tile is 8 rows wide and flat in b:
5214            // seven drafts + the tip fill it for free
5215            7
5216        } else if crate::gpu_wgpu::verify_i8_on() {
5217            5
5218        } else {
5219            4
5220        };
5221        #[cfg(not(feature = "gpu"))]
5222        let k_default = 4;
5223        let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
5224            .ok()
5225            .and_then(|v| v.parse().ok())
5226            .filter(|&v| (1..=8).contains(&v));
5227        // Adaptive depth: start below the card's flat-verify optimum and
5228        // let the accepted fraction move it — predictable text climbs to
5229        // the old default within a few rounds, prose settles at 2-3 where
5230        // the shorter verify pays.
5231        let (k_start, k_max) = if metal_native { (7, 7) } else { (3, k_default.max(5)) };
5232        let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
5233        let k_spec = k_full.min(room).max(1);
5234        // a tail round cut short by `room` says nothing about the text:
5235        // it must not move the adaptive depth the next request starts at
5236        let k_capped = k_spec < k_full;
5237        if next_pos == 0 {
5238            return None;
5239        }
5240        let t_round = std::time::Instant::now();
5241        // Submissions per phase — and they say where the round's money is.
5242        // Qwen3.6-27B on an RTX 5090, k=3:
5243        //
5244        //   draft   9.3 ms / 12 submissions   (four per MTP step)
5245        //   verify 52.8 ms /  1               (the batched graph)
5246        //   commit  5.4 ms /  6               (two per warm)
5247        //
5248        // The verify is already one submit. The draft's own work is 834 MB
5249        // a step — 0.8 ms at this card's measured 1056 GB/s — against 3.1
5250        // ms measured, so ~0.58 ms of every step is round trip, not
5251        // arithmetic, and the same holds for the warms. Eighteen round
5252        // trips a round at roughly half a millisecond each is ~11 ms of a
5253        // 68 ms round: fusing the MTP block into ONE submit the way the
5254        // trunk already is projects to ~64 tok/s against today's 50.9.
5255        // That is the largest measured item left on this path.
5256        let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
5257        let sub0 = subs();
5258        // Greedy without penalties verifies by argmax equality (bit-exact
5259        // against the plain path). Anything else is speculative SAMPLING:
5260        // each draft is a DRAW from the MTP head's post-chain distribution
5261        // q_j, kept for the accept test; the verify's rows give p_j.
5262        let cfg = self.sampler_config.clone();
5263        let penalized = !(cfg.repetition_penalty == 1.0
5264            && cfg.presence_penalty == 0.0
5265            && cfg.suppress_tokens.is_empty());
5266        // Three verify regimes: plain greedy (argmax of the raw rows),
5267        // greedy WITH penalties (argmax of the penalized rows — a single
5268        // pass each, no distributions), and sampling (draw / accept /
5269        // correct on post-chain distributions).
5270        let greedy_pen = cfg.temperature < 1e-6 && penalized;
5271        let sampling = cfg.temperature >= 1e-6;
5272        // Sampling with a top-k goes through the SPARSE chain: the dense
5273        // one builds nine 248k-float distributions a round (four drafts,
5274        // five verify rows) and measured 19-22 tok/s against a plain 40 —
5275        // the host, not the card. Sparse, the same nine cost tens of
5276        // microseconds each.
5277        let sparse = sampling && sampler::sparse_ok(&cfg);
5278        let base_len = all_ids.len();
5279        if sampling && !sparse && self.spec_q.len() < k_spec {
5280            self.spec_q.resize_with(k_spec, Vec::new);
5281        }
5282        if sparse && self.spec_qs.len() < k_spec {
5283            self.spec_qs.resize_with(k_spec, Vec::new);
5284        }
5285        // Draft the chain: first from the trunk's tip hidden, then the head
5286        // iterating on itself. Rows land in the MTP KV; the chain rows past
5287        // the first are speculation over speculative state and roll back
5288        // below, replaced by verified pairs.
5289        let mut drafts = Vec::with_capacity(k_spec);
5290        let mut hx = hidden.to_vec();
5291        // CMF_SPEC_DBG=1: draft 0 through BOTH MTP arms (graph and per-op)
5292        // from the same inputs — are the arms the difference, or the inputs?
5293        let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
5294        spec_stamp("pro");
5295        // Plain greedy on native Metal: the whole chain as one command
5296        // buffer (device argmax + embedding gather between the steps).
5297        // A decline before commit hands the round to the per-step loop
5298        // below; a failure after commit is terminal, like any graph
5299        // failure after admission.
5300        #[cfg(target_os = "macos")]
5301        if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
5302            match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
5303                Ok(ids) => {
5304                    self.mtp_graph_mode = Some(true);
5305                    drafts = ids;
5306                }
5307                Err(true) => {
5308                    tracing::error!("mtp Metal draft chain failed after commit");
5309                    self.clear_sequence_state();
5310                    self.graph_failed
5311                        .store(true, std::sync::atomic::Ordering::Relaxed);
5312                    self.cancel
5313                        .store(true, std::sync::atomic::Ordering::Relaxed);
5314                    return None;
5315                }
5316                Err(false) => {}
5317            }
5318        }
5319        for j in drafts.len()..k_spec {
5320            let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
5321            let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
5322            if spec_dbg {
5323                let saved = self.mtp_graph_mode;
5324                self.mtp_graph_mode = Some(false);
5325                let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
5326                self.mtp_graph_mode = saved;
5327                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
5328                    return None;
5329                }
5330                m.kv.truncate_last(1);
5331                dbg_ref = Some(r);
5332            }
5333            let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
5334            if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
5335                return None;
5336            }
5337            if let Some((lg_cpu, h_cpu)) = dbg_ref {
5338                let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
5339                let dl = lg
5340                    .iter()
5341                    .zip(&lg_cpu)
5342                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
5343                let dh = hj
5344                    .iter()
5345                    .zip(&h_cpu)
5346                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
5347                eprintln!(
5348                    "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 {}",
5349                    next_pos - 1 + j,
5350                    sampler::argmax(&lg_cpu),
5351                    sampler::argmax(&lg),
5352                    n(&h_cpu),
5353                    n(&hj),
5354                    m.kv.seq_len
5355                );
5356            }
5357            let dj = if sparse {
5358                let mut q = std::mem::take(&mut self.spec_qs[j]);
5359                let ok = sampler::sparse_distribution_into(
5360                    &lg,
5361                    &cfg,
5362                    all_ids,
5363                    &mut self.sampler_scratch,
5364                    self.pool.as_deref(),
5365                    &mut q,
5366                );
5367                let d = if ok {
5368                    sampler::draw_sparse(&q, &mut self.rng)
5369                } else {
5370                    // everything filtered: the dense chain's greedy fallback
5371                    let t = sampler::argmax(&lg);
5372                    q.clear();
5373                    q.push((t, 1.0));
5374                    t
5375                };
5376                self.spec_qs[j] = q;
5377                all_ids.push(d);
5378                d
5379            } else if sampling {
5380                let mut q = std::mem::take(&mut self.spec_q[j]);
5381                sampler::distribution_into(
5382                    &lg,
5383                    &cfg,
5384                    all_ids,
5385                    &mut self.sampler_scratch,
5386                    self.pool.as_deref(),
5387                    &mut q,
5388                );
5389                let d = sampler::draw(&q, &mut self.rng);
5390                self.spec_q[j] = q;
5391                all_ids.push(d); // the next draft's penalties see this one
5392                d
5393            } else if greedy_pen {
5394                let d = sampler::argmax_penalized(
5395                    &lg,
5396                    &cfg,
5397                    all_ids,
5398                    &mut self.sampler_scratch,
5399                    self.pool.as_deref(),
5400                );
5401                all_ids.push(d);
5402                d
5403            } else {
5404                sampler::argmax(&lg)
5405            };
5406            attention::recycle_buf(&mut lg);
5407            drafts.push(dj);
5408            hx = hj;
5409            spec_stamp("d.pick");
5410        }
5411        all_ids.truncate(base_len);
5412        *drafted += k_spec;
5413        let t_draft = t_round.elapsed();
5414        let sub_draft = subs();
5415        // Verify batch: [t_next, d1 .. d_{k-1}] at next_pos.. — every row's
5416        // logits come back from the graph's own head.
5417        let b = k_spec + 1;
5418        let mut hiddens = vec![0.0f32; b * self.hidden_size];
5419        for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
5420            let e = self.embed_single(t);
5421            hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
5422        }
5423        let positions: Vec<usize> = (next_pos..next_pos + b).collect();
5424        spec_stamp("v.emb");
5425        let (lm_gw, lm_rows) = {
5426            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
5427            (
5428                crate::gpu::GraphW {
5429                    idx: i,
5430                    kind,
5431                    row_scale: rs,
5432                    data: &[],
5433                    prism: crate::gpu::GraphPrismOp::None,
5434                    affine: false,
5435                },
5436                self.weights.lm_head.rows(),
5437            )
5438        };
5439        let mut logits = Vec::new();
5440        let final_norm = self.weights.final_norm.clone();
5441        // Plain greedy on Metal: the b argmaxes come from the device
5442        // (`argmax_rows` after the head) and the 7.9 MB logits plane is
5443        // never read back — the round's decision needs only the ids, and
5444        // the loop top takes the last verified id as `spec_forced`, which
5445        // is exactly what its argmax of the row would give. The full rows
5446        // stay for anything that reads them: sampling, penalties,
5447        // confidence, the verify oracle, the logit dump.
5448        // `CMF_METAL_DEV_ARGMAX=0` keeps the host path.
5449        #[cfg(target_os = "macos")]
5450        let greedy_dev = metal_native
5451            && !sampling
5452            && !greedy_pen
5453            && !self.confidence_on
5454            && self.final_softcap.is_none()
5455            // The host acceptance argmax scans the WHOLE head row
5456            // (`lm_rows`), the sampler's own row only `vocab_size`: they
5457            // coincide exactly when the head has no padding rows, and
5458            // only then is the device argmax (which scores `vocab_size`)
5459            // bit-identical to both.
5460            && self.vocab_size == lm_rows
5461            && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
5462            && std::env::var_os("CMF_LOGIT_DUMP").is_none()
5463            && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
5464        #[cfg(not(target_os = "macos"))]
5465        let greedy_dev = false;
5466        let mut dev_ids: Vec<u32> = Vec::new();
5467        #[cfg(target_os = "macos")]
5468        let verify_outcome = if metal_native {
5469            let lm = self.weights.lm_head.q1_parts()?;
5470            let n_score = self.vocab_size.min(lm_rows);
5471            self.try_batch_graph_metal(
5472                &mut hiddens,
5473                &positions,
5474                b,
5475                Some((lm, &final_norm, &mut logits)),
5476                if greedy_dev {
5477                    Some((n_score, &mut dev_ids))
5478                } else {
5479                    None
5480                },
5481            )
5482        } else {
5483            self.try_batch_graph_wgpu(
5484                &mut hiddens,
5485                &positions,
5486                b,
5487                Some(crate::gpu::SpecTail {
5488                    lm: lm_gw,
5489                    lm_rows,
5490                    final_norm: &final_norm,
5491                    logits_out: &mut logits,
5492                }),
5493            )
5494        };
5495        #[cfg(not(target_os = "macos"))]
5496        let verify_outcome = self.try_batch_graph_wgpu(
5497            &mut hiddens,
5498            &positions,
5499            b,
5500            Some(crate::gpu::SpecTail {
5501                lm: lm_gw,
5502                lm_rows,
5503                final_norm: &final_norm,
5504                logits_out: &mut logits,
5505            }),
5506        );
5507        match verify_outcome {
5508            crate::gpu::BatchGraphOutcome::Completed => {}
5509            crate::gpu::BatchGraphOutcome::Declined => {
5510                // The verifier refused before admission.  Its draft MTP
5511                // rows are still device-resident, so rewind the separate
5512                // mirror before the caller takes the exact one-token path.
5513                m.kv.truncate_last(k_spec);
5514                if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
5515                    self.clear_sequence_state();
5516                    self.graph_failed
5517                        .store(true, std::sync::atomic::Ordering::Relaxed);
5518                    self.cancel
5519                        .store(true, std::sync::atomic::Ordering::Relaxed);
5520                    tracing::error!("MTP graph mirror rewind failed after verify decline");
5521                }
5522                return None;
5523            }
5524            crate::gpu::BatchGraphOutcome::Failed => {
5525                // A failed batch may have advanced trunk/GDN state.  Clear
5526                // both mirrors and preserve the terminal outcome rather than
5527                // falling through to stale CPU state.
5528                self.clear_sequence_state();
5529                self.graph_failed
5530                    .store(true, std::sync::atomic::Ordering::Relaxed);
5531                self.cancel
5532                    .store(true, std::sync::atomic::Ordering::Relaxed);
5533                tracing::error!("MTP verify batch graph failed after admission");
5534                return None;
5535            }
5536        }
5537        // `CMF_METAL_VERIFY_CHECK=1`: run the same b tokens through the
5538        // plain per-token path and compare each row's argmax + logits with
5539        // the verify's — the bring-up oracle for the batched graph. The
5540        // plain forwards mutate the CPU state; it is snapshotted and put
5541        // back, and the K/V mirrors re-pointed, before the round goes on.
5542        #[cfg(target_os = "macos")]
5543        if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
5544            let snap: Vec<Vec<f32>> = self
5545                .kv_cache
5546                .layers
5547                .iter()
5548                .map(|l| l.linear_state.clone())
5549                .collect();
5550            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5551            let toks: Vec<u32> = std::iter::once(t_next)
5552                .chain(drafts.iter().copied())
5553                .collect();
5554            let want_save = self.graph_want_logits;
5555            self.graph_want_logits = false;
5556            for (i, &t) in toks.iter().enumerate() {
5557                let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5558                let _ = self.graph_logits.take();
5559                // CMF_SPEC_PLAIN_HIDDEN=1: the next round drafts from the
5560                // plain path's hidden instead of the verify's (an experiment
5561                // on the chain's sensitivity to the half-GEMM noise)
5562                if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
5563                    hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
5564                }
5565                let ref_lg = self.logits_from_hidden(&hi);
5566                let row = &logits[i * lm_rows..(i + 1) * lm_rows];
5567                let ra = sampler::argmax(&ref_lg);
5568                let va = sampler::argmax(row);
5569                let mut md = 0f32;
5570                let mut rms = 0f64;
5571                for j in 0..lm_rows.min(ref_lg.len()) {
5572                    let d = (ref_lg[j] - row[j]).abs();
5573                    md = md.max(d);
5574                    rms += (d as f64) * (d as f64);
5575                }
5576                let mut hd = 0f32;
5577                for j in 0..self.hidden_size {
5578                    hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
5579                }
5580                eprintln!(
5581                    "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
5582                    next_pos + i,
5583                    if ra == va { "OK" } else { "MISMATCH" },
5584                    (rms / lm_rows as f64).sqrt()
5585                );
5586            }
5587            self.graph_want_logits = want_save;
5588            // restore IN PLACE: the pending verify graph wraps these very
5589            // allocations (zero-copy) — replacing the Vec would strand it
5590            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5591                if l.linear_state.len() == st.len() {
5592                    l.linear_state.copy_from_slice(&st);
5593                } else {
5594                    l.linear_state = st;
5595                }
5596            }
5597            for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
5598                let extra = l.seq_len.saturating_sub(n0);
5599                if extra > 0 {
5600                    l.truncate_last(extra);
5601                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
5602                }
5603            }
5604        }
5605        let t_verify = t_round.elapsed();
5606        let sub_verify = subs();
5607        // Acceptance. Greedy: row i's argmax is the trunk's token after
5608        // input i. Sampling: accept draft i with min(1, p_i/q_i), and on
5609        // the first rejection draw the correction from max(0, p_i − q_i)
5610        // — that token is committed by the loop top as-is (spec_forced).
5611        let mut a = 0usize;
5612        let mut forced: Option<u32> = None;
5613        let ids: Vec<u32> = if sparse {
5614            let mut p = std::mem::take(&mut self.spec_ps);
5615            let mut res = std::mem::take(&mut self.spec_ress);
5616            while a < k_spec {
5617                let ok = sampler::sparse_distribution_into(
5618                    &logits[a * lm_rows..(a + 1) * lm_rows],
5619                    &cfg,
5620                    all_ids,
5621                    &mut self.sampler_scratch,
5622                    self.pool.as_deref(),
5623                    &mut p,
5624                );
5625                if !ok {
5626                    let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
5627                    p.clear();
5628                    p.push((t, 1.0));
5629                }
5630                match sampler::spec_accept_or_correct_sparse(
5631                    &p,
5632                    &self.spec_qs[a],
5633                    drafts[a],
5634                    &mut self.rng,
5635                    &mut res,
5636                ) {
5637                    None => {
5638                        all_ids.push(drafts[a]);
5639                        a += 1;
5640                    }
5641                    Some(c) => {
5642                        forced = Some(c);
5643                        break;
5644                    }
5645                }
5646            }
5647            all_ids.truncate(base_len);
5648            self.spec_ps = p;
5649            self.spec_ress = res;
5650            drafts.clone()
5651        } else if sampling {
5652            let mut p = std::mem::take(&mut self.spec_p);
5653            let mut res = std::mem::take(&mut self.spec_res);
5654            while a < k_spec {
5655                sampler::distribution_into(
5656                    &logits[a * lm_rows..(a + 1) * lm_rows],
5657                    &cfg,
5658                    all_ids,
5659                    &mut self.sampler_scratch,
5660                    self.pool.as_deref(),
5661                    &mut p,
5662                );
5663                match sampler::spec_accept_or_correct(
5664                    &p,
5665                    &self.spec_q[a],
5666                    drafts[a],
5667                    &mut self.rng,
5668                    &mut res,
5669                    self.pool.as_deref(),
5670                ) {
5671                    None => {
5672                        all_ids.push(drafts[a]);
5673                        a += 1;
5674                    }
5675                    Some(c) => {
5676                        forced = Some(c);
5677                        break;
5678                    }
5679                }
5680            }
5681            all_ids.truncate(base_len);
5682            self.spec_p = p;
5683            self.spec_res = res;
5684            // the accepted drafts ARE the verified tokens after inputs 0..a
5685            drafts.clone()
5686        } else if greedy_pen {
5687            // Row i's penalized argmax, penalties over the stream that
5688            // includes the accepted drafts before it — the plain loop's
5689            // exact arithmetic, one pass per row, no working copy.
5690            let mut ids: Vec<u32> = Vec::with_capacity(b);
5691            for i in 0..b {
5692                let t = sampler::argmax_penalized(
5693                    &logits[i * lm_rows..(i + 1) * lm_rows],
5694                    &cfg,
5695                    all_ids,
5696                    &mut self.sampler_scratch,
5697                    self.pool.as_deref(),
5698                );
5699                ids.push(t);
5700                if i < k_spec && t == drafts[i] {
5701                    all_ids.push(t);
5702                } else {
5703                    break;
5704                }
5705            }
5706            all_ids.truncate(base_len);
5707            while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
5708                a += 1;
5709            }
5710            // rows past the first mismatch were never scored; the loop
5711            // top re-samples the last verified row itself.
5712            ids
5713        } else if greedy_dev && dev_ids.len() == b {
5714            let ids = std::mem::take(&mut dev_ids);
5715            while a < k_spec && ids[a] == drafts[a] {
5716                a += 1;
5717            }
5718            ids
5719        } else {
5720            if logits.len() < b * lm_rows {
5721                // the device argmax was asked for and came back short:
5722                // no rows to fall back on — terminal like a failed batch
5723                self.clear_sequence_state();
5724                self.graph_failed
5725                    .store(true, std::sync::atomic::Ordering::Relaxed);
5726                self.cancel
5727                    .store(true, std::sync::atomic::Ordering::Relaxed);
5728                tracing::error!("Metal verify returned neither logits nor argmax ids");
5729                return None;
5730            }
5731            let ids: Vec<u32> = (0..b)
5732                .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
5733                .collect();
5734            while a < k_spec && ids[a] == drafts[a] {
5735                a += 1;
5736            }
5737            ids
5738        };
5739        spec_stamp("acc");
5740        if spec_dbg {
5741            eprintln!(
5742                "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
5743                drafts, ids
5744            );
5745        }
5746        // CMF_METAL_VERIFY_CHECK=2: the commit oracle — plain-forward the
5747        // a+1 accepted tokens from a snapshot, then diff the replayed GDN
5748        // states and the appended K/V rows against that.
5749        #[cfg(target_os = "macos")]
5750        let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
5751            && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
5752        {
5753            let snap: Vec<Vec<f32>> = self
5754                .kv_cache
5755                .layers
5756                .iter()
5757                .map(|l| l.linear_state.clone())
5758                .collect();
5759            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5760            let toks: Vec<u32> = std::iter::once(t_next)
5761                .chain(drafts.iter().copied())
5762                .collect();
5763            let want_save = self.graph_want_logits;
5764            self.graph_want_logits = false;
5765            for (i, &t) in toks.iter().take(a + 1).enumerate() {
5766                let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5767                let _ = self.graph_logits.take();
5768            }
5769            self.graph_want_logits = want_save;
5770            let plain_states: Vec<Vec<f32>> = self
5771                .kv_cache
5772                .layers
5773                .iter()
5774                .map(|l| l.linear_state.clone())
5775                .collect();
5776            let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5777            let mut rows = Vec::new();
5778            for (li, (l, n0)) in self
5779                .kv_cache
5780                .layers
5781                .iter_mut()
5782                .zip(attn_lens.iter())
5783                .enumerate()
5784            {
5785                let extra = l.seq_len.saturating_sub(*n0);
5786                if extra > 0 {
5787                    let mut kk = Vec::new();
5788                    let mut vv = Vec::new();
5789                    for g in 0..nkv {
5790                        kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5791                        vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5792                    }
5793                    rows.push((li, kk, vv));
5794                    l.truncate_last(extra);
5795                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
5796                }
5797            }
5798            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5799                if l.linear_state.len() == st.len() {
5800                    l.linear_state.copy_from_slice(&st);
5801                } else {
5802                    l.linear_state = st;
5803                }
5804            }
5805            Some((plain_states, rows))
5806        } else {
5807            None
5808        };
5809        let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
5810        // Metal: the MTP cache cut and the round's warm-up SUBMIT come
5811        // BEFORE the trunk commit, so the warm-up's command buffer is
5812        // queued ahead of the GDN replay (second queue) and its wait
5813        // below no longer sits behind the replay — measured: the warm-up's
5814        // wait grew with the accepted count exactly like the replay does
5815        // (8 ms at a=1, 17 ms at a=3, 25 ms at a=5 for ~2 ms of its own
5816        // work). The replay now overlaps the warm-up's readback, the
5817        // round's return and the next draft chain.
5818        #[cfg(target_os = "macos")]
5819        let mut warm_pending: Option<MetalWarmPending> = None;
5820        #[cfg(target_os = "macos")]
5821        if metal_native {
5822            m.kv.truncate_last(k_spec.saturating_sub(1));
5823            if self.mtp_graph_mode == Some(true) {
5824                // the mirror rows below the cut are the CPU rows: re-point,
5825                // no re-upload
5826                crate::gpu_metal::kv_mirror_set_stored(
5827                    self.mtp_kv_id(),
5828                    Self::MTP_LAYER_BASE,
5829                    m.kv.seq_len,
5830                );
5831                if !warm_off && a > 0 {
5832                    let pairs: Vec<(&[f32], u32)> = (0..a)
5833                        .map(|j| {
5834                            (
5835                                &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
5836                                ids[j],
5837                            )
5838                        })
5839                        .collect();
5840                    warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
5841                }
5842            }
5843            spec_stamp("c.wsub");
5844        }
5845        // a fully-accepted round needs no restore: every input was real.
5846        #[cfg(target_os = "macos")]
5847        if metal_native {
5848            // the Metal verify never wrote its states: the commit replays the
5849            // accepted prefix into the CPU owners and appends the K/V rows
5850            if !self.metal_verify_commit(a) {
5851                self.clear_sequence_state();
5852                self.graph_failed
5853                    .store(true, std::sync::atomic::Ordering::Relaxed);
5854                self.cancel
5855                    .store(true, std::sync::atomic::Ordering::Relaxed);
5856                tracing::error!("Metal verify state/KV handoff failed after admission");
5857                return None;
5858            }
5859            if let Some((plain_states, rows)) = commit_ref {
5860                crate::gpu_metal::queue_fence();
5861                // the commit's replay runs on the second queue: collect it
5862                // before the oracle reads the CPU owners it writes into
5863                let _ = crate::gpu_metal::wait_replay();
5864                let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5865                let mut worst_s = 0f32;
5866                let mut worst_li = 0usize;
5867                for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
5868                    if l.linear_state.len() != ps.len() || ps.is_empty() {
5869                        continue;
5870                    }
5871                    let d = l
5872                        .linear_state
5873                        .iter()
5874                        .zip(ps)
5875                        .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5876                    let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
5877                    let rel = d / n.max(1e-6);
5878                    if rel > worst_s {
5879                        worst_s = rel;
5880                        worst_li = li;
5881                    }
5882                }
5883                let mut worst_k = 0f32;
5884                for (li, kk, vv) in &rows {
5885                    let l = &self.kv_cache.layers[*li];
5886                    let n0 = l.seq_len - (kk.len() / (nkv * hd));
5887                    let mut ck = Vec::new();
5888                    let mut cv = Vec::new();
5889                    for g in 0..nkv {
5890                        ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5891                        cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5892                    }
5893                    if ck.len() == kk.len() {
5894                        let dk = ck
5895                            .iter()
5896                            .zip(kk)
5897                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5898                        let dv = cv
5899                            .iter()
5900                            .zip(vv)
5901                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5902                        worst_k = worst_k.max(dk).max(dv);
5903                    } else {
5904                        eprintln!(
5905                            "commit-check L{li}: kv row count mismatch {} vs {}",
5906                            ck.len(),
5907                            kk.len()
5908                        );
5909                    }
5910                }
5911                eprintln!(
5912                    "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}"
5913                );
5914            }
5915        }
5916        if !metal_native && a + 1 < b {
5917            let expected_gdn_layers = self.graph_gdn_layer_count();
5918            if expected_gdn_layers > 0
5919                && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
5920            {
5921                self.clear_sequence_state();
5922                self.graph_failed
5923                    .store(true, std::sync::atomic::Ordering::Relaxed);
5924                self.cancel
5925                    .store(true, std::sync::atomic::Ordering::Relaxed);
5926                tracing::error!("GDN speculative restore failed after verify");
5927                return None;
5928            }
5929        }
5930        if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
5931            // The verify graph committed the full batch, but one of its
5932            // persistent Full-attention mirrors could not be re-pointed to
5933            // the accepted prefix.  Treat that as terminal state failure;
5934            // an exact CPU fallback would otherwise consume stale GDN/KV.
5935            self.clear_sequence_state();
5936            self.graph_failed
5937                .store(true, std::sync::atomic::Ordering::Relaxed);
5938            self.cancel
5939                .store(true, std::sync::atomic::Ordering::Relaxed);
5940            tracing::error!("trunk graph KV rewind failed after speculative verify");
5941            return None;
5942        }
5943        *accepted += a;
5944        // MTP cache: keep the first draft row (its inputs were real), drop
5945        // the chain's, then append the verified pairs the round produced.
5946        // Each of those is a whole MTP block on the per-op path and they
5947        // cost 5.8 ms of a 69 ms round at k=3 — a third of what the
5948        // round's own draft costs. PRICED, and they earn it: skipping
5949        // them (`CMF_SPEC_WARM=0`) drops acceptance from 89% to 81% at
5950        // k=3 and 85% to 74% at k=4, and the tok/s goes nowhere at k=3
5951        // (50.3 against 50.5) and backwards at k=4 (48.1 against 50.1).
5952        // The knob stays so the next person can re-price it after the
5953        // warms are batched instead of assuming either way.
5954        if !metal_native {
5955            // (Metal cut its MTP cache before the trunk commit, above)
5956            m.kv.truncate_last(k_spec.saturating_sub(1));
5957        }
5958        spec_stamp("c.trunc");
5959        if !metal_native
5960            && self.mtp_graph_mode == Some(true)
5961            && !self.rewind_mtp_graph_mirror(next_pos)
5962        {
5963            // The graph draft was admitted, so inability to move its cursor
5964            // back to the real anchor is a state failure, not a capability
5965            // refusal.  Do not warm or continue with a stale mirror.
5966            self.clear_sequence_state();
5967            self.graph_failed
5968                .store(true, std::sync::atomic::Ordering::Relaxed);
5969            self.cancel
5970                .store(true, std::sync::atomic::Ordering::Relaxed);
5971            tracing::error!("MTP graph mirror rewind failed after verify commit");
5972            return None;
5973        }
5974        if !warm_off && a > 0 {
5975            // Graph arm: all accepted pairs in ONE batched run over the
5976            // MTP block; the token graph one by one if the batch declines.
5977            let mut warmed = false;
5978            #[cfg(target_os = "macos")]
5979            if metal_native && self.mtp_graph_mode == Some(true) {
5980                // the batched warm-up was submitted before the trunk
5981                // commit: collect it here; one by one on the token graph
5982                // if it declined (or failed)
5983                warmed = match warm_pending.take() {
5984                    Some(p) => self.mtp_warm_batch_finish(m, p),
5985                    None => false,
5986                };
5987                if !warmed {
5988                    warmed = true;
5989                    for j in 0..a {
5990                        let row =
5991                            hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
5992                        if self
5993                            .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
5994                            .is_none()
5995                        {
5996                            warmed = false;
5997                            break;
5998                        }
5999                    }
6000                }
6001            }
6002            if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
6003                let rows: Vec<Vec<f32>> = (0..a)
6004                    .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
6005                    .collect();
6006                let pairs: Vec<(&[f32], u32)> = rows
6007                    .iter()
6008                    .zip(ids.iter())
6009                    .map(|(r, &t)| (r.as_slice(), t))
6010                    .collect();
6011                match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
6012                    Ok(()) => warmed = true,
6013                    Err(err) => {
6014                        // A warm-up failure after graph admission cannot
6015                        // fall back to `mtp_warm`: the detached CPU cache is
6016                        // not authoritative for the device mirror.  Mark it
6017                        // terminal so the generation caller clears state and
6018                        // returns instead of drafting from stale attention.
6019                        tracing::error!("{err}");
6020                        self.clear_sequence_state();
6021                        self.graph_failed
6022                            .store(true, std::sync::atomic::Ordering::Relaxed);
6023                        self.cancel
6024                            .store(true, std::sync::atomic::Ordering::Relaxed);
6025                        return None;
6026                    }
6027                }
6028            }
6029            if !warmed {
6030                for j in 0..a {
6031                    let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
6032                    let row = row.to_vec();
6033                    self.mtp_warm(m, &row, ids[j], next_pos + j);
6034                }
6035            }
6036        }
6037        // The sampler's contract: logits of the LAST verified position —
6038        // unless a rejected draft already drew the correction, in which
6039        // case the loop top commits that token and samples nothing.
6040        spec_stamp("c.warm");
6041        if let Some(c) = forced {
6042            self.spec_forced = Some(c);
6043            self.graph_logits = None;
6044        } else if greedy_dev && logits.is_empty() {
6045            // the row's argmax IS the token the loop top would pick from
6046            // it (plain greedy, no penalties): commit it as forced
6047            self.spec_forced = Some(ids[a]);
6048            self.graph_logits = None;
6049        } else {
6050            let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
6051            row.resize(self.vocab_size, 0.0);
6052            if let Some(c) = self.final_softcap {
6053                for l in row.iter_mut() {
6054                    *l = c * (*l / c).tanh();
6055                }
6056            }
6057            self.graph_logits = Some(row);
6058        }
6059        let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
6060        spec_stamp("c.row");
6061        // Three phases, not two. The round's wall clock was 4 ms longer
6062        // than draft+verify and the difference had nowhere to be seen:
6063        // the accepted prefix re-runs the MTP block once per token to
6064        // keep the draft head's attention cache warm, and the GDN state
6065        // rolls back on any rejection. Both live here, after the verify.
6066        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6067            let end = subs();
6068            eprintln!(
6069                "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
6070                 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
6071                t_draft.as_secs_f64() * 1e3,
6072                sub_draft - sub0,
6073                (t_verify - t_draft).as_secs_f64() * 1e3,
6074                sub_verify - sub_draft,
6075                (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
6076                end - sub_verify,
6077                self.draft_full_streak,
6078            );
6079        }
6080        // Native Metal's verify tile is flat in b (eight rows for the price
6081        // of one), so a shorter round only forfeits tokens — measured on
6082        // the M4: an essay round at k=2 still verified in 260 ms. The
6083        // adaptation is for cards whose verify grows with the rows.
6084        if k_env.is_none() && !metal_native && !k_capped {
6085            // Slow average and a wide band: a fast one oscillated 2↔3 on
6086            // an essay every other round (measured), which forfeits the
6087            // draft it just paid for.
6088            let f = a as f32 / k_spec.max(1) as f32;
6089            self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
6090            let mut k_next = k_spec;
6091            if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
6092                k_next = k_spec + 1;
6093            } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
6094                k_next = k_spec - 1;
6095            }
6096            if k_next != k_spec {
6097                self.spec_acc_ewma = 0.6;
6098                if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6099                    eprintln!("spec-k: {k_spec} → {k_next}");
6100                }
6101            }
6102            self.spec_k_adapt = Some(k_next);
6103        }
6104        spec_stamp("end");
6105        Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
6106    }
6107
6108    /// Micro-benchmark: two single-position forwards vs one fused pair
6109    /// from the current cache state (KV rewound after each probe).
6110    /// Returns (two_singles_ms, fused_pair_ms) per probe, or the (0, 0)
6111    /// sentinel when this model has no pair path to measure — the same
6112    /// answer the o1 arm gives, and the bench prints it the same way.
6113    /// (An architecture that loads its own layers leaves `weights.layers`
6114    /// empty; walking it here was an index panic, found by `bench` on
6115    /// deepseek_v4.)
6116    pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
6117        if !self.pair_supported() {
6118            return (0.0, 0.0);
6119        }
6120        // This is a host-side pair micro-benchmark. It truncates the host KV
6121        // after every probe, so letting the whole-token graph participate
6122        // would leave its device GDN/KV mirror ahead of the next probe and
6123        // poison the process-wide graph verdict before the real generation
6124        // benchmark starts. Keep the existing per-op/GPU arithmetic while
6125        // suppressing only the stateful token graph for this measurement.
6126        let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
6127        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
6128        let emb1 = self.embed_single(1);
6129        let emb2 = self.embed_single(2);
6130        let pos = self.kv_cache.seq_len();
6131
6132        let t0 = std::time::Instant::now();
6133        for _ in 0..iters {
6134            let _ = self.forward_layers(&emb1, pos, None);
6135            let _ = self.forward_layers(&emb2, pos + 1, None);
6136            for l in &mut self.kv_cache.layers {
6137                l.truncate_last(2);
6138            }
6139        }
6140        let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
6141
6142        let t1 = std::time::Instant::now();
6143        for _ in 0..iters {
6144            let _ = self.forward_pair(&emb1, &emb2, pos);
6145            for l in &mut self.kv_cache.layers {
6146                l.truncate_last(2);
6147            }
6148        }
6149        let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
6150        match graph_env {
6151            Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
6152            None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
6153        }
6154        (singles_ms, pair_ms)
6155    }
6156
6157    /// Fused two-position forward: weight rows are streamed from memory
6158    /// once per layer for both positions. Full layers → fused GQA pair;
6159    /// linear layers → vmf_phase pair (lane 2 state is tentative in the
6160    /// per-layer scratch until the draft is accepted).
6161    /// Whether the fused two-position path covers every layer kind in
6162    /// this model. MLA and KDA run per position (their pair arms are
6163    /// unreachable); the seq prefill falls back to singles for them.
6164    fn pair_supported(&self) -> bool {
6165        // An EMPTY layer stack means the architecture loaded its own and
6166        // this path has nothing to walk. Checking that directly, rather
6167        // than naming each such architecture, is what makes the guard hold
6168        // for the next one: `any()` over no layers is false, so a
6169        // feature-by-feature test says "supported" for a model that has no
6170        // layers here at all.
6171        !self.weights.layers.is_empty()
6172            && self.g3n.is_none()
6173            && !self
6174                .weights
6175                .layers
6176                .iter()
6177                .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
6178    }
6179
6180    fn forward_pair(
6181        &mut self,
6182        emb1: &[f32],
6183        emb2: &[f32],
6184        position: usize,
6185    ) -> (Vec<f32>, Vec<f32>) {
6186        let mut h1 = emb1.to_vec();
6187        let mut h2 = emb2.to_vec();
6188        let (_nkv, _hd, hs, _rd, eps) = (
6189            self.num_kv_heads,
6190            self.head_dim,
6191            self.hidden_size,
6192            self.rotary_dim,
6193            self.rms_eps,
6194        );
6195        let pool = self.pool.clone();
6196
6197        for li in 0..self.num_layers {
6198            let lw = &self.weights.layers[self.phys_layer(li)];
6199            // Norms into pipeline scratch (4 allocs/layer on the MTP
6200            // decode hot path before this).
6201            inference::rms_norm_into(
6202                &h1,
6203                &lw.input_norm,
6204                self.rms_eps,
6205                self.norm_style,
6206                &mut self.ws.n1,
6207            );
6208            inference::rms_norm_into(
6209                &h2,
6210                &lw.input_norm,
6211                self.rms_eps,
6212                self.norm_style,
6213                &mut self.ws.n2,
6214            );
6215
6216            let (a1, a2) = match &lw.attn {
6217                AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6218                AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6219                AttnKind::Linear(w) => {
6220                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
6221                    let layer = &mut self.kv_cache.layers[li];
6222                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
6223                    vmf_phase_pair(
6224                        &self.ws.n1,
6225                        &self.ws.n2,
6226                        w,
6227                        &cfg,
6228                        state,
6229                        scratch,
6230                        self.pool.as_deref(),
6231                    )
6232                }
6233                AttnKind::LinearGdn(w) => {
6234                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6235                    let layer = &mut self.kv_cache.layers[li];
6236                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
6237                    gdn_pair(
6238                        &self.ws.n1,
6239                        &self.ws.n2,
6240                        w,
6241                        &cfg,
6242                        state,
6243                        scratch,
6244                        self.pool.as_deref(),
6245                    )
6246                }
6247                AttnKind::ShortConv(w) => {
6248                    let cfg = self
6249                        .short_conv_cfg
6250                        .expect("short-conv layer without short_conv_cfg");
6251                    let layer = &mut self.kv_cache.layers[li];
6252                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
6253                    short_conv_pair(
6254                        &self.ws.n1,
6255                        &self.ws.n2,
6256                        w,
6257                        &cfg,
6258                        state,
6259                        scratch,
6260                        self.pool.as_deref(),
6261                    )
6262                }
6263                AttnKind::Full {
6264                    wq,
6265                    wk,
6266                    wv,
6267                    wo,
6268                    q_norm,
6269                    k_norm,
6270                    output_gate,
6271                    softplus_gate,
6272                    bias,
6273                } => {
6274                    let inv_freq_l = self.layer_inv_freq(li);
6275                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
6276                    let cfg = QwenAttnCfg {
6277                        num_heads: self.layer_num_heads(li),
6278                        num_kv_heads: nkv_l,
6279                        head_dim: hd_l,
6280                        hidden_size: hs,
6281                        position,
6282                        inv_freq: &inv_freq_l,
6283                        rotary_dim: rd_l,
6284                        scale: self.attn_scale,
6285                        softcap: self.attn_softcap,
6286                        window: self.layer_window(li),
6287                        v_norm: self.attn_v_norm,
6288                        qk_norm_after_rope: self.qk_norm_after_rope,
6289                        q_norm: q_norm.as_deref(),
6290                        k_norm: k_norm.as_deref(),
6291                        output_gate: *output_gate,
6292                        softplus_gate: softplus_gate
6293                            .as_ref()
6294                            .map(|(gate, per_head)| (gate, *per_head)),
6295                        rope_scale: self.layer_rope_scale(li),
6296                        bias: bias
6297                            .as_ref()
6298                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6299                        rms_eps: eps,
6300                        norm_style: self.norm_style,
6301                        pool: pool.as_deref(),
6302                    };
6303                    attention::qwen_attention_pair(
6304                        &self.ws.n1,
6305                        &self.ws.n2,
6306                        wq,
6307                        wk,
6308                        wv,
6309                        wo,
6310                        &mut self.kv_cache.layers[li],
6311                        &cfg,
6312                    )
6313                }
6314            };
6315            let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
6316                Some(w) => (
6317                    inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
6318                    inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
6319                ),
6320                None => (a1, a2),
6321            };
6322            for i in 0..self.hidden_size {
6323                h1[i] += a1[i];
6324                h2[i] += a2[i];
6325            }
6326            let (mut a1, mut a2) = (a1, a2);
6327            attention::recycle_buf(&mut a1);
6328            attention::recycle_buf(&mut a2);
6329
6330            let lw = &self.weights.layers[self.phys_layer(li)];
6331            inference::rms_norm_into(
6332                &h1,
6333                &lw.post_norm,
6334                self.rms_eps,
6335                self.norm_style,
6336                &mut self.ws.p1,
6337            );
6338            inference::rms_norm_into(
6339                &h2,
6340                &lw.post_norm,
6341                self.rms_eps,
6342                self.norm_style,
6343                &mut self.ws.p2,
6344            );
6345            let (f1, f2) = match &lw.ffn {
6346                // Dual-branch layers need the raw residuals — run the
6347                // two positions through the same fn decode uses.
6348                FfnKind::DenseMoe(dm) => (
6349                    dense_moe_ffn(
6350                        dm,
6351                        &self.ws.p1,
6352                        &h1,
6353                        self.rms_eps,
6354                        self.norm_style,
6355                        self.pool.as_deref(),
6356                    ),
6357                    dense_moe_ffn(
6358                        dm,
6359                        &self.ws.p2,
6360                        &h2,
6361                        self.rms_eps,
6362                        self.norm_style,
6363                        self.pool.as_deref(),
6364                    ),
6365                ),
6366                _ => ffn_forward_pair(
6367                    &lw.ffn,
6368                    &self.ws.p1,
6369                    &self.ws.p2,
6370                    self.pool.as_deref(),
6371                    None,
6372                ),
6373            };
6374            let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
6375                Some(w) => (
6376                    inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
6377                    inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
6378                ),
6379                None => (f1, f2),
6380            };
6381            for i in 0..self.hidden_size {
6382                h1[i] += f1[i];
6383                h2[i] += f2[i];
6384            }
6385            let (mut f1, mut f2) = (f1, f2);
6386            attention::recycle_buf(&mut f1);
6387            attention::recycle_buf(&mut f2);
6388            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
6389                for i in 0..self.hidden_size {
6390                    h1[i] *= sc;
6391                    h2[i] *= sc;
6392                }
6393            }
6394            // Looped Transformer: apply final norm at the end of each loop iteration.
6395            if self.is_loop_end(li) && li + 1 < self.num_layers {
6396                h1 = inference::rms_norm(
6397                    &h1,
6398                    &self.weights.final_norm,
6399                    self.rms_eps,
6400                    self.norm_style,
6401                );
6402                h2 = inference::rms_norm(
6403                    &h2,
6404                    &self.weights.final_norm,
6405                    self.rms_eps,
6406                    self.norm_style,
6407                );
6408            }
6409        }
6410        // Real O(1) prefill pairs may also carry tentative lane-2 recurrent
6411        // state. Commit it before publishing the transition epoch so the
6412        // next serial/device row cannot observe a new attention epoch with an
6413        // old GDN state. Speculative pairs run only when O(1) is inactive and
6414        // retain their existing caller-controlled commit/rollback semantics.
6415        if self.o1_active() {
6416            self.commit_linear_scratch();
6417        }
6418        self.o1_progress();
6419        (h1, h2)
6420    }
6421
6422    /// Commit lane-2 linear states after an accepted draft.
6423    fn commit_linear_scratch(&mut self) {
6424        for layer in &mut self.kv_cache.layers {
6425            if !layer.linear_scratch.is_empty() {
6426                std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
6427                layer.linear_scratch.clear();
6428            }
6429        }
6430    }
6431
6432    /// Forward a full id sequence from a fresh cache and return the
6433    /// logits after the last position (golden-parity harness, bench).
6434    pub fn forward_ids(
6435        &mut self,
6436        ids: &[u32],
6437        task_mask: Option<&TaskMask>,
6438    ) -> Result<Vec<f32>, String> {
6439        if ids.is_empty() {
6440            return Err("empty id sequence".to_string());
6441        }
6442        self.clear_sequence_state();
6443        self.check_forward_graph("forward_ids setup", 0)?;
6444        if task_mask.is_none() {
6445            self.o1_begin();
6446        }
6447        let mut hidden = vec![0.0f32; self.hidden_size];
6448        let mut pos = 0usize;
6449        if let Some(b) = &mut self.dsv41 {
6450            let pool = self.pool.clone();
6451            let mut logits = Vec::new();
6452            crate::dsv41::forward_chunk(
6453                &b.0,
6454                &b.1,
6455                &b.2,
6456                &mut b.3,
6457                ids,
6458                0,
6459                pool.as_deref(),
6460                &mut logits,
6461            );
6462            if let Err(err) = self.o1_seal_checked() {
6463                self.clear_sequence_state();
6464                return Err(err);
6465            }
6466            return Ok(logits);
6467        }
6468        // Same routing predicate generation uses. Two reasons it must be
6469        // the same one: (1) a GDN hybrid's recurrent state is GPU-
6470        // resident, and a batched CPU prefill would build it on the host
6471        // only — decode then reads buffers the prefill never wrote;
6472        // (2) bench times THIS function and calls the result "prefill",
6473        // so a different path here reports a number production never
6474        // sees (W2 on 2×5090: 8.7 tok/s reported against 125 real).
6475        if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
6476            // prefill-GEMM in chunks; only the last position's hidden is
6477            // needed. (o1-compatible: the batch path attends per position
6478            // through qwen_attention, which carries the collection hook.)
6479            let chunk = prefill_chunk();
6480            let hs = self.hidden_size;
6481            while pos < ids.len() {
6482                let end = (pos + chunk).min(ids.len());
6483                let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6484                self.check_forward_graph("forward_ids batched prefill", end - 1)?;
6485                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
6486                pos = end;
6487            }
6488        }
6489        // Same guards as generation's prefill — INCLUDING the graph one.
6490        // The CPU pair walk was intercepting positions that the resident
6491        // token graph would have run itself: on a GDN hybrid over wgpu
6492        // that is 89 ms of host forward against 7 ms of device submit,
6493        // and it made prefill look 12× slower than it is (W2 on an RTX
6494        // 5090, ctx 512: 11.2 tok/s with the walk, 136.6 without).
6495        // CMF_PAIR=0 opts out; a model whose layers live outside
6496        // `weights.layers` has no pair walk to take.
6497        if task_mask.is_none()
6498            && !self.graph_prefill_preferred()
6499            && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
6500            && self.pair_supported()
6501        {
6502            while pos + 1 < ids.len() {
6503                let e1 = self.embed_single(ids[pos]);
6504                let e2 = self.embed_single(ids[pos + 1]);
6505                let (_, h2) = self.forward_pair(&e1, &e2, pos);
6506                self.check_forward_graph("forward_ids pair", pos + 1)?;
6507                self.commit_linear_scratch();
6508                hidden = h2;
6509                pos += 2;
6510            }
6511        }
6512        while pos < ids.len() {
6513            hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6514            self.check_forward_graph("forward_ids", pos)?;
6515            pos += 1;
6516        }
6517        // Harness contract: after forward_ids the cache is decode-ready —
6518        // under o1 that means sealed (bench measures the seal as part of
6519        // prefill, honestly).
6520        if let Err(err) = self.o1_seal_checked() {
6521            self.clear_sequence_state();
6522            return Err(err);
6523        }
6524        let normed = inference::rms_norm(
6525            &hidden,
6526            &self.weights.final_norm,
6527            self.rms_eps,
6528            self.norm_style,
6529        );
6530        Ok(self.lm_head_forward(&normed))
6531    }
6532
6533    /// Run the V4.1 stack one token at a time and retain logits for every
6534    /// position. This is a diagnostic surface for comparing a converted
6535    /// checkpoint with a tokenwise reference implementation.
6536    #[doc(hidden)]
6537    pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
6538        #[cfg(target_os = "macos")]
6539        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
6540        if ids.is_empty() {
6541            return Err("empty id sequence".to_string());
6542        }
6543        self.clear_sequence_state();
6544        self.dsv41
6545            .as_ref()
6546            .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
6547        self.o1_begin();
6548        let rows = {
6549            let pool = self.pool.clone();
6550            let b = self
6551                .dsv41
6552                .as_mut()
6553                .expect("dsv41 checked above; state cannot change during forward");
6554            let mut rows = Vec::with_capacity(ids.len());
6555            for (position, &id) in ids.iter().enumerate() {
6556                let mut logits = Vec::new();
6557                crate::dsv41::forward_token(
6558                    &b.0,
6559                    &b.1,
6560                    &b.2,
6561                    &mut b.3,
6562                    id,
6563                    position,
6564                    pool.as_deref(),
6565                    &mut logits,
6566                );
6567                rows.push(logits);
6568            }
6569            rows
6570        };
6571        self.o1_seal();
6572        Ok(rows)
6573    }
6574
6575    /// Teacher-forced perplexity over a token sequence (phase-C gate:
6576    /// honest quant comparisons instead of prompt vibes).
6577    ///
6578    /// Attention is EXACT even on a model whose layers are flagged for
6579    /// the O(1) kernel — scoring the backbone is the default on purpose
6580    /// (it is the yardstick). `nll_ids_o1` scores the CONVERTED model.
6581    pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
6582        let (nll, cnt) = self.nll_ids_from(ids, 0)?;
6583        Ok((nll / cnt.max(1) as f64).exp())
6584    }
6585
6586    /// DTG-MA calibration pass (Patent 2): run `ids` through the model
6587    /// (CPU path, per position) and return each layer's per-neuron
6588    /// activation mass Σ|silu(gate)·up| — the statistic the task-guided
6589    /// FFN mask is derived from.
6590    pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
6591        self.clear_sequence_state();
6592        FFN_PROBE.with(|p| {
6593            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6594        });
6595        crate::gpu::cpu_scope(|| {
6596            for (pos, &id) in ids.iter().enumerate() {
6597                let emb = self.embed_single(id);
6598                let _ = self.forward_layers(&emb, pos, None);
6599            }
6600        });
6601        self.clear_sequence_state();
6602        FFN_PROBE
6603            .with(|p| p.borrow_mut().take())
6604            .unwrap_or_default()
6605    }
6606
6607    /// `probe_ffn_mass` over the BATCHED prefill: same accumulator, one
6608    /// sweep instead of one forward per token. What makes the statistic
6609    /// affordable on a 27B.
6610    pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
6611        if let Err(err) = self.nll_begin() {
6612            // A recorder can be left by a caller that was interrupted before
6613            // this request entered its scoring block.  Consume it even when
6614            // the preflight failure prevents initialization of a new one.
6615            let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
6616            self.nll_end();
6617            return Err(err);
6618        }
6619        FFN_PROBE.with(|p| {
6620            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6621        });
6622        let result: Result<(), String> = (|| {
6623            for chunk in ids.chunks(256) {
6624                if chunk.len() < 2 {
6625                    continue;
6626                }
6627                self.nll_ids_masked(chunk, 0, None)?;
6628            }
6629            Ok(())
6630        })();
6631        self.nll_end();
6632        let probe = FFN_PROBE
6633            .with(|p| p.borrow_mut().take())
6634            .unwrap_or_default();
6635        match result {
6636            Ok(()) => Ok(probe),
6637            Err(err) => {
6638                drop(probe);
6639                Err(err)
6640            }
6641        }
6642    }
6643
6644    /// Teacher-forced PPL with a task mask active (sparse execution) —
6645    /// the quality gate for a DTG-MA-masked skill. Sequential per
6646    /// position: the batched prefill path is dense-only.
6647    pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
6648        self.nll_begin()?;
6649        let result: Result<f64, String> = (|| {
6650            let mut nll = 0f64;
6651            let mut cnt = 0usize;
6652            let mut hidden = vec![0f32; self.hidden_size];
6653            for (pos, &id) in ids.iter().enumerate() {
6654                if pos > 0 {
6655                    inference::rms_norm_into(
6656                        &hidden,
6657                        &self.weights.final_norm,
6658                        self.rms_eps,
6659                        self.norm_style,
6660                        &mut self.ws.n1,
6661                    );
6662                    let mut logits = self.lm_head_forward(&self.ws.n1);
6663                    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
6664                    let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
6665                    let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
6666                    nll -= p.max(1e-300).ln();
6667                    cnt += 1;
6668                    attention::recycle_buf(&mut logits);
6669                }
6670                let emb = self.embed_single(id);
6671                hidden = self.forward_layers(&emb, pos, Some(mask));
6672                self.nll_check_graph("masked serial forward", pos)?;
6673                // Consume a possible graph logits side channel before the
6674                // next row.  Masked scoring normally disables that route,
6675                // but stale channel state must never survive a request.
6676                let _ = self.graph_logits.take();
6677            }
6678            Ok((nll / cnt.max(1) as f64).exp())
6679        })();
6680        self.nll_end();
6681        result
6682    }
6683
6684    /// Teacher-forced NLL sum + scored-token count over positions
6685    /// `start..len-1`, attention EXACT. Positions below `start` still
6686    /// run — they are the context — they are just not scored, so this
6687    /// pairs with `nll_ids_o1(ids, start)` over the very same tokens.
6688    ///
6689    /// Returning (nll, cnt) rather than a ppl is what lets a windowed
6690    /// caller combine windows before the exp, so every scored token
6691    /// weighs the same regardless of how the windows are cut.
6692    /// `nll_ids_from` with a task mask held active at every position.
6693    ///
6694    /// The batched prefill path does not thread masks, so this walks the
6695    /// per-position forward — slower, but it scores the file exactly the
6696    /// way `run --task` will serve it, which is the point of the gate
6697    /// that calls it. With `None` it defers to the fast path.
6698    /// Masked scoring rides the SAME batched sweep as unmasked scoring —
6699    /// the masked-inference fast path: `prefill_batch_masked` lands the
6700    /// per-visit FFN rows on the activations inside the fused arms. The
6701    /// per-position loop below remains only as the no-batch fallback.
6702    pub fn nll_ids_masked(
6703        &mut self,
6704        ids: &[u32],
6705        start: usize,
6706        task_mask: Option<&TaskMask>,
6707    ) -> Result<(f64, usize), String> {
6708        let task_mask = self.drop_open_mask(task_mask);
6709        self.nll_ids_inner(ids, start, task_mask)
6710    }
6711
6712    pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
6713        self.nll_ids_inner(ids, start, None)
6714    }
6715
6716    fn nll_ids_inner(
6717        &mut self,
6718        ids: &[u32],
6719        start: usize,
6720        task_mask: Option<&TaskMask>,
6721    ) -> Result<(f64, usize), String> {
6722        self.nll_begin()?;
6723        let result: Result<(f64, usize), String> = (|| {
6724            let mut nll = 0f64;
6725            let mut cnt = 0usize;
6726            // An unmasked quality run with the resident wgpu graph must score
6727            // the same stateful path used by generation.  The layer-major
6728            // GEMM prefill below is a valid CPU/GEMM oracle, but it seeds
6729            // neither the graph's device GDN state nor its device KV mirrors;
6730            // using it here would silently score a different execution.  Keep
6731            // masked scoring on the exact per-position path as before, and
6732            // let the serial arm below drive the graph-aware scorer.
6733            // Only native Metal has a fused graph lm_head contract.  Vulkan
6734            // and other graph backends may expose hidden state without the
6735            // optional logits side channel; preserve their established CPU
6736            // norm/head fallback instead of turning that valid route into a
6737            // hard missing-logits error.
6738            let (graph_quality, fused_head_quality) = nll_graph_policy(
6739                task_mask.is_none(),
6740                self.graph_prefill_preferred(),
6741                crate::gpu::q1_force(),
6742            );
6743            self.graph_head_required = fused_head_quality;
6744            self.graph_want_logits = fused_head_quality;
6745            #[cfg(target_os = "macos")]
6746            if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
6747                match self.nll_batch_metal(ids, start) {
6748                    MetalBatchNllOutcome::Completed(nll, count) => {
6749                        return Ok((nll, count));
6750                    }
6751                    MetalBatchNllOutcome::Declined => {}
6752                    MetalBatchNllOutcome::Failed(err) => return Err(err),
6753                }
6754            }
6755            if self.can_prefill_batched() && !graph_quality {
6756                // prefill-GEMM: layer-major position chunks, lm_head batched
6757                // (254MB lm_head read once per chunk, not per position).
6758                // The layer chunk is large (grouping positions by MoE experts
6759                // wins with size), lm_head in sub-blocks (logit buffer
6760                // 32×vocab ≈ 32MB instead of 128×).
6761                const CHUNK: usize = 128;
6762                const LM_SUB: usize = 32;
6763                let n = ids.len().saturating_sub(1);
6764                let hs = self.hidden_size;
6765                let rows = self.weights.lm_head.rows();
6766                let mut pos = 0usize;
6767                while pos < n {
6768                    let end = (pos + CHUNK).min(n);
6769                    let bsz = end - pos;
6770                    let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6771                    self.nll_check_graph("batched prefill", pos)?;
6772                    let mut k0 = 0usize;
6773                    while k0 < bsz {
6774                        let k1 = (k0 + LM_SUB).min(bsz);
6775                        let sb = k1 - k0;
6776                        // Sub-block entirely below the scored range: the KV
6777                        // it just built is all this pass needed from it.
6778                        if pos + k1 <= start {
6779                            k0 = k1;
6780                            continue;
6781                        }
6782                        let mut normed = vec![0.0f32; sb * hs];
6783                        for k in 0..sb {
6784                            let r = inference::rms_norm(
6785                                &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
6786                                &self.weights.final_norm,
6787                                self.rms_eps,
6788                                self.norm_style,
6789                            );
6790                            normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
6791                        }
6792                        let mut logits = vec![0.0f32; sb * rows];
6793                        self.weights
6794                            .lm_head
6795                            .matmat(&normed, sb, &mut logits, self.pool.as_deref());
6796                        for k in 0..sb {
6797                            if pos + k0 + k < start {
6798                                continue;
6799                            }
6800                            self.nll_check_graph("batched score row", pos + k0 + k)?;
6801                            let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
6802                            if let Some(mu) = self.logit_multiplier {
6803                                for v in lg.iter_mut() {
6804                                    *v *= mu;
6805                                }
6806                            }
6807                            // Gemma-class final-logit soft-capping: the
6808                            // decode paths apply it; scoring must too, or
6809                            // the uncapped softmax misprices every token.
6810                            if let Some(c) = self.final_softcap {
6811                                for v in lg.iter_mut() {
6812                                    *v = c * (*v / c).tanh();
6813                                }
6814                            }
6815                            // Cortiq Embryo hierarchical head: same correction
6816                            // the decode path applies (lm_head_forward).
6817                            if let Some(cm) = self.head_clusters.clone() {
6818                                self.hierarchical_head_logprobs(
6819                                    &normed[k * hs..(k + 1) * hs],
6820                                    &cm,
6821                                    lg,
6822                                );
6823                            }
6824                            let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
6825                            let target = ids[pos + k0 + k + 1] as usize;
6826                            let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6827                            let lse: f64 = lg
6828                                .iter()
6829                                .map(|&v| ((v - max) as f64).exp())
6830                                .sum::<f64>()
6831                                .ln()
6832                                + max as f64;
6833                            nll += lse - lg[target] as f64;
6834                            cnt += 1;
6835                            if std::env::var("CMF_PPL_TRACE").is_ok() {
6836                                let top = lg
6837                                    .iter()
6838                                    .enumerate()
6839                                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6840                                    .map(|(i, _)| i)
6841                                    .unwrap_or(0);
6842                                eprintln!(
6843                                    "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
6844                                    pos + k0 + k,
6845                                    target,
6846                                    lse - lg[target] as f64,
6847                                    top,
6848                                    lg[target],
6849                                    lg[top]
6850                                );
6851                            }
6852                        }
6853                        k0 = k1;
6854                    }
6855                    pos = end;
6856                }
6857                return Ok((nll, cnt));
6858            }
6859            for pos in 0..ids.len().saturating_sub(1) {
6860                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6861                self.nll_check_graph("serial forward", pos)?;
6862                // Architectures whose head lives inside their own stack return
6863                // the logits out of band and a zero hidden — DeepSeek-V4 folds
6864                // its hyper-connection copies between the last layer and the
6865                // norm, so it cannot hand back a vector this loop could use.
6866                // Scoring the zeros gave a perplexity of exactly the vocabulary
6867                // size, which is a uniform distribution reported as a
6868                // measurement. `generate` already reads this channel.
6869                let out_of_band = self.graph_logits.take();
6870                if self.graph_head_required && out_of_band.is_none() {
6871                    METAL_GRAPH_HEAD_MISS.fetch_add(
6872                        1,
6873                        std::sync::atomic::Ordering::Relaxed,
6874                    );
6875                    return Err(format!(
6876                        "fused Metal graph head did not complete at NLL position {pos}"
6877                    ));
6878                }
6879                if pos < start {
6880                    continue;
6881                }
6882                let logits = match out_of_band {
6883                    Some(lg) => lg,
6884                    None => {
6885                        let normed = inference::rms_norm(
6886                            &hidden,
6887                            &self.weights.final_norm,
6888                            self.rms_eps,
6889                            self.norm_style,
6890                        );
6891                        // lm_head_forward applies the final-logit softcap itself
6892                        // — capping again here double-squashed gemma-class
6893                        // logits (tanh∘tanh) and reported a flattered ppl.
6894                        self.lm_head_forward(&normed)
6895                    }
6896                };
6897                let target = ids[pos + 1] as usize;
6898                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6899                let lse: f64 = logits
6900                    .iter()
6901                    .map(|&v| ((v - max) as f64).exp())
6902                    .sum::<f64>()
6903                    .ln()
6904                    + max as f64;
6905                let tok_nll = lse - logits[target] as f64;
6906                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6907                    let top = logits
6908                        .iter()
6909                        .enumerate()
6910                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6911                        .map(|(i, _)| i)
6912                        .unwrap_or(0);
6913                    eprintln!(
6914                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6915                        logits[target], logits[top]
6916                    );
6917                }
6918                nll += tok_nll;
6919                cnt += 1;
6920            }
6921            Ok((nll, cnt))
6922        })();
6923        self.nll_end();
6924        result
6925    }
6926
6927    /// Score one post-layer hidden with the same final norm/head path used by
6928    /// decode. Keeping this in one helper is important for the production
6929    /// batch scorer: its rows stop before the final norm, just like the
6930    /// per-position O(1) path below.
6931    fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
6932        let normed = inference::rms_norm(
6933            hidden,
6934            &self.weights.final_norm,
6935            self.rms_eps,
6936            self.norm_style,
6937        );
6938        // lm_head_forward applies the final-logit softcap itself — capping
6939        // again here double-squashed gemma-class logits in earlier scorers.
6940        let mut logits = self.lm_head_forward(&normed);
6941        let target = target as usize;
6942        let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6943        let lse: f64 = logits
6944            .iter()
6945            .map(|&v| ((v - max) as f64).exp())
6946            .sum::<f64>()
6947            .ln()
6948            + max as f64;
6949        let tok_nll = lse - logits[target] as f64;
6950        if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6951            let top = logits
6952                .iter()
6953                .enumerate()
6954                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6955                .map(|(i, _)| i)
6956                .unwrap_or(0);
6957            eprintln!(
6958                "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6959                logits[target], logits[top]
6960            );
6961        }
6962        attention::recycle_buf(&mut logits);
6963        tok_nll
6964    }
6965
6966    /// Teacher-forced NLL of the CONVERTED model: the O(1) Nyström path
6967    /// is ACTIVE over the scored positions. Returns `Ok((nll sum, scored
6968    /// count))` over `prefill..len-1` and surfaces a post-mutation batch
6969    /// failure instead of returning a partial score.
6970    ///
6971    /// Runtime discipline, deliberately NOT the matrix probe's: the
6972    /// requested prefix plus any required deferred lead-in run the exact
6973    /// prompt pass — that pass is what freezes the landmarks and M — and
6974    /// every post-seal scored position goes through `NystromState::step()`,
6975    /// the same code decode runs.
6976    /// So the landmarks are PREFILL-frozen (what ships), not
6977    /// full-sequence oracles (what the published probe measured). When the
6978    /// requested prefix is shorter than the bounded transition, rows in the
6979    /// exact lead-in are still scored so the shifted target range is stable.
6980    ///
6981    /// Pair with `nll_ids_from(ids, prefill)` for the exact baseline
6982    /// over the identical token set — that ratio is the honest one.
6983    pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
6984        // This scorer consumes host hiddens, so never request the optional
6985        // token-graph lm_head side channel. `nll_begin` also consumes a
6986        // prior graph failure and clears only the cancel bit that failure
6987        // raised, leaving a caller-owned cancellation observable.
6988        self.nll_begin()?;
6989        let requested_prefix = (prefill > 0).then_some(prefill);
6990        self.o1_begin_with_prefix(requested_prefix);
6991        let n = ids.len().saturating_sub(1);
6992        let requested_start = prefill.min(n);
6993        // The exact prefix must reach the deferred boundary before a
6994        // collecting layer can convert. Rows between the requested start and
6995        // that boundary remain part of the public NLL range and are scored
6996        // from the same hidden pass below.
6997        let exact_end = if self.o1_active() {
6998            match requested_prefix {
6999                Some(requested) => self.o1_effective_boundary(requested),
7000                None => self
7001                    .o1_cfg
7002                    .as_ref()
7003                    .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
7004            }
7005            .unwrap_or(requested_start)
7006            .min(n)
7007        } else {
7008            requested_start
7009        };
7010        let mut nll = 0f64;
7011        let mut cnt = 0usize;
7012
7013        // Exact prompt pass over ids[..exact_end]: the seal consumes its
7014        // q/k/v. Rows at or after requested_start are scored here when the
7015        // bounded lead-in is longer than the caller's requested prefix.
7016        let mut pos = 0usize;
7017        if self.can_prefill_batched() {
7018            const CHUNK: usize = 128;
7019            while pos < exact_end {
7020                let end = (pos + CHUNK).min(exact_end);
7021                let hiddens = self.prefill_batch(&ids[pos..end], pos);
7022                if self
7023                    .graph_failed
7024                    .swap(false, std::sync::atomic::Ordering::Relaxed)
7025                {
7026                    self.cancel
7027                        .store(false, std::sync::atomic::Ordering::Relaxed);
7028                    self.nll_end();
7029                    return Err("GPU graph failed during O(1) NLL prefix".into());
7030                }
7031                for row in 0..end - pos {
7032                    let score_pos = pos + row;
7033                    if score_pos >= requested_start && score_pos < n {
7034                        nll += self.nll_from_hidden(
7035                            &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
7036                            ids[score_pos + 1],
7037                            score_pos,
7038                        );
7039                        cnt += 1;
7040                    }
7041                }
7042                pos = end;
7043            }
7044        } else {
7045            while pos < exact_end {
7046                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
7047                if self
7048                    .graph_failed
7049                    .swap(false, std::sync::atomic::Ordering::Relaxed)
7050                {
7051                    self.cancel
7052                        .store(false, std::sync::atomic::Ordering::Relaxed);
7053                    self.nll_end();
7054                    return Err("GPU graph failed during O(1) NLL prefix".into());
7055                }
7056                if pos >= requested_start && pos < n {
7057                    nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
7058                    cnt += 1;
7059                }
7060                pos += 1;
7061            }
7062        }
7063        self.o1_seal_checked().map_err(|err| {
7064            self.nll_end();
7065            err
7066        })?;
7067
7068        // Reuse the production whole-token batch graph for the post-seal
7069        // suffix when the caller explicitly enabled both routes. This is a
7070        // teacher-forced scorer, so every row is ids[pos] and its target is
7071        // ids[pos + 1]; no speculative tail or rollback state is involved.
7072        // A first Declined is safe to handle with the established serial O(1)
7073        // path. Once a chunk completes, however, the device recurrent state
7074        // owns the sequence and a later decline must be terminal rather than
7075        // falling back to stale CPU state.
7076        let batch_k = std::env::var("CMF_BATCH_K")
7077            .ok()
7078            .and_then(|v| v.parse::<usize>().ok())
7079            .unwrap_or(0);
7080        let batch_admitted = batch_k > 0
7081            && self.can_prefill_batched()
7082            && self.o1_active()
7083            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
7084            && (0..self.num_layers).all(|li| {
7085                let cache = &self.kv_cache.layers[self.phys_layer(li)];
7086                cache.o1.is_none() || cache.o1_views().is_some()
7087            });
7088        if std::env::var("CMF_GRAPH_PROF").is_ok() {
7089            eprintln!(
7090                "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
7091                batch_admitted,
7092                batch_k,
7093                n.saturating_sub(exact_end),
7094            );
7095        }
7096        let mut batch_completed = false;
7097        if batch_admitted && exact_end < n {
7098            let hs = self.hidden_size;
7099            let mut batch_pos = exact_end;
7100            while batch_pos < n {
7101                let end = (batch_pos + batch_k).min(n);
7102                let bk = end - batch_pos;
7103                let mut hiddens = vec![0.0f32; bk * hs];
7104                for (row, &id) in ids[batch_pos..end].iter().enumerate() {
7105                    hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
7106                }
7107                let positions: Vec<usize> = (batch_pos..end).collect();
7108                let t_batch = std::time::Instant::now();
7109                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
7110                if std::env::var("CMF_GRAPH_PROF").is_ok() {
7111                    let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
7112                    eprintln!(
7113                        "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
7114                        batch_pos,
7115                        end.saturating_sub(1),
7116                        bk as f64 / (ms / 1000.0),
7117                    );
7118                }
7119                if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
7120                    self.nll_end();
7121                    return Err(err);
7122                }
7123                match outcome {
7124                    crate::gpu::BatchGraphOutcome::Completed => {
7125                        batch_completed = true;
7126                        for row in 0..bk {
7127                            nll += self.nll_from_hidden(
7128                                &hiddens[row * hs..(row + 1) * hs],
7129                                ids[batch_pos + row + 1],
7130                                batch_pos + row,
7131                            );
7132                            cnt += 1;
7133                        }
7134                        batch_pos = end;
7135                    }
7136                    crate::gpu::BatchGraphOutcome::Declined => {
7137                        if batch_completed {
7138                            self.nll_end();
7139                            return Err(format!(
7140                                "O(1) NLL batch declined after completed chunk at position {batch_pos}"
7141                            ));
7142                        }
7143                        break;
7144                    }
7145                    crate::gpu::BatchGraphOutcome::Failed => {
7146                        self.nll_end();
7147                        return Err(format!(
7148                            "O(1) NLL batch graph failed after admission at position {batch_pos}"
7149                        ));
7150                    }
7151                }
7152            }
7153            if batch_completed && cnt == n.saturating_sub(requested_start) {
7154                self.nll_end();
7155                return Ok((nll, cnt));
7156            }
7157        }
7158
7159        // Serial O(1) fallback/reference. It is intentionally retained when
7160        // batch admission declines before mutation; callers must label this
7161        // CMF_BATCH_K=0/per-position path separately from the production
7162        // whole-token batch route.
7163        for pos in exact_end..n {
7164            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
7165            if self
7166                .graph_failed
7167                .swap(false, std::sync::atomic::Ordering::Relaxed)
7168            {
7169                self.cancel
7170                    .store(false, std::sync::atomic::Ordering::Relaxed);
7171                self.nll_end();
7172                return Err(format!(
7173                    "GPU graph failed during O(1) NLL serial scoring at position {pos}"
7174                ));
7175            }
7176            nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
7177            cnt += 1;
7178        }
7179        self.nll_end();
7180        Ok((nll, cnt))
7181    }
7182
7183    /// Teacher-forced calibration data (B1): for each position, whether the
7184    /// argmax equals the actual next token, and the top-1 softmax prob
7185    /// (top-1 probability) under EACH temperature in `temps` — all from ONE forward
7186    /// pass (argmax/correctness are temperature-invariant; only p_max
7187    /// reshapes). Feeds `cortiq calibrate` (reliability/ECE + temperature
7188    /// fit): is the model's confidence a true property, or does it need a
7189    /// measured scaling?
7190    pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
7191        self.clear_sequence_state();
7192        let n = ids.len().saturating_sub(1);
7193        let mut correct = Vec::with_capacity(n);
7194        let mut pmax = Vec::with_capacity(n);
7195        for pos in 0..n {
7196            let emb = self.embed_single(ids[pos]);
7197            let hidden = self.forward_layers(&emb, pos, None);
7198            let normed = inference::rms_norm(
7199                &hidden,
7200                &self.weights.final_norm,
7201                self.rms_eps,
7202                self.norm_style,
7203            );
7204            // lm_head_forward applies the final-logit softcap itself —
7205            // capping again here double-squashed gemma-class logits
7206            // (tanh∘tanh) and reported a flattered ppl.
7207            let logits = self.lm_head_forward(&normed);
7208            let target = ids[pos + 1] as usize;
7209            let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
7210            for (i, &v) in logits.iter().enumerate() {
7211                if v > mval {
7212                    mval = v;
7213                    amax = i;
7214                }
7215            }
7216            correct.push(amax == target);
7217            let row: Vec<f32> = temps
7218                .iter()
7219                .map(|&t| {
7220                    let tt = t.max(1e-3);
7221                    let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
7222                    1.0 / s.max(1e-12) // numerator at the max is exp(0)=1
7223                })
7224                .collect();
7225            pmax.push(row);
7226        }
7227        self.clear_sequence_state();
7228        (correct, pmax)
7229    }
7230
7231    /// Teacher-forced PPL with the dynamic router driving per-window
7232    /// skill switches (VMF experiment №2 measurement). Sequential (φ
7233    /// must update per token), returns (ppl, switch_count). The router
7234    /// must be enabled (`enable_dynamic_routing`); else this equals
7235    /// plain `ppl_ids`. The active skill when scoring token t shapes the
7236    /// logits for t+1 — on-policy over the held-out text itself.
7237    pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
7238        if self.dyn_router.is_none() {
7239            return Ok((self.ppl_ids(ids)?, 0));
7240        }
7241        self.nll_begin()?;
7242        let saved_active = self.dyn_active;
7243        let mut router = self
7244            .dyn_router
7245            .take()
7246            .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
7247        router.reset();
7248        self.dyn_phi_seen = 0;
7249        let _ = self.set_active_skill(None);
7250
7251        let result: Result<(f64, usize), String> = (|| {
7252            let mut nll = 0f64;
7253            let mut cnt = 0usize;
7254            for pos in 0..ids.len().saturating_sub(1) {
7255                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
7256                self.nll_check_graph("dynamic serial forward", pos)?;
7257                let out_of_band = self.graph_logits.take();
7258                let mut logits = match out_of_band {
7259                    Some(lg) => lg,
7260                    None => {
7261                        let normed = inference::rms_norm(
7262                            &hidden,
7263                            &self.weights.final_norm,
7264                            self.rms_eps,
7265                            self.norm_style,
7266                        );
7267                        // lm_head_forward applies the final-logit softcap itself —
7268                        // capping again here double-squashed gemma-class logits
7269                        // and reported a flattered ppl.
7270                        self.lm_head_forward(&normed)
7271                    }
7272                };
7273                let target = ids[pos + 1] as usize;
7274                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
7275                let lse: f64 = logits
7276                    .iter()
7277                    .map(|&v| ((v - max) as f64).exp())
7278                    .sum::<f64>()
7279                    .ln()
7280                    + max as f64;
7281                let tok_nll = lse - logits[target] as f64;
7282                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
7283                    let top = logits
7284                        .iter()
7285                        .enumerate()
7286                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7287                        .map(|(i, _)| i)
7288                        .unwrap_or(0);
7289                    eprintln!(
7290                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
7291                        logits[target], logits[top]
7292                    );
7293                }
7294                nll += tok_nll;
7295                cnt += 1;
7296                attention::recycle_buf(&mut logits);
7297                // Route on the evolving phi (drives the NEXT token's skill).
7298                let phi = self.dyn_phi_ema.clone();
7299                if let Some(new_active) = router.step(&phi, pos) {
7300                    let _ = self.set_active_skill(new_active);
7301                }
7302            }
7303            Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
7304        })();
7305
7306        // Restore the detached router and the active overlay on both success
7307        // and failure. The scoring state is cleared independently below.
7308        let _ = self.set_active_skill(saved_active);
7309        self.dyn_router = Some(router);
7310        self.nll_end();
7311        result
7312    }
7313
7314    /// Routing probe φ (spec §9): mean-pooled hidden after `layer`.
7315    pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
7316        self.clear_sequence_state();
7317        let mut acc = vec![0f32; self.hidden_size];
7318        for (pos, &id) in ids.iter().enumerate() {
7319            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
7320            for (a, v) in acc.iter_mut().zip(&h) {
7321                *a += v;
7322            }
7323        }
7324        let n = ids.len().max(1) as f32;
7325        for a in acc.iter_mut() {
7326            *a /= n;
7327        }
7328        self.clear_sequence_state();
7329        acc
7330    }
7331
7332    /// Layer-major batched prefill (prefill-GEMM): full-attention —
7333    /// per-position with the existing operators (KV grows naturally,
7334    /// causality preserved), GDN projections / FFN / MoE — batched
7335    /// (a weight row is read from DRAM once per chunk, not per
7336    /// position). Returns the hidden of all positions [b × hidden].
7337    fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
7338        self.prefill_batch_masked(ids, start_pos, None)
7339    }
7340
7341    /// `prefill_batch` with a task mask honored on the dense-FFN panels
7342    /// (the masked-inference fast path: full fused compute, mask lands on
7343    /// the activations). The whole-chunk GPU graph is skipped for masked
7344    /// layers by the callers' arms; the per-GEMM device paths stay in
7345    /// play because the zeroing happens on the host between them.
7346    fn prefill_batch_masked(
7347        &mut self,
7348        ids: &[u32],
7349        start_pos: usize,
7350        task_mask: Option<&TaskMask>,
7351    ) -> Vec<f32> {
7352        self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
7353    }
7354
7355    /// The layer-major batched walk over a layer span [from..upto_excl):
7356    /// the whole prefill machinery (chunk graph, batched attends, GEMM
7357    /// panels) for a PARTIAL stack — the network split's prefill rides
7358    /// the same canon as the local one. Input is token ids (embeds
7359    /// itself, coordinator side) or ready boundary hiddens (worker side).
7360    fn prefill_batch_span(
7361        &mut self,
7362        input: PrefillIn<'_>,
7363        start_pos: usize,
7364        task_mask: Option<&TaskMask>,
7365        from: usize,
7366        upto_excl: usize,
7367    ) -> Vec<f32> {
7368        let hs = self.hidden_size;
7369        let b = match input {
7370            PrefillIn::Ids(ids) => ids.len(),
7371            PrefillIn::Hidden(hb) => hb.len() / hs,
7372        };
7373        let upto_excl = upto_excl.min(self.num_layers);
7374        // The CPU embed is deferred: when the chunk graph takes the run
7375        // from layer 0 it gathers the embeddings on the device instead.
7376        // A hidden input is ready by definition.
7377        let mut h: Vec<f32>;
7378        let mut h_ready;
7379        match input {
7380            PrefillIn::Ids(_) => {
7381                h = vec![0.0; b * hs];
7382                h_ready = false;
7383            }
7384            PrefillIn::Hidden(hb) => {
7385                h = hb.to_vec();
7386                h_ready = true;
7387            }
7388        }
7389        let fill_h = |h: &mut Vec<f32>, me: &Self| {
7390            if let PrefillIn::Ids(ids) = input {
7391                for (bi, &id) in ids.iter().enumerate() {
7392                    let e = me.embed_single(id);
7393                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
7394                }
7395                if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
7396                    if let Ok(t) = tp.parse::<usize>() {
7397                        if t >= start_pos && t < start_pos + ids.len() {
7398                            let bi = t - start_pos;
7399                            let row = &h[bi * hs..(bi + 1) * hs];
7400                            let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
7401                            eprintln!(
7402                                "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
7403                                ids[bi],
7404                                row[0],
7405                                row[1],
7406                                ids.len(),
7407                                &ids[..ids.len().min(8)]
7408                            );
7409                        }
7410                    }
7411                }
7412            }
7413        };
7414        let (_nkv, _hd, _rd, eps) = (
7415            self.num_kv_heads,
7416            self.head_dim,
7417            self.rotary_dim,
7418            self.rms_eps,
7419        );
7420        let pool = self.pool.clone();
7421        let norm_style = self.norm_style;
7422        let automatic_gpu_prefix = self.automatic_gpu_prefix();
7423
7424        #[cfg(target_os = "macos")]
7425        let mut chunk_skip_until = 0usize;
7426        for li in from..upto_excl {
7427            let _capacity_tail = automatic_gpu_prefix
7428                .filter(|&prefix| li >= prefix)
7429                .map(|_| crate::gpu::enter_cpu_scope());
7430            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU
7431            // GPU chunk graph (default-on under CMF_GPU=1): a run of
7432            // consecutive eligible layers for the whole chunk in ONE
7433            // Metal submission — norm, QKV, RoPE with fused mirror
7434            // append, causal attend, O, FFN, hidden device-resident
7435            // across the run. Any refusal falls through to the CPU path.
7436            #[cfg(target_os = "macos")]
7437            if task_mask.is_none() {
7438                if li < chunk_skip_until {
7439                    continue;
7440                }
7441                // Device-side embedding needs a q8_row embedding matrix;
7442                // with any other layout the CPU fills `h` first and the
7443                // graph starts from a ready hidden (refusing the whole
7444                // run over the embedding alone kept q4t models — the
7445                // whole Nanbeige/Bonsai class — on the CPU prefill).
7446                if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
7447                    fill_h(&mut h, self);
7448                    h_ready = true;
7449                }
7450                let ids_for_embed = match input {
7451                    PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
7452                    PrefillIn::Hidden(_) => None,
7453                };
7454                let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
7455                if end > li {
7456                    h_ready = true;
7457                    chunk_skip_until = end;
7458                    // Looped Transformer: the graph stopped at a loop
7459                    // boundary — apply final norm before the next iteration.
7460                    if self.is_loop_end(end - 1) && end < self.num_layers {
7461                        for bi in 0..b {
7462                            let normed = inference::rms_norm(
7463                                &h[bi * hs..(bi + 1) * hs],
7464                                &self.weights.final_norm,
7465                                eps,
7466                                norm_style,
7467                            );
7468                            h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
7469                        }
7470                    }
7471                    continue;
7472                }
7473            }
7474            if !h_ready {
7475                fill_h(&mut h, self);
7476                h_ready = true;
7477            }
7478            let lw = &self.weights.layers[self.phys_layer(li)];
7479            // ── attention ──
7480            match &lw.attn {
7481                AttnKind::Kda(w) => {
7482                    // Projections batched, recurrence sequential.
7483                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
7484                    let mut normed = vec![0.0f32; b * hs];
7485                    for bi in 0..b {
7486                        inference::rms_norm_into(
7487                            &h[bi * hs..(bi + 1) * hs],
7488                            &lw.input_norm,
7489                            eps,
7490                            norm_style,
7491                            &mut normed[bi * hs..(bi + 1) * hs],
7492                        );
7493                    }
7494                    let attn = crate::linear_core::kda_forward_batch(
7495                        &normed,
7496                        b,
7497                        w,
7498                        &cfg,
7499                        &mut self.kv_cache.layers[li].linear_state,
7500                        pool.as_deref(),
7501                    );
7502                    for (dst, &a) in h.iter_mut().zip(&attn) {
7503                        *dst += a;
7504                    }
7505                }
7506                AttnKind::LinearGdn(w) => {
7507                    // Projections batched, recurrence sequential.
7508                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
7509                    let mut normed = vec![0.0f32; b * hs];
7510                    for bi in 0..b {
7511                        let r = inference::rms_norm(
7512                            &h[bi * hs..(bi + 1) * hs],
7513                            &lw.input_norm,
7514                            eps,
7515                            norm_style,
7516                        );
7517                        normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7518                    }
7519                    let attn = crate::linear_core::gdn_forward_batch(
7520                        &normed,
7521                        b,
7522                        w,
7523                        &cfg,
7524                        &mut self.kv_cache.layers[li].linear_state,
7525                        pool.as_deref(),
7526                    );
7527                    for (dst, &a) in h.iter_mut().zip(&attn) {
7528                        *dst += a;
7529                    }
7530                }
7531                AttnKind::ShortConv(w) => {
7532                    // Projections batched over the chunk; the conv walks the
7533                    // contiguous positions in order (same ring as decode).
7534                    let cfg = self
7535                        .short_conv_cfg
7536                        .expect("short-conv layer without short_conv_cfg");
7537                    let mut normed = vec![0.0f32; b * hs];
7538                    for bi in 0..b {
7539                        inference::rms_norm_into(
7540                            &h[bi * hs..(bi + 1) * hs],
7541                            &lw.input_norm,
7542                            eps,
7543                            norm_style,
7544                            &mut normed[bi * hs..(bi + 1) * hs],
7545                        );
7546                    }
7547                    let attn = short_conv_forward_batch(
7548                        &normed,
7549                        b,
7550                        w,
7551                        &cfg,
7552                        &mut self.kv_cache.layers[li].linear_state,
7553                        pool.as_deref(),
7554                    );
7555                    for (dst, &a) in h.iter_mut().zip(&attn) {
7556                        *dst += a;
7557                    }
7558                }
7559                AttnKind::Mla(w) => {
7560                    // Per-position prefill (correctness first; latent
7561                    // batching is a later optimization).
7562                    let inv_freq_l = self.layer_inv_freq(li);
7563                    let rs = self.layer_rope_scale(li);
7564                    let mut normed = vec![0.0f32; hs];
7565                    for bi in 0..b {
7566                        inference::rms_norm_into(
7567                            &h[bi * hs..(bi + 1) * hs],
7568                            &lw.input_norm,
7569                            eps,
7570                            norm_style,
7571                            &mut normed,
7572                        );
7573                        let ao = mla_attention(
7574                            w,
7575                            &normed,
7576                            &mut self.kv_cache.layers[li],
7577                            start_pos + bi,
7578                            &inv_freq_l,
7579                            rs,
7580                            eps,
7581                            pool.as_deref(),
7582                        );
7583                        for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
7584                            *dst += a;
7585                        }
7586                    }
7587                }
7588                AttnKind::Full {
7589                    wq,
7590                    wk,
7591                    wv,
7592                    wo,
7593                    q_norm,
7594                    k_norm,
7595                    output_gate,
7596                    softplus_gate,
7597                    bias,
7598                } => {
7599                    // Chunk-GEMM QKV/O; per-position causal attention
7600                    // inside (roadmap §3 P0 — full-attention prefill no
7601                    // longer re-reads the projection weights b times).
7602                    let mut normed = vec![0.0f32; b * hs];
7603                    for bi in 0..b {
7604                        inference::rms_norm_into(
7605                            &h[bi * hs..(bi + 1) * hs],
7606                            &lw.input_norm,
7607                            eps,
7608                            norm_style,
7609                            &mut normed[bi * hs..(bi + 1) * hs],
7610                        );
7611                    }
7612                    let inv_freq_l = self.layer_inv_freq(li);
7613                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7614                    let cfg = QwenAttnCfg {
7615                        num_heads: self.layer_num_heads(li),
7616                        num_kv_heads: nkv_l,
7617                        head_dim: hd_l,
7618                        hidden_size: hs,
7619                        position: start_pos,
7620                        inv_freq: &inv_freq_l,
7621                        rotary_dim: rd_l,
7622                        scale: self.attn_scale,
7623                        softcap: self.attn_softcap,
7624                        window: self.layer_window(li),
7625                        v_norm: self.attn_v_norm,
7626                        qk_norm_after_rope: self.qk_norm_after_rope,
7627                        q_norm: q_norm.as_deref(),
7628                        k_norm: k_norm.as_deref(),
7629                        output_gate: *output_gate,
7630                        softplus_gate: softplus_gate
7631                            .as_ref()
7632                            .map(|(gate, per_head)| (gate, *per_head)),
7633                        rope_scale: self.layer_rope_scale(li),
7634                        bias: bias
7635                            .as_ref()
7636                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7637                        rms_eps: eps,
7638                        norm_style,
7639                        pool: pool.as_deref(),
7640                    };
7641                    let mut attn = attention::qwen_attention_batch(
7642                        &normed,
7643                        b,
7644                        wq,
7645                        wk,
7646                        wv,
7647                        wo,
7648                        &mut self.kv_cache.layers[li],
7649                        &cfg,
7650                    );
7651                    if let Some(w) = &lw.attn_out_norm {
7652                        for bi in 0..b {
7653                            inference::rms_norm_into(
7654                                &attn[bi * hs..(bi + 1) * hs],
7655                                w,
7656                                eps,
7657                                norm_style,
7658                                &mut normed[bi * hs..(bi + 1) * hs],
7659                            );
7660                        }
7661                        attn.copy_from_slice(&normed);
7662                    }
7663                    for (dst, &a) in h.iter_mut().zip(&attn) {
7664                        *dst += a;
7665                    }
7666                }
7667                AttnKind::Linear(w) => {
7668                    for bi in 0..b {
7669                        let normed = inference::rms_norm(
7670                            &h[bi * hs..(bi + 1) * hs],
7671                            &lw.input_norm,
7672                            eps,
7673                            norm_style,
7674                        );
7675                        vmf_phase_forward(
7676                            &normed,
7677                            w,
7678                            &self.vmf_cfg.expect("linear layer without vmf_cfg"),
7679                            &mut self.kv_cache.layers[li].linear_state,
7680                            pool.as_deref(),
7681                        )
7682                        .iter()
7683                        .enumerate()
7684                        .for_each(|(i, &a)| h[bi * hs + i] += a);
7685                    }
7686                }
7687            }
7688
7689            // ── FFN batched ──
7690            let lw = &self.weights.layers[self.phys_layer(li)];
7691            let mut post = vec![0.0f32; b * hs];
7692            for bi in 0..b {
7693                let r =
7694                    inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
7695                post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7696            }
7697            // A restrictive per-visit FFN row lands on the activations
7698            // inside the dense arm; an all-open row costs nothing.
7699            let mask_row = task_mask
7700                .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
7701                .and_then(|m| m.ffn_masks.get(li))
7702                .map(|v| v.as_slice());
7703            let mut ffn = match &lw.ffn {
7704                FfnKind::Dense(d) if !d.segs.is_empty() => {
7705                    tube_ffn(d, &post, b, pool.as_deref(), mask_row)
7706                }
7707                FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
7708                FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
7709                // Dual-branch layers run per position (the expert branch
7710                // reads the raw residual — nothing to batch yet).
7711                FfnKind::DenseMoe(dm) => {
7712                    let mut out = vec![0.0f32; b * hs];
7713                    for bi in 0..b {
7714                        let r = dense_moe_ffn(
7715                            dm,
7716                            &post[bi * hs..(bi + 1) * hs],
7717                            &h[bi * hs..(bi + 1) * hs],
7718                            eps,
7719                            norm_style,
7720                            pool.as_deref(),
7721                        );
7722                        out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7723                    }
7724                    out
7725                }
7726            };
7727            if let Some(w) = &lw.ffn_out_norm {
7728                for bi in 0..b {
7729                    inference::rms_norm_into(
7730                        &ffn[bi * hs..(bi + 1) * hs],
7731                        w,
7732                        eps,
7733                        norm_style,
7734                        &mut post[bi * hs..(bi + 1) * hs],
7735                    );
7736                }
7737                ffn.copy_from_slice(&post);
7738            }
7739            for (dst, &f) in h.iter_mut().zip(&ffn) {
7740                *dst += f;
7741            }
7742            if let Some(sc) = lw.layer_scale {
7743                for v in h.iter_mut() {
7744                    *v *= sc;
7745                }
7746            }
7747            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
7748                if let Ok(t) = tp.parse::<usize>() {
7749                    if t >= start_pos && t < start_pos + b {
7750                        let bi = t - start_pos;
7751                        let row = &h[bi * hs..(bi + 1) * hs];
7752                        let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
7753                        eprintln!(
7754                            "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
7755                            row[0], row[1]
7756                        );
7757                    }
7758                }
7759            }
7760            // CMF_DEBUG_LAYERS=1: per-layer hidden-state health of the
7761            // LAST prompt position — the knife for "which layer type
7762            // breaks first" on a new architecture.
7763            if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
7764                let row = &h[(b - 1) * hs..b * hs];
7765                let rms =
7766                    (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
7767                let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
7768                eprintln!(
7769                    "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
7770                    match &self.weights.layers[self.phys_layer(li)].attn {
7771                        AttnKind::LinearGdn(_) => "gdn",
7772                        AttnKind::Linear(_) => "vmf",
7773                        AttnKind::ShortConv(_) => "conv",
7774                        _ => "attn",
7775                    },
7776                    match &lw.ffn {
7777                        FfnKind::Moe(_) => "moe",
7778                        FfnKind::Dense(_) => "dense",
7779                        FfnKind::DenseMoe(_) => "dense+moe",
7780                    },
7781                );
7782            }
7783            // Looped Transformer: apply final norm at the end of each loop iteration.
7784            if self.is_loop_end(li) && li + 1 < self.num_layers {
7785                for bi in 0..b {
7786                    let normed = inference::rms_norm(
7787                        &h[bi * hs..(bi + 1) * hs],
7788                        &self.weights.final_norm,
7789                        eps,
7790                        norm_style,
7791                    );
7792                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
7793                }
7794            }
7795            if std::env::var("CMF_TRACE_H").is_ok() {
7796                let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
7797                let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
7798                eprintln!(
7799                    "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
7800                    lw.layer_scale
7801                );
7802            }
7803        }
7804        crate::gpu::set_layer(-1); // lm_head/final ops outside layer-split
7805        // A batched span owns a complete set of positions. Publish any
7806        // collecting→sealed transition only after every layer has finished;
7807        // callers that cross into serial/device work must see the new epoch
7808        // before this function returns.
7809        self.o1_progress();
7810        h
7811    }
7812
7813    /// Embed a single token.
7814    fn embed_single(&self, id: u32) -> Vec<f32> {
7815        let mut out = vec![0.0f32; self.hidden_size];
7816        if (id as usize) < self.weights.embed_tokens.rows() {
7817            self.weights.embed_tokens.row_f32(id as usize, &mut out);
7818        }
7819        if self.embed_multiplier != 1.0 {
7820            for v in out.iter_mut() {
7821                *v *= self.embed_multiplier;
7822            }
7823        }
7824        // DeepSeek-V4's hash layers route by TOKEN ID, so the id has to
7825        // reach the forward. It rides in slot 0 (the forward re-reads the
7826        // real embedding itself from the table).
7827        if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
7828            let mut v = vec![0.0f32; self.hidden_size.max(1)];
7829            v[0] = id as f32;
7830            return v;
7831        }
7832        // Gemma-3n: the per-layer-embedding half needs the token ID, so
7833        // it rides appended to the embedding; the g3n forward splits it.
7834        if let Some(b) = &self.g3n {
7835            return b.0.extend_embedding(id, &out, self.pool.as_deref());
7836        }
7837        out
7838    }
7839
7840    /// A run of consecutive prefill layers on the GPU for the whole
7841    /// chunk (default-on under CMF_GPU=1; CMF_GPU_CHUNK=0 disables).
7842    /// Eligibility per layer: q8_row weights, plain full attention
7843    /// (no output gate), F32 KV, no o1/masks/gemma extras. Returns the
7844    /// first layer index NOT processed (== `li0` when the run is empty).
7845    #[cfg(target_os = "macos")]
7846    fn chunk_run_gpu(
7847        &mut self,
7848        li0: usize,
7849        h: &mut [f32],
7850        b: usize,
7851        pos0: usize,
7852        embed_ids: Option<&[u32]>,
7853        cap: usize,
7854    ) -> usize {
7855        // (The old streaming attend needed a depth bound at ~1k; the
7856        // GEMM attention scales like the CPU path and lifted it.)
7857        // CMF_GPU_CHUNK=0 disables the graph.
7858        if !crate::gpu::enabled_here()
7859            || std::env::var("CMF_GPU_CHUNK")
7860                .map(|v| v == "0")
7861                .unwrap_or(false)
7862            || b < 32
7863            || self.swa.is_some()
7864            || self.global_attn.is_some()
7865            // Collection owns the exact Q trace and boundary conversion;
7866            // this chunk graph appends dense KV without feeding that trace.
7867            || self.o1_active()
7868            || self.attn_v_norm
7869            || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
7870        {
7871            return li0;
7872        }
7873        let Some(model) = self.model.clone() else {
7874            return li0;
7875        };
7876        let inv_freq = self.inv_freq.clone();
7877        let (nh, nkv, hd, hs) = (
7878            self.num_heads,
7879            self.num_kv_heads,
7880            self.head_dim,
7881            self.hidden_size,
7882        );
7883        // Collect the longest run of consecutive eligible layers.
7884        // Looped Transformer: stop at the loop boundary so the CPU can
7885        // apply loop_final_norm between iterations.
7886        let loop_end = if self.loop_final_norm {
7887            ((li0 / self.physical_layers) + 1) * self.physical_layers
7888        } else {
7889            self.num_layers
7890        };
7891        let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
7892        let mut stored_at: Vec<usize> = Vec::new();
7893        for li in li0..self.num_layers.min(loop_end).min(cap) {
7894            let lw = &self.weights.layers[self.phys_layer(li)];
7895            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
7896                break;
7897            }
7898            let AttnKind::Full {
7899                wq,
7900                wk,
7901                wv,
7902                wo,
7903                q_norm,
7904                k_norm,
7905                output_gate: false,
7906                softplus_gate: None,
7907                bias,
7908            } = &lw.attn
7909            else {
7910                break;
7911            };
7912            let FfnKind::Dense(d) = &lw.ffn else { break };
7913            if d.act != Act::Silu || !d.segs.is_empty() {
7914                break;
7915            }
7916            // q8_row (row_scale populated), or q4_tiled / q4tp (row_scale
7917            // empty — their scales are in the payload). Mixing across the
7918            // seven projections of one layer is fine; the encoder branches
7919            // per weight on the tensor's dtype. Anything else refuses.
7920            fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
7921                t.q8_row_parts()
7922                    .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7923                    .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7924            }
7925            let parts = (
7926                cw(wq),
7927                cw(wk),
7928                cw(wv),
7929                cw(wo),
7930                cw(&d.gate_proj),
7931                cw(&d.up_proj),
7932                cw(&d.down_proj),
7933            );
7934            let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
7935            else {
7936                break;
7937            };
7938            let layer = &self.kv_cache.layers[li];
7939            if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
7940                break;
7941            }
7942            stored_at.push(layer.head_len(0));
7943            layers.push(crate::gpu_metal::ChunkLayer {
7944                model: &model,
7945                kv_id: self.graph_kv_id,
7946                layer: li,
7947                wq: pq,
7948                wk: pk,
7949                wv: pv,
7950                wo: po,
7951                gate: pg,
7952                up: pu,
7953                down: pd,
7954                input_norm: &lw.input_norm,
7955                post_norm: &lw.post_norm,
7956                bias: bias
7957                    .as_ref()
7958                    .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
7959                q_norm: q_norm.as_deref(),
7960                k_norm: k_norm.as_deref(),
7961                inv_freq: &inv_freq,
7962                rd: self.rotary_dim,
7963                nh,
7964                nkv,
7965                hd,
7966                hs,
7967                inter: d.gate_proj.rows(),
7968                gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
7969                late_qk_norm: self.qk_norm_after_rope,
7970                eps: self.rms_eps as f32,
7971            });
7972        }
7973        if layers.is_empty() {
7974            return li0;
7975        }
7976        let row = nkv * hd;
7977        let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
7978            .iter()
7979            .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
7980            .collect();
7981        let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
7982        for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
7983            let li = layers[i].layer;
7984            let layer = &self.kv_cache.layers[li];
7985            io.push(crate::gpu_metal::ChunkIo {
7986                cpu_stored: stored_at[i],
7987                cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
7988                cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
7989                out_k: ok,
7990                out_v: ov,
7991                imp: oi,
7992            });
7993        }
7994        let n_run = layers.len();
7995        let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
7996        // Device-side embedding when the run starts the model and the
7997        // embedding matrix is q8_row-mapped.
7998        let ep = embed_ids.and_then(|ids| {
7999            self.weights
8000                .embed_tokens
8001                .q8_row_parts()
8002                .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
8003                    idx,
8004                    rows,
8005                    row_scale: rs,
8006                    ids,
8007                    mult: self.embed_multiplier,
8008                })
8009        });
8010        if embed_ids.is_some() && ep.is_none() {
8011            return li0;
8012        }
8013        if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
8014            return li0;
8015        }
8016        drop(io);
8017        drop(layers);
8018        // CPU caches stay the owners of record: append the chunk rows
8019        // and bank the importance masses per layer.
8020        for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
8021            let li = li0 + i;
8022            let layer = &mut self.kv_cache.layers[li];
8023            for bi in 0..b {
8024                layer.append(
8025                    &ok[bi * row..(bi + 1) * row],
8026                    &ov[bi * row..(bi + 1) * row],
8027                    &[],
8028                );
8029            }
8030            layer.accumulate_imp(oi);
8031        }
8032        last
8033    }
8034
8035    /// Is layer `li` a sliding-window (local-RoPE) layer? Gemma-3:
8036    /// every `pattern`-th layer is global, the rest are local.
8037    fn layer_is_local(&self, li: usize) -> bool {
8038        if let Some(layers) = &self.sliding_layers {
8039            return layers.get(li).copied().unwrap_or(false);
8040        }
8041        match self.swa {
8042            Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
8043            None => false,
8044        }
8045    }
8046
8047    /// The RoPE table for layer `li` (local layers may have their own;
8048    /// Gemma-4 global layers use the proportional padded table).
8049    fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
8050        if self.layer_is_local(li) {
8051            if let Some(f) = &self.inv_freq_local {
8052                return f.clone();
8053            }
8054        } else if let Some(f) = &self.inv_freq_global {
8055            return f.clone();
8056        }
8057        self.inv_freq.clone()
8058    }
8059
8060    /// The attend window for layer `li` (None = full context).
8061    fn layer_window(&self, li: usize) -> Option<usize> {
8062        self.swa
8063            .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
8064    }
8065
8066    fn layer_num_heads(&self, li: usize) -> usize {
8067        self.attention_heads_per_layer
8068            .as_ref()
8069            .and_then(|v| v.get(li).copied())
8070            .unwrap_or(self.num_heads)
8071    }
8072
8073    fn layer_rope_scale(&self, li: usize) -> f32 {
8074        if self.layer_is_local(li) {
8075            self.rope_scale_local
8076        } else {
8077            self.rope_scale
8078        }
8079    }
8080
8081    /// Attention geometry of layer `li`: (num_kv_heads, head_dim,
8082    /// rotary_dim). Gemma-4 global layers override all three.
8083    fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
8084        if !self.layer_is_local(li) {
8085            if let Some((ghd, gkv)) = self.global_attn {
8086                return (gkv, ghd, ghd);
8087            }
8088        }
8089        (
8090            self.num_kv_heads,
8091            self.head_dim,
8092            if self.layer_is_local(li) {
8093                self.rotary_dim_local.unwrap_or(self.rotary_dim)
8094            } else {
8095                self.rotary_dim
8096            },
8097        )
8098    }
8099
8100    /// Forward one position through all layers (hybrid dispatch).
8101    fn forward_layers(
8102        &mut self,
8103        hidden: &[f32],
8104        position: usize,
8105        task_mask: Option<&TaskMask>,
8106    ) -> Vec<f32> {
8107        let out = self.forward_layers_upto(hidden, position, task_mask, None);
8108        self.o1_progress();
8109        out
8110    }
8111
8112    // ── Network pipeline-split building blocks (coordinator/worker) ──
8113    // A remote worker owns layers [from ..= upto] and their KV; the
8114    // coordinator owns the rest plus embed / final norm / head. Attention
8115    // causality is per-layer, so a whole prompt's boundary hiddens ship
8116    // as one batch and decode ships one vector per token.
8117
8118    /// Embed one token id (embed multiplier applied).
8119    pub fn embed_id(&self, id: u32) -> Vec<f32> {
8120        self.embed_single(id)
8121    }
8122
8123    /// Refuse the archs/modes whose forward cannot be cut at a layer
8124    /// boundary. Loud by design: a split that silently changed the math
8125    /// would be a chimera.
8126    pub fn split_supported(&self) -> Result<(), String> {
8127        if self.dsv4.is_some() {
8128            return Err(
8129                "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
8130            );
8131        }
8132        if self.dsv41.is_some() {
8133            return Err(
8134                "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
8135                    .into(),
8136            );
8137        }
8138        if self.qwen4_exp.is_some() {
8139            return Err(
8140                "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
8141            );
8142        }
8143        if self.g3n.is_some() {
8144            return Err(
8145                "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
8146            );
8147        }
8148        Ok(())
8149    }
8150
8151    /// Forward `hidden` through layers [from ..= upto] at `position`,
8152    /// appending those layers' KV/state. Both split sides call this
8153    /// over their own range; a task mask applies to the span's own
8154    /// layers (each side masks what it runs).
8155    pub fn forward_span(
8156        &mut self,
8157        hidden: &[f32],
8158        position: usize,
8159        from: usize,
8160        upto: usize,
8161        task_mask: Option<&TaskMask>,
8162    ) -> Result<Vec<f32>, String> {
8163        self.split_supported()?;
8164        if from > upto || upto >= self.num_layers {
8165            return Err(format!(
8166                "forward_span: layer range {from}..={upto} outside 0..{}",
8167                self.num_layers
8168            ));
8169        }
8170        if hidden.len() != self.hidden_size {
8171            return Err(format!(
8172                "forward_span: hidden len {} ≠ hidden_size {}",
8173                hidden.len(),
8174                self.hidden_size
8175            ));
8176        }
8177        let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
8178        self.o1_progress();
8179        if self
8180            .graph_failed
8181            .swap(false, std::sync::atomic::Ordering::Relaxed)
8182        {
8183            self.cancel
8184                .store(false, std::sync::atomic::Ordering::Relaxed);
8185            self.clear_sequence_state();
8186            return Err("forward_span: deferred O(1) transition failed".into());
8187        }
8188        Ok(out)
8189    }
8190
8191    /// Final norm + lm_head over a boundary hidden (the final-logit
8192    /// softcap is applied by lm_head_forward itself).
8193    pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
8194        let normed = inference::rms_norm(
8195            hidden,
8196            &self.weights.final_norm,
8197            self.rms_eps,
8198            self.norm_style,
8199        );
8200        self.lm_head_forward(&normed)
8201    }
8202
8203    /// Sample the next token with this pipeline's sampler state.
8204    pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
8205        sampler::sample_with_scratch(
8206            logits,
8207            &self.sampler_config,
8208            past_tokens,
8209            &mut self.rng,
8210            &mut self.sampler_scratch,
8211        )
8212    }
8213
8214    /// Fresh sequence: clear KV, reuse history and device mirrors.
8215    pub fn reset_session(&mut self) {
8216        self.clear_sequence_state();
8217    }
8218
8219    /// Batched span prefill from token ids (coordinator side): embed +
8220    /// layers [0 ..= upto]; returns the boundary hiddens of ALL positions
8221    /// (ids.len() × hidden). Rides the same layer-major machinery as the
8222    /// local prefill; falls back to the per-position walk under
8223    /// CMF_PREFILL=seq.
8224    pub fn prefill_span_ids(
8225        &mut self,
8226        ids: &[u32],
8227        start_pos: usize,
8228        upto: usize,
8229        task_mask: Option<&TaskMask>,
8230    ) -> Result<Vec<f32>, String> {
8231        self.split_supported()?;
8232        if upto >= self.num_layers {
8233            return Err(format!(
8234                "prefill_span_ids: upto {upto} outside 0..{}",
8235                self.num_layers
8236            ));
8237        }
8238        // Same predicate as the whole-stack prefill: a span whose GDN
8239        // state lives on the device must walk positions through the
8240        // graph, not through the batched CPU span.
8241        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
8242            let out =
8243                self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
8244            self.check_o1_progress_failure("prefill_span_ids")?;
8245            Ok(out)
8246        } else {
8247            let hs = self.hidden_size;
8248            let mut out = Vec::with_capacity(ids.len() * hs);
8249            for (i, &id) in ids.iter().enumerate() {
8250                let emb = self.embed_id(id);
8251                out.extend_from_slice(&self.forward_span(
8252                    &emb,
8253                    start_pos + i,
8254                    0,
8255                    upto,
8256                    task_mask,
8257                )?);
8258            }
8259            Ok(out)
8260        }
8261    }
8262
8263    /// Batched span prefill from boundary hiddens (worker side): layers
8264    /// [from ..= upto] for every position in the batch; returns the batch.
8265    pub fn prefill_span_hidden(
8266        &mut self,
8267        hidden: &[f32],
8268        start_pos: usize,
8269        from: usize,
8270        upto: usize,
8271        task_mask: Option<&TaskMask>,
8272    ) -> Result<Vec<f32>, String> {
8273        self.split_supported()?;
8274        let hs = self.hidden_size;
8275        if hidden.is_empty() || hidden.len() % hs != 0 {
8276            return Err(format!(
8277                "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
8278                hidden.len()
8279            ));
8280        }
8281        if from > upto || upto >= self.num_layers {
8282            return Err(format!(
8283                "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
8284                self.num_layers
8285            ));
8286        }
8287        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
8288            let out = self.prefill_batch_span(
8289                PrefillIn::Hidden(hidden),
8290                start_pos,
8291                task_mask,
8292                from,
8293                upto + 1,
8294            );
8295            self.check_o1_progress_failure("prefill_span_hidden")?;
8296            Ok(out)
8297        } else {
8298            let b = hidden.len() / hs;
8299            let mut out = Vec::with_capacity(hidden.len());
8300            for i in 0..b {
8301                let h = self.forward_span(
8302                    &hidden[i * hs..(i + 1) * hs],
8303                    start_pos + i,
8304                    from,
8305                    upto,
8306                    task_mask,
8307                )?;
8308                out.extend_from_slice(&h);
8309            }
8310            Ok(out)
8311        }
8312    }
8313
8314    /// Build the whole-token wgpu graph for a pure-attention q1 model (every
8315    /// layer Full q1 + dense q1 FFN, no gate/bias). Returns the post-stack
8316    /// hidden (caller does final norm + lm_head), or None to fall back.
8317    fn try_token_graph_wgpu(
8318        &self,
8319        hidden: &[f32],
8320        position: usize,
8321        logits_out: &mut Vec<f32>,
8322        layers_run: &mut usize,
8323    ) -> Option<Result<Vec<f32>, ()>> {
8324        self.try_token_graph_wgpu_steps(
8325            hidden,
8326            position,
8327            logits_out,
8328            1,
8329            None,
8330            Some(layers_run),
8331            0,
8332            self.num_layers,
8333        )
8334    }
8335
8336    /// The span twin (network split): the graph covers [from..upto_excl)
8337    /// — one submit per SEGMENT per token. lm_head folds in only when
8338    /// the span reaches the last layer.
8339    fn try_token_graph_wgpu_span(
8340        &self,
8341        hidden: &[f32],
8342        position: usize,
8343        logits_out: &mut Vec<f32>,
8344        from: usize,
8345        upto_excl: usize,
8346        layers_run: &mut usize,
8347    ) -> Option<Result<Vec<f32>, ()>> {
8348        self.try_token_graph_wgpu_steps(
8349            hidden,
8350            position,
8351            logits_out,
8352            1,
8353            None,
8354            Some(layers_run),
8355            from,
8356            upto_excl,
8357        )
8358    }
8359
8360    /// Greedy burst: forward `t_next` and let the device pick + re-embed
8361    /// the next k−1 tokens — k frames, ONE submit, k ids back. The ZML
8362    /// trade, on wgpu. None ⇒ caller keeps the per-token path.
8363    fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
8364        if self.o1_active() || self.attn_softcap > 0.0 {
8365            return None;
8366        }
8367        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
8368        if !graph_on || crate::gpu::graph_unsupported() {
8369            // Same memo as the decode site: this path builds the very
8370            // same graph, so a model it cannot build for must not be
8371            // walked again here either. Missing this guard was worth
8372            // 2.5x on an Adreno — 0.361 tok/s against 0.905 — because
8373            // the burst retried per token what decode had already given
8374            // up on.
8375            return None;
8376        }
8377        let emb = self.embed_single(t_next);
8378        let mut lg = Vec::new();
8379        let mut ids = Vec::new();
8380        match self.try_token_graph_wgpu_steps(
8381            &emb,
8382            position,
8383            &mut lg,
8384            k,
8385            Some(&mut ids),
8386            None,
8387            0,
8388            self.num_layers,
8389        ) {
8390            Some(Ok(_)) => {}
8391            Some(Err(())) => {
8392                // Preserve the backend's post-admission failure through the
8393                // Option-based burst API.  The decode caller consumes this
8394                // flag and clears the sequence instead of falling through
8395                // to a stale CPU recurrent state.
8396                self.graph_failed
8397                    .store(true, std::sync::atomic::Ordering::Relaxed);
8398                return None;
8399            }
8400            None => return None,
8401        }
8402        (ids.len() == k).then_some(ids)
8403    }
8404
8405    /// Multi-step greedy: k whole frames in ONE submit, argmax and re-embed
8406    /// on the device. `ids_out` receives the k winner ids; the hidden/logits
8407    /// outputs are NOT produced in that mode.
8408    fn try_token_graph_wgpu_steps(
8409        &self,
8410        hidden: &[f32],
8411        position: usize,
8412        logits_out: &mut Vec<f32>,
8413        steps: usize,
8414        ids_out: Option<&mut Vec<u32>>,
8415        layers_run: Option<&mut usize>,
8416        from: usize,
8417        upto_excl: usize,
8418    ) -> Option<Result<Vec<f32>, ()>> {
8419        // O(1) Nyström decode runs off the sealed state, not the KV cache the
8420        // graph mirrors — never take the graph while o1 is active.
8421        let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
8422        if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
8423            // Softcapped scores have no graph kernel yet — CPU owns them.
8424            // o1 rides the graph only behind CMF_O1_GPU=1 while the port
8425            // proves itself; without it the CPU path owns o1 as before.
8426            return None;
8427        }
8428        // Per-layer sealed o1 state for the graph. During prefill the
8429        // state is still Collecting -> views are None -> the graph
8430        // refuses below and the CPU prefill records the q trace and
8431        // seals, exactly as the o1 design requires.
8432        let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
8433            .map(|li| {
8434                if !o1_gpu {
8435                    return None;
8436                }
8437                self.kv_cache.layers[self.phys_layer(li)].o1_views()
8438            })
8439            .collect();
8440        if self.o1_active() && o1_gpu {
8441            // Any o1 layer not sealed (or degenerate exact-only) keeps the
8442            // whole token on the CPU: half-graph forwards would desync.
8443            let want: usize = (from..upto_excl)
8444                .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
8445                .count();
8446            let have = o1_views.iter().filter(|v| v.is_some()).count();
8447            if want == 0 || have != want {
8448                // The silent twin of the gpu-side o1 gates, found the
8449                // same way: a 15x decode drop with an empty log. Views
8450                // stay None until the layer's state SEALS, so `have`
8451                // lagging `want` early in a run is the o1 design working
8452                // — but it must say so, or the next reader spends a
8453                // night proving the kernels innocent.
8454                // On CHANGE, not once: the first decline is the legal
8455                // unsealed prefill, and a once-print buries the state
8456                // that matters — what the count reads AFTER the seal.
8457                use std::sync::atomic::{AtomicUsize, Ordering};
8458                static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
8459                let code = have * 1000 + want;
8460                if LAST.swap(code, Ordering::Relaxed) != code {
8461                    tracing::warn!(
8462                        "o1 graph: {have} of {want} layers sealed — per-op until all seal"
8463                    );
8464                }
8465                return None;
8466            }
8467        }
8468        let nh = self.num_heads;
8469        let (nkv, hd, rd) = self.layer_geom(0);
8470        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8471        let mut layers = Vec::with_capacity(upto_excl - from);
8472        let mut model = None;
8473        let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
8474        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
8475            if let Some((m, i, kind, rs)) = t
8476                .graph_weight()
8477                .or_else(|| t.graph_weight_descriptor())
8478            {
8479                let name = &m.tensors[i].name;
8480                let prism = if crate::prism::is_inverse_embedding(m, name) {
8481                    crate::gpu::GraphPrismOp::InverseEmbedding
8482                } else if crate::prism::is_forward_weight(m, name) {
8483                    crate::gpu::GraphPrismOp::Forward
8484                } else {
8485                    crate::gpu::GraphPrismOp::None
8486                };
8487                return Some(crate::gpu::GraphW {
8488                    idx: i,
8489                    kind,
8490                    row_scale: rs,
8491                    data: &[],
8492                    prism,
8493                    affine: crate::prism::is_affine_target(m, name),
8494                });
8495            }
8496            // Small unquantized projections (GDN in_proj_a/b) stay f32.
8497            match t.as_f32() {
8498                Some(d) => Some(crate::gpu::GraphW {
8499                    idx: 0,
8500                    kind: 4,
8501                    row_scale: &[],
8502                    data: d,
8503                    prism: crate::gpu::GraphPrismOp::None,
8504                    affine: false,
8505                }),
8506                None => {
8507                    if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
8508                        eprintln!("batch graph: weight has no graph/f32 representation");
8509                    }
8510                    None
8511                }
8512            }
8513        }
8514        for li in from..upto_excl {
8515            let lw = &self.weights.layers[self.phys_layer(li)];
8516            if dbg {
8517                let ak = match &lw.attn {
8518                    AttnKind::Mla(_) => "Mla".into(),
8519                    AttnKind::Full {
8520                        output_gate, bias, ..
8521                    } => format!("Full gate={output_gate} bias={}", bias.is_some()),
8522                    AttnKind::LinearGdn(_) => "LinearGdn".into(),
8523                    AttnKind::Kda(_) => "Kda".into(),
8524                    AttnKind::Linear(_) => "Linear".into(),
8525                    AttnKind::ShortConv(_) => "ShortConv".into(),
8526                };
8527                let fk = match &lw.ffn {
8528                    FfnKind::Dense(_) => "Dense",
8529                    FfnKind::Moe(_) => "Moe",
8530                    FfnKind::DenseMoe(_) => "DenseMoe",
8531                };
8532                eprintln!("graph L{li}: attn={ak} ffn={fk}");
8533            }
8534            let gffn = match &lw.ffn {
8535                FfnKind::DenseMoe(_) => return None, // dual branch: CPU path
8536                // A tube layer is several matrices, not one — the
8537                // whole-layer graph has no shape for it yet.
8538                FfnKind::Dense(d) if !d.segs.is_empty() => return None,
8539                FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
8540                    gate: gw(&d.gate_proj)?,
8541                    up: gw(&d.up_proj)?,
8542                    down: gw(&d.down_proj)?,
8543                },
8544                FfnKind::Moe(m) => {
8545                    // Adaptive τ and expert masks keep the CPU path, where
8546                    // they are implemented. Sigmoid routing with a selection
8547                    // bias (LFM2-MoE / DeepSeek noaux_tc), a routed scale ≠ 1
8548                    // and an UNGATED shared expert (HunYuan hy_v3: ×2.826 on
8549                    // the routed mix, the shared expert at weight 1) are all
8550                    // graphed — before, every such token fell to the per-op
8551                    // path whole (145 submits/token on Hy-MT2-30B-A3B).
8552                    if m.route_tau.is_some() || m.mask.is_some() {
8553                        return None;
8554                    }
8555                    let shared = m.shared.as_ref();
8556                    let has_shared = shared.is_some();
8557                    let shared_gated = matches!(shared, Some((_, Some(_))));
8558                    let sgate = match shared {
8559                        Some((_, Some(sg))) => gw(sg)?,
8560                        // No gate (hy_v3) or no shared expert at all: the
8561                        // router weight stands in so the plumbing stays
8562                        // total; the select kernels pin weight 1 or skip.
8563                        _ => gw(&m.router)?,
8564                    };
8565                    let router = gw(&m.router)?;
8566                    // The resident MoE kernels do not yet carry the
8567                    // descriptor-aware transform through router/shared-gate
8568                    // selection.  Refuse the complete layer instead of
8569                    // scoring with an untransformed Prism plane (the dense
8570                    // path has an explicit FWHT boundary below).
8571                    if router.prism != crate::gpu::GraphPrismOp::None
8572                        || sgate.prism != crate::gpu::GraphPrismOp::None
8573                        || router.affine
8574                        || sgate.affine
8575                    {
8576                        tracing::warn!(
8577                            "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
8578                        );
8579                        return None;
8580                    }
8581                    let inter = m.experts.first()?.gate_proj.rows();
8582                    let mut experts = Vec::with_capacity(m.experts.len() + 1);
8583                    // q4t or q4tp, but not both in one layer — the kernels
8584                    // are picked per layer, not per expert.
8585                    let mut q4tp: Option<bool> = None;
8586                    // The mixed 2-bit profile: q2tp gate/up over a q4tp
8587                    // down. Uniform across the layer, like `q4tp` itself.
8588                    let mut gu_q2: Option<bool> = None;
8589                    for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
8590                        if !matches!(e.act, Act::Silu)
8591                            || e.gate_proj.rows() != inter
8592                            || e.up_proj.rows() != inter
8593                        {
8594                            return None;
8595                        }
8596                        // Expert tensors are packed into one resident buffer
8597                        // and the MoE kernels have no transform slot per
8598                        // expert.  Keep the CPU/per-op owner for Prism or
8599                        // affine experts rather than silently using raw bytes.
8600                        for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
8601                            let Some((em, ei, _, _)) = expert_weight
8602                                .graph_weight()
8603                                .or_else(|| expert_weight.graph_weight_descriptor())
8604                            else {
8605                                return None;
8606                            };
8607                            let name = &em.tensors[ei].name;
8608                            if crate::prism::is_forward_weight(em, name)
8609                                || crate::prism::is_inverse_embedding(em, name)
8610                                || crate::prism::is_affine_target(em, name)
8611                            {
8612                                tracing::warn!(
8613                                    "resident MoE declined: expert Prism/affine transform is not implemented"
8614                                );
8615                                return None;
8616                            }
8617                        }
8618                        let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8619                            Some((mm, gi)) => (
8620                                mm,
8621                                gi,
8622                                e.up_proj.mapped_q4t()?.1,
8623                                e.down_proj.mapped_q4t()?.1,
8624                                false,
8625                                false,
8626                            ),
8627                            None => match e.gate_proj.mapped_q2tp() {
8628                                Some((mm, gi)) => (
8629                                    mm,
8630                                    gi,
8631                                    e.up_proj.mapped_q2tp()?.1,
8632                                    e.down_proj.mapped_q4tp()?.1,
8633                                    true,
8634                                    true,
8635                                ),
8636                                None => {
8637                                    let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8638                                    (
8639                                        mm,
8640                                        gi,
8641                                        e.up_proj.mapped_q4tp()?.1,
8642                                        e.down_proj.mapped_q4tp()?.1,
8643                                        true,
8644                                        false,
8645                                    )
8646                                }
8647                            },
8648                        };
8649                        if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
8650                        {
8651                            // The shared expert rides in the same packed
8652                            // buffer as the routed ones, so a layer that
8653                            // mixes layouts cannot be indexed by one stride.
8654                            // Say so: the symptom is a whole model quietly
8655                            // running its MoE on the CPU.
8656                            tracing::warn!(
8657                                "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."
8658                            );
8659                            return None;
8660                        }
8661                        model.get_or_insert_with(|| mm.clone());
8662                        experts.push((gi, ui, di));
8663                    }
8664                    crate::gpu::GraphFfn::Moe {
8665                        router,
8666                        shared_gate: sgate,
8667                        experts,
8668                        n_exp: m.experts.len(),
8669                        // CMF_TOPK_PROBE: timing probe only — output is WRONG.
8670                        // Fewer experts shrink the MoE arithmetic while the
8671                        // dispatch count stays identical, which is the only
8672                        // clean way to tell a launch-bound decode from a
8673                        // compute-bound one.
8674                        top_k: std::env::var("CMF_TOPK_PROBE")
8675                            .ok()
8676                            .and_then(|v| v.parse::<usize>().ok())
8677                            .filter(|k| *k > 0 && *k <= m.top_k)
8678                            .unwrap_or(m.top_k),
8679                        inter,
8680                        norm_topk: m.norm_topk_prob,
8681                        q4tp: q4tp?,
8682                        gu_q2: gu_q2.unwrap_or(false),
8683                        sigmoid: m.router_sigmoid,
8684                        bias: m.expert_bias.as_deref(),
8685                        has_shared,
8686                        shared_gated,
8687                        route_scale: m.routed_scaling,
8688                    }
8689                }
8690            };
8691            let attn = match &lw.attn {
8692                AttnKind::Full {
8693                    wq,
8694                    wk,
8695                    wv,
8696                    wo,
8697                    q_norm,
8698                    k_norm,
8699                    output_gate,
8700                    softplus_gate,
8701                    bias,
8702                } => {
8703                    if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
8704                        return None;
8705                    }
8706                    let (m, _, _, _) = wq
8707                        .graph_weight()
8708                        .or_else(|| wq.graph_weight_descriptor())?;
8709                    model = Some(m.clone());
8710                    crate::gpu::GraphAttn::Full {
8711                        wq: gw(wq)?,
8712                        wk: gw(wk)?,
8713                        wv: gw(wv)?,
8714                        wo: gw(wo)?,
8715                        q_norm: q_norm.as_deref(),
8716                        k_norm: k_norm.as_deref(),
8717                        late_qk_norm: self.qk_norm_after_rope,
8718                        bias: bias
8719                            .as_ref()
8720                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8721                        output_gate: *output_gate,
8722                        cpu_k: self.kv_cache.layers[li].k_heads(),
8723                        cpu_v: self.kv_cache.layers[li].v_heads(),
8724                    }
8725                }
8726                AttnKind::LinearGdn(w) => {
8727                    let cfg = self.gdn_cfg?;
8728                    let (m, _, _, _) = w
8729                        .in_proj_qkv
8730                        .graph_weight()
8731                        .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
8732                    model = Some(m.clone());
8733                    crate::gpu::GraphAttn::Gdn {
8734                        qkv: gw(&w.in_proj_qkv)?,
8735                        z: gw(&w.in_proj_z)?,
8736                        a: gw(&w.in_proj_a)?,
8737                        b: gw(&w.in_proj_b)?,
8738                        out: gw(&w.out_proj)?,
8739                        conv1d: &w.conv1d,
8740                        a_log: &w.a_log,
8741                        dt_bias: &w.dt_bias,
8742                        norm: &w.norm,
8743                        nv: cfg.num_v_heads,
8744                        nk: cfg.num_k_heads,
8745                        dk: cfg.key_head_dim,
8746                        dv: cfg.value_head_dim,
8747                        kk: cfg.conv_kernel,
8748                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8749                    }
8750                }
8751                AttnKind::ShortConv(w) => {
8752                    let cfg = self.short_conv_cfg?;
8753                    let (m, _, _, _) = w
8754                        .in_proj
8755                        .graph_weight()
8756                        .or_else(|| w.in_proj.graph_weight_descriptor())?;
8757                    model = Some(m.clone());
8758                    crate::gpu::GraphAttn::ShortConv {
8759                        inp: gw(&w.in_proj)?,
8760                        out: gw(&w.out_proj)?,
8761                        taps: &w.conv,
8762                        kernel: cfg.kernel,
8763                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8764                    }
8765                }
8766                _ => return None,
8767            };
8768            layers.push(crate::gpu::GraphLayer {
8769                input_norm: &lw.input_norm,
8770                attn,
8771                post_norm: &lw.post_norm,
8772                ffn: gffn,
8773            });
8774        }
8775        let model = model?;
8776        // Fold final-norm + lm_head into the graph when this call wants logits
8777        // and the lm_head is a graphable (quantized) weight — the graph then
8778        // reads back logits (into logits_out) instead of the hidden, dropping
8779        // the separate CPU/GPU lm_head op + its sync. Never the f32 fallback:
8780        // an unquantized lm_head is vocab·hidden and must not be uploaded.
8781        let lm_gw = if upto_excl == self.num_layers
8782            && self.graph_want_logits
8783            && std::env::var("CMF_GPU_LMHEAD")
8784                .map(|v| v != "0")
8785                .unwrap_or(true)
8786        {
8787            self.weights
8788                .lm_head
8789                .graph_weight()
8790                .or_else(|| self.weights.lm_head.graph_weight_descriptor())
8791                .map(|(m, i, kind, rs)| {
8792                let name = &m.tensors[i].name;
8793                let prism = if crate::prism::is_inverse_embedding(m, name) {
8794                    crate::gpu::GraphPrismOp::InverseEmbedding
8795                } else if crate::prism::is_forward_weight(m, name) {
8796                    crate::gpu::GraphPrismOp::Forward
8797                } else {
8798                    crate::gpu::GraphPrismOp::None
8799                };
8800                (
8801                    crate::gpu::GraphW {
8802                        idx: i,
8803                        kind,
8804                        row_scale: rs,
8805                        data: &[],
8806                        prism,
8807                        affine: crate::prism::is_affine_target(m, name),
8808                    },
8809                    self.weights.lm_head.rows(),
8810                )
8811            })
8812        } else {
8813            None
8814        };
8815        let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
8816        // Multi-step re-embeds the winner on the device.
8817        let emb_gw = if steps > 1 {
8818            self.weights
8819                .embed_tokens
8820                .graph_weight()
8821                .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
8822                .map(|(m, i, kind, rs)| {
8823                    let name = &m.tensors[i].name;
8824                    let prism = if crate::prism::is_inverse_embedding(m, name) {
8825                        crate::gpu::GraphPrismOp::InverseEmbedding
8826                    } else if crate::prism::is_forward_weight(m, name) {
8827                        crate::gpu::GraphPrismOp::Forward
8828                    } else {
8829                        crate::gpu::GraphPrismOp::None
8830                    };
8831                    (
8832                        crate::gpu::GraphW {
8833                            idx: i,
8834                            kind,
8835                            row_scale: rs,
8836                            data: &[],
8837                            prism,
8838                            affine: crate::prism::is_affine_target(m, name),
8839                        },
8840                        self.weights.embed_tokens.rows(),
8841                        self.embed_multiplier,
8842                    )
8843                })
8844        } else {
8845            None
8846        };
8847
8848        // Loop boundaries: virtual layer indices after which final_norm is
8849        // applied (mid-stack only; the GLOBAL last layer's norm folds into
8850        // lm_head). Span-relative — the executor compares its enumerate
8851        // index. A span ending mid-stack keeps its boundary norm even when
8852        // it is the span's own last layer.
8853        let loop_norm_at: Vec<usize> = if self.loop_final_norm {
8854            (from..upto_excl.min(self.num_layers - 1))
8855                .filter(|&li| (li + 1) % self.physical_layers == 0)
8856                .map(|li| li - from)
8857                .collect()
8858        } else {
8859            Vec::new()
8860        };
8861        let mut h = hidden.to_vec();
8862        // The normal decode path only needs the fused lm-head logits.  A
8863        // CMF_LOGIT_DUMP diagnostic, however, promises a prompt-boundary
8864        // post-stack hidden alongside those logits; request the existing
8865        // second readback only for that explicit probe instead of dumping
8866        // the input copy left in `h` by a folded-head graph.
8867        let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
8868        let outcome = crate::gpu::forward_token_graph(
8869            &model,
8870            self.graph_kv_id,
8871            &layers,
8872            &o1_views,
8873            self.o1_epoch,
8874            &self.inv_freq,
8875            &mut h,
8876            nh,
8877            nkv,
8878            hd,
8879            self.attn_scale,
8880            rd,
8881            self.hidden_size,
8882            self.intermediate_size,
8883            position,
8884            self.kv_cache.max_seq_len,
8885            gemma,
8886            self.rms_eps as f32,
8887            lm,
8888            &self.weights.final_norm,
8889            logits_out,
8890            &loop_norm_at,
8891            steps,
8892            emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
8893            ids_out,
8894            layers_run,
8895            from,
8896            dump_hidden,
8897        );
8898        match outcome {
8899            crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
8900            crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
8901            crate::gpu::TokenGraphOutcome::Declined => None,
8902        }
8903    }
8904
8905    /// Batched prefill: k contiguous prompt positions through the whole wgpu
8906    /// graph in ONE submit (projections/FFN as GEMMs). `hiddens` is [k·hidden]
8907    /// in/out (embeddings in, layer output out); KV mirror / GDN state advance.
8908    /// false ⇒ unsupported → caller keeps the per-position graph.
8909    /// The b-row Metal graph plan for the whole model: every layer as a
8910    /// GDN run or a full-attention item, all-or-nothing (a layer outside the
8911    /// graph's contract → None, the caller runs plain). Shared by the
8912    /// speculative verify and the batched prefill.
8913    #[cfg(target_os = "macos")]
8914    #[allow(clippy::type_complexity)]
8915    fn metal_rows_plan(
8916        &self,
8917    ) -> Option<(
8918        Vec<MetalRowsItem<'_>>,
8919        std::sync::Arc<cortiq_core::CmfModel>,
8920        Option<crate::gpu_metal::GdnGpuCfg>,
8921    )> {
8922        use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
8923        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
8924        if !graph_force
8925            || !crate::gpu::enabled_here()
8926            || std::env::var("CMF_GPU_BLOCK")
8927                .map(|v| v == "0")
8928                .unwrap_or(false)
8929            || self.attn_softcap > 0.0
8930            || self.o1_active()
8931            || self.swa.is_some()
8932            || self.global_attn.is_some()
8933            || self.attention_heads_per_layer.is_some()
8934            || self.attn_v_norm
8935            || self.loop_final_norm
8936        {
8937            return None;
8938        }
8939        let attend_contract = self.head_dim % 4 == 0
8940            && self.head_dim <= 256
8941            && self.rotary_dim >= 2
8942            && self.rotary_dim <= self.head_dim
8943            && (self.rotary_dim / 2) % 32 == 0
8944            && self.num_kv_heads > 0
8945            && self.num_heads % self.num_kv_heads == 0;
8946        if !attend_contract {
8947            return None;
8948        }
8949        let mut plan: Vec<MetalRowsItem> = Vec::new();
8950        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
8951        for li in 0..self.num_layers {
8952            let lw = &self.weights.layers[self.phys_layer(li)];
8953            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
8954                return None;
8955            }
8956            let ffn = match &lw.ffn {
8957                FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
8958                    let (Some(g), Some(u), Some(dn)) = (
8959                        d.gate_proj.metal_graph_parts(),
8960                        d.up_proj.metal_graph_parts(),
8961                        d.down_proj.metal_graph_parts(),
8962                    ) else {
8963                        return None;
8964                    };
8965                    MetalFfn::Dense {
8966                        gate: g,
8967                        up: u,
8968                        down: dn,
8969                    }
8970                }
8971                _ => return None,
8972            };
8973            match &lw.attn {
8974                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
8975                    let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
8976                        w.in_proj_qkv.metal_graph_parts(),
8977                        w.in_proj_z.metal_graph_parts(),
8978                        w.in_proj_a.f32_parts(),
8979                        w.in_proj_b.f32_parts(),
8980                        w.out_proj.metal_graph_parts(),
8981                    ) else {
8982                        return None;
8983                    };
8984                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
8985                        model_ref.get_or_insert_with(|| model.clone());
8986                    }
8987                    let gl = GdnGpuLayer {
8988                        attn_norm: &lw.input_norm,
8989                        post_norm: &lw.post_norm,
8990                        qkv,
8991                        z,
8992                        a,
8993                        b: bb,
8994                        out,
8995                        ffn,
8996                        conv1d: &w.conv1d,
8997                        a_log: &w.a_log,
8998                        dt_bias: &w.dt_bias,
8999                        gnorm: &w.norm,
9000                    };
9001                    match plan.last_mut() {
9002                        Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
9003                        _ => plan.push(MetalRowsItem::Gdn {
9004                            run: vec![gl],
9005                            first: li,
9006                        }),
9007                    }
9008                }
9009                AttnKind::Full {
9010                    wq,
9011                    wk,
9012                    wv,
9013                    wo,
9014                    q_norm,
9015                    k_norm,
9016                    output_gate,
9017                    softplus_gate: None,
9018                    bias: None,
9019                } => {
9020                    let (Some(pq), Some(pk), Some(pv), Some(po)) =
9021                        (
9022                            wq.metal_graph_parts(),
9023                            wk.metal_graph_parts(),
9024                            wv.metal_graph_parts(),
9025                            wo.metal_graph_parts(),
9026                        )
9027                    else {
9028                        return None;
9029                    };
9030                    if let QTensor::Mapped { model, .. } = wq {
9031                        model_ref.get_or_insert_with(|| model.clone());
9032                    }
9033                    let cache = &self.kv_cache.layers[li];
9034                    if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
9035                        return None;
9036                    }
9037                    plan.push(MetalRowsItem::Attn {
9038                        l: AttnGpuLayer {
9039                            attn_norm: &lw.input_norm,
9040                            post_norm: &lw.post_norm,
9041                            wq: pq,
9042                            wk: pk,
9043                            wv: pv,
9044                            wo: po,
9045                            ffn,
9046                        },
9047                        li,
9048                        q_norm: q_norm.as_deref(),
9049                        k_norm: k_norm.as_deref(),
9050                        output_gate: *output_gate,
9051                    });
9052                }
9053                _ => return None,
9054            }
9055        }
9056        let model = model_ref?;
9057        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
9058            nv: cfg.num_v_heads,
9059            nk: cfg.num_k_heads,
9060            dk: cfg.key_head_dim,
9061            dv: cfg.value_head_dim,
9062            kk: cfg.conv_kernel,
9063            hidden: self.hidden_size,
9064            inter: self.intermediate_size,
9065            c_dim: cfg.conv_dim(),
9066            eps: cfg.rms_eps as f32,
9067            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9068        });
9069        Some((plan, model, gcfg))
9070    }
9071
9072    /// `AttnDeviceParams` for a plan item over the CPU cache as it stands.
9073    #[cfg(target_os = "macos")]
9074    #[allow(clippy::too_many_arguments)]
9075    fn metal_attn_params<'a>(
9076        li: usize,
9077        cache: &'a crate::kv_cache::LayerKvCache,
9078        q_norm: Option<&'a [f32]>,
9079        k_norm: Option<&'a [f32]>,
9080        output_gate: bool,
9081        inv_freq: &'a [f32],
9082        geom: (usize, usize, usize, usize),
9083        pos0: usize,
9084        kv_id: u64,
9085        scale: f32,
9086        eps: f32,
9087        gemma: bool,
9088        late_qk_norm: bool,
9089    ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
9090        let (nh, nkv, hd, rd) = geom;
9091        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9092        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9093        let cpu_stored = cpu_k[0].len() / hd;
9094        (
9095            crate::gpu_metal::AttnDeviceParams {
9096                kv_id,
9097                layer: li,
9098                nh,
9099                nkv,
9100                hd,
9101                rd,
9102                position: pos0,
9103                scale,
9104                eps,
9105                gemma,
9106                late_qk_norm,
9107                output_gate,
9108                q_norm,
9109                k_norm,
9110                inv_freq,
9111                cpu_k,
9112                cpu_v,
9113                cpu_stored,
9114                o1: None,
9115            },
9116            cpu_stored,
9117        )
9118    }
9119
9120    /// Run the rows plan over `hiddens` (b rows at `pos0..`): validate,
9121    /// encode every item, optionally the head, sync. Returns the graph
9122    /// (for the commit / state finish) plus the GDN layer indices and the
9123    /// attention layers with the row count they were encoded against.
9124    #[cfg(target_os = "macos")]
9125    #[allow(clippy::type_complexity)]
9126    fn metal_rows_run(
9127        &mut self,
9128        hiddens: &mut [f32],
9129        pos0: usize,
9130        b: usize,
9131        prefill: bool,
9132        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
9133        // Greedy verify: (row length scored, the b argmax ids out) — the
9134        // head's argmax runs on the device and the logits plane is NOT
9135        // read back (`spec.2` stays empty).
9136        mut argmax_out: Option<(usize, &mut Vec<u32>)>,
9137    ) -> MetalRowsRun {
9138        use crate::gpu_metal::{GraphDims, VerifyGraph};
9139        // The previous round's commit may still be replaying into the
9140        // trunk GDN owners on the second queue: this graph reads them
9141        // (zero-copy wraps) and may reallocate them below — collect the
9142        // replay first. Normally already complete (the draft chain ran
9143        // in between); a failed replay is terminal like a failed commit.
9144        if !crate::gpu_metal::wait_replay() {
9145            tracing::error!("Metal rows graph: the pending async replay failed");
9146            return MetalRowsRun::Failed;
9147        }
9148        spec_stamp("v.wait");
9149        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
9150        for l in &mut self.kv_cache.layers {
9151            if l.linear_state.len() != want && want > 0 {
9152                l.linear_state = vec![0f32; want];
9153            }
9154        }
9155        let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
9156            return MetalRowsRun::Declined;
9157        };
9158        spec_stamp("v.plan");
9159        let dims = GraphDims {
9160            hidden: self.hidden_size,
9161            eps: self.rms_eps as f32,
9162            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9163        };
9164        let Some(mut graph) = (if prefill {
9165            VerifyGraph::new_prefill(&model, dims, hiddens, b)
9166        } else {
9167            VerifyGraph::new(&model, dims, hiddens, b)
9168        }) else {
9169            return MetalRowsRun::Declined;
9170        };
9171        let geom = (
9172            self.num_heads,
9173            self.num_kv_heads,
9174            self.head_dim,
9175            self.rotary_dim,
9176        );
9177        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9178        let eps = self.rms_eps as f32;
9179        let kv_id = self.graph_kv_id;
9180        let inv_freq = self.inv_freq.clone();
9181        for item in &plan {
9182            let ok = match item {
9183                MetalRowsItem::Gdn { run, .. } => gcfg
9184                    .as_ref()
9185                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
9186                    .unwrap_or(false),
9187                MetalRowsItem::Attn {
9188                    l,
9189                    li,
9190                    q_norm,
9191                    k_norm,
9192                    output_gate,
9193                } => {
9194                    let (p, _) = Self::metal_attn_params(
9195                        *li,
9196                        &self.kv_cache.layers[*li],
9197                        *q_norm,
9198                        *k_norm,
9199                        *output_gate,
9200                        &inv_freq,
9201                        geom,
9202                        pos0,
9203                        kv_id,
9204                        self.attn_scale,
9205                        eps,
9206                        gemma,
9207                        self.qk_norm_after_rope,
9208                    );
9209                    graph.attn_ok(l, &p)
9210                }
9211            };
9212            if !ok {
9213                use std::sync::atomic::{AtomicBool, Ordering};
9214                static SAID: AtomicBool = AtomicBool::new(false);
9215                if !SAID.swap(true, Ordering::Relaxed) {
9216                    tracing::warn!("metal rows graph: a layer failed preflight — declining");
9217                }
9218                return MetalRowsRun::Declined;
9219            }
9220        }
9221        let lm = match &spec {
9222            Some((lm, _, _)) => {
9223                if !graph.lm_head_ok(*lm) {
9224                    return MetalRowsRun::Declined;
9225                }
9226                Some(*lm)
9227            }
9228            None => None,
9229        };
9230        let mut gdn_layers = Vec::new();
9231        let mut attn_layers = Vec::new();
9232        for item in &plan {
9233            match item {
9234                MetalRowsItem::Gdn { run, first } => {
9235                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
9236                        .iter()
9237                        .map(|l| l.linear_state.as_slice())
9238                        .collect();
9239                    if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
9240                        return MetalRowsRun::Declined;
9241                    }
9242                    gdn_layers.extend(*first..*first + run.len());
9243                }
9244                MetalRowsItem::Attn {
9245                    l,
9246                    li,
9247                    q_norm,
9248                    k_norm,
9249                    output_gate,
9250                } => {
9251                    let (p, cpu_stored) = Self::metal_attn_params(
9252                        *li,
9253                        &self.kv_cache.layers[*li],
9254                        *q_norm,
9255                        *k_norm,
9256                        *output_gate,
9257                        &inv_freq,
9258                        geom,
9259                        pos0,
9260                        kv_id,
9261                        self.attn_scale,
9262                        eps,
9263                        gemma,
9264                        self.qk_norm_after_rope,
9265                    );
9266                    if !graph.encode_attn_b(l, &p) {
9267                        return MetalRowsRun::Declined;
9268                    }
9269                    attn_layers.push((*li, cpu_stored));
9270                }
9271            }
9272        }
9273        if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
9274            if !graph.encode_lm_head_b(final_norm, lm) {
9275                return MetalRowsRun::Declined;
9276            }
9277            // The device argmax is an OPTIMISATION, never a reason to
9278            // decline the round: if it will not encode, drop it and read
9279            // the logits plane back the old way (the head is encoded
9280            // either way, so the rows are there).
9281            if let Some((n, _)) = argmax_out.as_ref() {
9282                if !graph.encode_argmax_b(*n) {
9283                    argmax_out = None;
9284                }
9285            }
9286        }
9287        spec_stamp("v.enc");
9288        if !graph.sync() {
9289            return MetalRowsRun::Failed;
9290        }
9291        spec_stamp("v.gpu");
9292        match (spec, argmax_out) {
9293            (Some(_), Some((_, ids))) => {
9294                ids.resize(b, 0);
9295                if !graph.read_argmax(ids) {
9296                    return MetalRowsRun::Failed;
9297                }
9298                spec_stamp("v.am");
9299            }
9300            (Some((lm, _, logits)), None) => {
9301                logits.resize(b * lm.1, 0.0);
9302                if !graph.read_logits(logits) {
9303                    return MetalRowsRun::Failed;
9304                }
9305                spec_stamp("v.lg");
9306            }
9307            (None, _) => {}
9308        }
9309        if !graph.read_hidden(hiddens) {
9310            return MetalRowsRun::Failed;
9311        }
9312        spec_stamp("v.hid");
9313        MetalRowsRun::Completed(MetalVerifyPending {
9314            graph,
9315            gdn_layers,
9316            attn_layers,
9317        })
9318    }
9319
9320    /// Native-Metal twin of `try_batch_graph_wgpu`: the b rows through the
9321    /// whole model on the `VerifyGraph` (one submit), the head folded in
9322    /// when `spec` asks; `hiddens` come back as the last layer's output
9323    /// rows, `spec.2` as `[b][lm_rows]` logits. The graph is parked in
9324    /// `metal_verify` for `metal_verify_commit`.
9325    #[cfg(target_os = "macos")]
9326    fn try_batch_graph_metal(
9327        &mut self,
9328        hiddens: &mut [f32],
9329        positions: &[usize],
9330        b: usize,
9331        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
9332        argmax_out: Option<(usize, &mut Vec<u32>)>,
9333    ) -> crate::gpu::BatchGraphOutcome {
9334        let _t0 = std::time::Instant::now();
9335        if positions.len() != b
9336            || positions.windows(2).any(|w| w[1] != w[0] + 1)
9337            || hiddens.len() != b * self.hidden_size
9338        {
9339            return crate::gpu::BatchGraphOutcome::Declined;
9340        }
9341        let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
9342            MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
9343            MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
9344            MetalRowsRun::Completed(pending) => pending,
9345        };
9346        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
9347            eprintln!(
9348                "metal-verify: {:.1} ms | b={b}",
9349                _t0.elapsed().as_secs_f64() * 1e3
9350            );
9351        }
9352        self.metal_verify = Some(pending);
9353        crate::gpu::BatchGraphOutcome::Completed
9354    }
9355
9356    /// Batched prefill on the Metal rows graph: `ids` (≤ 512) at
9357    /// `start_pos..`, states written in place, K/V rows appended to the
9358    /// CPU caches; optional final norm/head logits are returned in `spec`.
9359    /// Declined means no command buffer was admitted; Failed is terminal.
9360    #[cfg(target_os = "macos")]
9361    fn prefill_rows_metal(
9362        &mut self,
9363        ids: &[u32],
9364        start_pos: usize,
9365        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
9366    ) -> MetalPrefillOutcome {
9367        let b = ids.len();
9368        if b == 0 || b > 512 {
9369            return MetalPrefillOutcome::Declined;
9370        }
9371        METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9372        let with_head = spec.is_some();
9373        let hs = self.hidden_size;
9374        let mut hiddens = vec![0f32; b * hs];
9375        for (j, &id) in ids.iter().enumerate() {
9376            let e = self.embed_single(id);
9377            hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
9378        }
9379        let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
9380            MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
9381            MetalRowsRun::Failed => {
9382                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9383                return MetalPrefillOutcome::Failed;
9384            }
9385            MetalRowsRun::Completed(pending) => pending,
9386        };
9387        // states are final: copy them to the owners
9388        let idxs = pending.gdn_layers.clone();
9389        let mut outs: Vec<&mut [f32]> = self
9390            .kv_cache
9391            .layers
9392            .iter_mut()
9393            .enumerate()
9394            .filter(|(i, _)| idxs.binary_search(i).is_ok())
9395            .map(|(_, l)| l.linear_state.as_mut_slice())
9396            .collect();
9397        if !pending.graph.finish_states(&mut outs) {
9398            METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9399            return MetalPrefillOutcome::Failed;
9400        }
9401        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
9402        // Read every layer before mutating any CPU cache.  A missing mirror
9403        // row is a terminal graph failure, not a reason to append a partial
9404        // prefix and replay the remainder serially.
9405        let mut rows = Vec::with_capacity(pending.attn_layers.len());
9406        for (li, cpu_stored) in &pending.attn_layers {
9407            let mut kbuf = vec![0f32; b * nkv * hd];
9408            let mut vbuf = vec![0f32; b * nkv * hd];
9409            if !crate::gpu_metal::kv_mirror_read_rows(
9410                self.graph_kv_id,
9411                *li,
9412                nkv,
9413                hd,
9414                *cpu_stored,
9415                b,
9416                &mut kbuf,
9417                &mut vbuf,
9418            ) {
9419                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9420                return MetalPrefillOutcome::Failed;
9421            }
9422            rows.push((*li, *cpu_stored, kbuf, vbuf));
9423        }
9424        for (li, cpu_stored, kbuf, vbuf) in rows {
9425            let cache = &mut self.kv_cache.layers[li];
9426            for r in 0..b {
9427                cache.append(
9428                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9429                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9430                    &[],
9431                );
9432            }
9433            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
9434        }
9435        METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
9436        if with_head {
9437            METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
9438        }
9439        MetalPrefillOutcome::Completed(hiddens)
9440    }
9441
9442    #[cfg(target_os = "macos")]
9443    fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
9444        self.prefill_rows_metal(ids, start_pos, None)
9445    }
9446
9447    /// Exact teacher-forced NLL through the ordinary Metal rows graph.  This
9448    /// is intentionally separate from the serial TokenGraph scorer: every
9449    /// chunk owns a real b-row graph/head completion and the recurrent/KV
9450    /// handoff is committed before the next chunk begins.
9451    #[cfg(target_os = "macos")]
9452    fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
9453        if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
9454            return MetalBatchNllOutcome::Declined;
9455        }
9456        let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
9457            return MetalBatchNllOutcome::Declined;
9458        };
9459        let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
9460            .ok()
9461            .and_then(|v| v.parse::<usize>().ok())
9462            .filter(|&v| (1..=512).contains(&v))
9463            .unwrap_or(32);
9464        let final_norm = self.weights.final_norm.clone();
9465        let mut nll = 0.0f64;
9466        let mut count = 0usize;
9467        let mut pos = 0usize;
9468        let mut completed = 0usize;
9469        while pos < ids.len() {
9470            let end = (pos + chunk).min(ids.len());
9471            let mut logits = Vec::new();
9472            let outcome = self.prefill_rows_metal(
9473                &ids[pos..end],
9474                pos,
9475                Some((lm, &final_norm, &mut logits)),
9476            );
9477            match outcome {
9478                MetalPrefillOutcome::Declined => {
9479                    return if completed == 0 {
9480                        MetalBatchNllOutcome::Declined
9481                    } else {
9482                        MetalBatchNllOutcome::Failed(format!(
9483                            "ordinary Metal NLL batch declined after {completed} chunks"
9484                        ))
9485                    };
9486                }
9487                MetalPrefillOutcome::Failed => {
9488                    return MetalBatchNllOutcome::Failed(
9489                        "ordinary Metal NLL batch failed after admission".to_string(),
9490                    );
9491                }
9492                MetalPrefillOutcome::Completed(_) => {}
9493            }
9494            completed += 1;
9495            let vocab = self.vocab_size.min(lm.1);
9496            if logits.len() != (end - pos) * lm.1 || vocab == 0 {
9497                return MetalBatchNllOutcome::Failed(
9498                    "ordinary Metal NLL head returned an invalid shape".to_string(),
9499                );
9500            }
9501            for row in 0..(end - pos) {
9502                let absolute = pos + row;
9503                if absolute < start || absolute + 1 >= ids.len() {
9504                    continue;
9505                }
9506                let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
9507                if let Some(mu) = self.logit_multiplier {
9508                    for v in lg.iter_mut() {
9509                        *v *= mu;
9510                    }
9511                }
9512                if let Some(c) = self.final_softcap {
9513                    for v in lg.iter_mut() {
9514                        *v = c * (*v / c).tanh();
9515                    }
9516                }
9517                let target = ids[absolute + 1] as usize;
9518                if target >= vocab {
9519                    return MetalBatchNllOutcome::Failed(format!(
9520                        "target token {target} exceeds Metal head rows {vocab}"
9521                    ));
9522                }
9523                let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9524                let lse: f64 = lg
9525                    .iter()
9526                    .map(|&v| ((v - max) as f64).exp())
9527                    .sum::<f64>()
9528                    .ln()
9529                    + max as f64;
9530                nll += lse - lg[target] as f64;
9531                count += 1;
9532            }
9533            pos = end;
9534        }
9535        MetalBatchNllOutcome::Completed(nll, count)
9536    }
9537
9538    /// Commit a Metal verify round: replay the GDN recurrences over the
9539    /// `a + 1` accepted positions into the CPU states, append the accepted
9540    /// K/V rows from the mirrors to the CPU caches, re-point the mirrors.
9541    #[cfg(target_os = "macos")]
9542    fn metal_verify_commit(&mut self, a: usize) -> bool {
9543        let Some(mut pending) = self.metal_verify.take() else {
9544            return false;
9545        };
9546        let n = a + 1;
9547        // encode order == ascending layer order (the plan walks 0..layers)
9548        let idxs = pending.gdn_layers.clone();
9549        let mut outs: Vec<&mut [f32]> = self
9550            .kv_cache
9551            .layers
9552            .iter_mut()
9553            .enumerate()
9554            .filter(|(i, _)| idxs.binary_search(i).is_ok())
9555            .map(|(_, l)| l.linear_state.as_mut_slice())
9556            .collect();
9557        if !pending.graph.commit(n, &mut outs) {
9558            return false;
9559        }
9560        spec_stamp("c.replay");
9561        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
9562        // Read every layer before mutating any CPU cache.  Missing rows are
9563        // terminal after the replay has executed; never append a partial KV
9564        // prefix and continue on a serial path.
9565        let mut rows = Vec::with_capacity(pending.attn_layers.len());
9566        for (li, cpu_stored) in &pending.attn_layers {
9567            let mut kbuf = vec![0f32; n * nkv * hd];
9568            let mut vbuf = vec![0f32; n * nkv * hd];
9569            if !crate::gpu_metal::kv_mirror_read_rows(
9570                self.graph_kv_id,
9571                *li,
9572                nkv,
9573                hd,
9574                *cpu_stored,
9575                n,
9576                &mut kbuf,
9577                &mut vbuf,
9578            ) {
9579                return false;
9580            }
9581            rows.push((*li, *cpu_stored, kbuf, vbuf));
9582        }
9583        for (li, cpu_stored, kbuf, vbuf) in rows {
9584            let cache = &mut self.kv_cache.layers[li];
9585            for r in 0..n {
9586                cache.append(
9587                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9588                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9589                    &[],
9590                );
9591            }
9592            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
9593        }
9594        spec_stamp("c.kv");
9595        true
9596    }
9597
9598    /// The round's warm-ups as ONE b-row graph run over the MTP block on
9599    /// Metal: `pairs` = (trunk hidden, next token) at consecutive positions
9600    /// from `first_pos`; the block's input projection is folded in. This
9601    /// half encodes and SUBMITS (no wait); `mtp_warm_batch_finish` waits
9602    /// and pulls the appended K/V rows into the CPU MTP cache. None = the
9603    /// graph declined (nothing submitted, nothing appended).
9604    #[cfg(target_os = "macos")]
9605    fn mtp_warm_batch_submit(
9606        &mut self,
9607        m: &mut MtpModule,
9608        pairs: &[(&[f32], u32)],
9609        first_pos: usize,
9610    ) -> Option<MetalWarmPending> {
9611        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
9612        let b = pairs.len();
9613        if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
9614            return None;
9615        }
9616        let AttnKind::Full {
9617            wq,
9618            wk,
9619            wv,
9620            wo,
9621            q_norm,
9622            k_norm,
9623            output_gate,
9624            softplus_gate: None,
9625            bias: None,
9626        } = &m.layer.attn
9627        else {
9628            return None;
9629        };
9630        let FfnKind::Dense(d) = &m.layer.ffn else {
9631            return None;
9632        };
9633        if !d.segs.is_empty() {
9634            return None;
9635        }
9636        let (Some(pq), Some(pk), Some(pv), Some(po)) =
9637            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
9638        else {
9639            return None;
9640        };
9641        let (Some(g), Some(u), Some(dn)) = (
9642            d.gate_proj.q1_parts(),
9643            d.up_proj.q1_parts(),
9644            d.down_proj.q1_parts(),
9645        ) else {
9646            return None;
9647        };
9648        let Some(eh) = m.eh_proj.q1_parts() else {
9649            return None;
9650        };
9651        let QTensor::Mapped { model, .. } = wq else {
9652            return None;
9653        };
9654        let model = model.clone();
9655        let hs = self.hidden_size;
9656        // [enorm(embed(tok)); hnorm(hidden)] rows
9657        let mut cat = vec![0f32; b * 2 * hs];
9658        for (j, (h, tok)) in pairs.iter().enumerate() {
9659            let e = self.embed_single(*tok);
9660            let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
9661            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
9662            inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
9663        }
9664        let dims = GraphDims {
9665            hidden: hs,
9666            eps: self.rms_eps as f32,
9667            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9668        };
9669        spec_stamp("w.cat");
9670        let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
9671            return None;
9672        };
9673        spec_stamp("w.new");
9674        let l = AttnGpuLayer {
9675            attn_norm: &m.layer.input_norm,
9676            post_norm: &m.layer.post_norm,
9677            wq: pq,
9678            wk: pk,
9679            wv: pv,
9680            wo: po,
9681            ffn: MetalFfn::Dense {
9682                gate: g,
9683                up: u,
9684                down: dn,
9685            },
9686        };
9687        let (nh, nkv, hd, rd) = (
9688            self.num_heads,
9689            self.num_kv_heads,
9690            self.head_dim,
9691            self.rotary_dim,
9692        );
9693        let inv_freq = self.inv_freq.clone();
9694        let cpu_stored;
9695        {
9696            let cache = &m.kv;
9697            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9698            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9699            cpu_stored = cpu_k[0].len() / hd;
9700            // The cache may LAG the position (rows nobody warmed): the
9701            // pairs land at cpu_stored.. with their true RoPE positions
9702            // first_pos.., exactly what the one-by-one warm does. A cache
9703            // AHEAD of the position is a real inconsistency.
9704            if cpu_stored > first_pos {
9705                spec_stamp("w.decl");
9706                return None;
9707            }
9708            let p = AttnDeviceParams {
9709                kv_id: self.mtp_kv_id(),
9710                layer: Self::MTP_LAYER_BASE,
9711                nh,
9712                nkv,
9713                hd,
9714                rd,
9715                position: first_pos,
9716                scale: self.attn_scale,
9717                eps: self.rms_eps as f32,
9718                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9719                late_qk_norm: self.qk_norm_after_rope,
9720                output_gate: *output_gate,
9721                q_norm: q_norm.as_deref(),
9722                k_norm: k_norm.as_deref(),
9723                inv_freq: &inv_freq,
9724                cpu_k,
9725                cpu_v,
9726                cpu_stored,
9727                o1: None,
9728            };
9729            if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
9730                return None;
9731            }
9732        }
9733        spec_stamp("w.enc");
9734        if !graph.submit() {
9735            return None;
9736        }
9737        spec_stamp("w.sub");
9738        Some(MetalWarmPending {
9739            graph,
9740            cpu_stored,
9741            b,
9742        })
9743    }
9744
9745    /// Submit and finish in one call (the prefill's MTP warm-up, where
9746    /// nothing runs in between).
9747    #[cfg(target_os = "macos")]
9748    fn mtp_warm_batch_metal(
9749        &mut self,
9750        m: &mut MtpModule,
9751        pairs: &[(&[f32], u32)],
9752        first_pos: usize,
9753    ) -> bool {
9754        match self.mtp_warm_batch_submit(m, pairs, first_pos) {
9755            Some(p) => self.mtp_warm_batch_finish(m, p),
9756            None => false,
9757        }
9758    }
9759
9760    /// Second half of the batched warm-up: wait for the submitted graph,
9761    /// pull its b appended K/V rows into the CPU MTP cache, re-point the
9762    /// mirror. False = the command buffer failed or the rows are missing
9763    /// (nothing appended; the caller falls back to the one-by-one warm).
9764    #[cfg(target_os = "macos")]
9765    fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
9766        let MetalWarmPending {
9767            mut graph,
9768            cpu_stored,
9769            b,
9770        } = pending;
9771        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
9772        if !graph.sync() {
9773            return false;
9774        }
9775        spec_stamp("w.gpu");
9776        let mut kbuf = vec![0f32; b * nkv * hd];
9777        let mut vbuf = vec![0f32; b * nkv * hd];
9778        if !crate::gpu_metal::kv_mirror_read_rows(
9779            self.mtp_kv_id(),
9780            Self::MTP_LAYER_BASE,
9781            nkv,
9782            hd,
9783            cpu_stored,
9784            b,
9785            &mut kbuf,
9786            &mut vbuf,
9787        ) {
9788            return false;
9789        }
9790        for r in 0..b {
9791            m.kv.append(
9792                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9793                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9794                &[],
9795            );
9796        }
9797        crate::gpu_metal::kv_mirror_set_stored(
9798            self.mtp_kv_id(),
9799            Self::MTP_LAYER_BASE,
9800            cpu_stored + b,
9801        );
9802        spec_stamp("w.kv");
9803        true
9804    }
9805
9806    /// A committed token id from the high table (Cyrillic, CJK and the
9807    /// like sit above 131072 in Qwen's vocabulary; Latin subwords past
9808    /// the 65536 cut are rare enough to lose as rejected drafts) switches
9809    /// the draft to the full head for the next 16 tokens; other ids count
9810    /// down. On an M4 the full 660 MB head costs 5.5 ms a draft step
9811    /// against 1.4 for the shortlist, so the streak is kept short.
9812    pub(crate) fn note_draft_id(&mut self, id: u32) {
9813        let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
9814        if (id as usize) >= cut {
9815            self.draft_full_streak = 16;
9816        } else {
9817            self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
9818        }
9819    }
9820
9821    /// The draft head's rows for the next step: the shortlist, or the full
9822    /// head while `draft_full_streak` runs.
9823    fn draft_head_rows(&self, head_rows: usize) -> usize {
9824        if self.draft_full_streak > 0 {
9825            head_rows
9826        } else {
9827            Self::draft_vocab_rows(head_rows)
9828        }
9829    }
9830
9831    /// Draft-head shortlist size: `CMF_DRAFT_VOCAB` rows (default 65536,
9832    /// capped at the head; 0 = full head).
9833    fn draft_vocab_rows(head_rows: usize) -> usize {
9834        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9835        let n = *N.get_or_init(|| {
9836            std::env::var("CMF_DRAFT_VOCAB")
9837                .ok()
9838                .and_then(|v| v.parse().ok())
9839                .unwrap_or(65536)
9840        });
9841        if n == 0 { head_rows } else { n.min(head_rows) }
9842    }
9843
9844    /// One MTP block step on the native Metal token graph: block input on
9845    /// the host, the attention layer + FFN device-resident over the MTP
9846    /// mirror, the head folded in when `want_logits`. The appended K/V row
9847    /// is pulled into the CPU MTP cache (owner of record) after the sync.
9848    #[cfg(target_os = "macos")]
9849    fn mtp_step_metal(
9850        &mut self,
9851        m: &mut MtpModule,
9852        hidden: &[f32],
9853        next_token: u32,
9854        position: usize,
9855        want_logits: bool,
9856    ) -> Option<(Vec<f32>, Vec<f32>)> {
9857        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
9858        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9859            || !crate::gpu::q1_force()
9860            || !crate::gpu::enabled_here()
9861            || self.attn_softcap > 0.0
9862            || self.attention_heads_per_layer.is_some()
9863            || m.kv.mode != crate::kv_cache::KvMode::F32
9864            || m.kv.o1.is_some()
9865        {
9866            return None;
9867        }
9868        let AttnKind::Full {
9869            wq,
9870            wk,
9871            wv,
9872            wo,
9873            q_norm,
9874            k_norm,
9875            output_gate,
9876            softplus_gate: None,
9877            bias: None,
9878        } = &m.layer.attn
9879        else {
9880            return None;
9881        };
9882        let FfnKind::Dense(d) = &m.layer.ffn else {
9883            return None;
9884        };
9885        if d.act != Act::Silu || !d.segs.is_empty() {
9886            return None;
9887        }
9888        let (pq, pk, pv, po) = (
9889            wq.q1_parts()?,
9890            wk.q1_parts()?,
9891            wv.q1_parts()?,
9892            wo.q1_parts()?,
9893        );
9894        let (g, u, dn) = (
9895            d.gate_proj.q1_parts()?,
9896            d.up_proj.q1_parts()?,
9897            d.down_proj.q1_parts()?,
9898        );
9899        let QTensor::Mapped { model, .. } = wq else {
9900            return None;
9901        };
9902        let model = model.clone();
9903        let lm = if want_logits {
9904            Some(self.weights.lm_head.q1_parts()?)
9905        } else {
9906            None
9907        };
9908        let dims = GraphDims {
9909            hidden: self.hidden_size,
9910            eps: self.rms_eps as f32,
9911            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9912        };
9913        // The block input `eh_proj · [enorm(e); hnorm(h)]` rides in the
9914        // graph (one submit a step); the host per-op matvec if it cannot.
9915        let hs = self.hidden_size;
9916        let mut x = vec![0f32; hs];
9917        let mut graph = TokenGraph::new(&model, dims, &x)?;
9918        let mut folded = false;
9919        if let Some(eh) = m.eh_proj.q1_parts() {
9920            let e = self.embed_single(next_token);
9921            let mut cat = vec![0.0f32; 2 * hs];
9922            let (cat_e, cat_h) = cat.split_at_mut(hs);
9923            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9924            inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9925            folded = graph.encode_input_proj(eh, &cat);
9926        }
9927        if !folded {
9928            x = self.mtp_block_input(m, hidden, next_token);
9929            graph = TokenGraph::new(&model, dims, &x)?;
9930        }
9931        spec_stamp("d.in");
9932        let l = AttnGpuLayer {
9933            attn_norm: &m.layer.input_norm,
9934            post_norm: &m.layer.post_norm,
9935            wq: pq,
9936            wk: pk,
9937            wv: pv,
9938            wo: po,
9939            ffn: MetalFfn::Dense {
9940                gate: g,
9941                up: u,
9942                down: dn,
9943            },
9944        };
9945        let (nh, nkv, hd, rd) = (
9946            self.num_heads,
9947            self.num_kv_heads,
9948            self.head_dim,
9949            self.rotary_dim,
9950        );
9951        let inv_freq = self.inv_freq.clone();
9952        {
9953            let cache = &m.kv;
9954            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9955            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9956            let cpu_stored = cpu_k[0].len() / hd;
9957            let p = AttnDeviceParams {
9958                kv_id: self.mtp_kv_id(),
9959                layer: Self::MTP_LAYER_BASE,
9960                nh,
9961                nkv,
9962                hd,
9963                rd,
9964                position,
9965                scale: self.attn_scale,
9966                eps: self.rms_eps as f32,
9967                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9968                late_qk_norm: self.qk_norm_after_rope,
9969                output_gate: *output_gate,
9970                q_norm: q_norm.as_deref(),
9971                k_norm: k_norm.as_deref(),
9972                inv_freq: &inv_freq,
9973                cpu_k,
9974                cpu_v,
9975                cpu_stored,
9976                o1: None,
9977            };
9978            if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
9979                return None;
9980            }
9981        }
9982        // The draft's head over a vocabulary SHORTLIST (the first
9983        // CMF_DRAFT_VOCAB rows — BPE ids run roughly by merge rank, so the
9984        // low ids carry the mass): the verify keeps the full head, so a true
9985        // token past the cut is only a rejected draft, never a wrong token.
9986        // 662 MB a step on Qwen3.8 becomes 170 MB at 65536.
9987        let draft_rows = if let Some(lm) = lm {
9988            self.draft_head_rows(lm.1)
9989        } else {
9990            0
9991        };
9992        if let Some(lm) = lm {
9993            if !graph.lm_head_ok(lm) {
9994                return None;
9995            }
9996            if draft_rows < lm.1 {
9997                if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
9998                    return None;
9999                }
10000            } else {
10001                graph.encode_lm_head(&m.final_norm, lm);
10002            }
10003        }
10004        spec_stamp("d.enc");
10005        if graph.sync_checked().is_err() {
10006            return None;
10007        }
10008        spec_stamp("d.gpu");
10009        let mut logits = Vec::new();
10010        if let Some(lm) = lm {
10011            let n_read = draft_rows.min(lm.1).min(self.vocab_size);
10012            logits = attention::take_buf(n_read);
10013            graph.read_logits(&mut logits);
10014            // ids past the shortlist: never drafted (−∞ in every chain)
10015            logits.resize(self.vocab_size, f32::NEG_INFINITY);
10016        }
10017        graph.finish(&mut x);
10018        let mut krow = attention::take_buf(nkv * hd);
10019        let mut vrow = attention::take_buf(nkv * hd);
10020        if crate::gpu_metal::kv_mirror_read_last(
10021            self.mtp_kv_id(),
10022            Self::MTP_LAYER_BASE,
10023            nkv,
10024            hd,
10025            &mut krow,
10026            &mut vrow,
10027        ) {
10028            m.kv.append(&krow, &vrow, &[]);
10029        }
10030        attention::recycle_buf(&mut krow);
10031        attention::recycle_buf(&mut vrow);
10032        spec_stamp("d.rd");
10033        Some((logits, x))
10034    }
10035
10036    /// `CMF_MTP_CHAIN=0` keeps the per-step draft (one submit and one
10037    /// host round trip per MTP step); the default drafts the whole chain
10038    /// in one command buffer when the round is plain greedy.
10039    ///
10040    /// Measured on an M4 (24 GB), Qwen3.8-27B q4tp, P3 at 160 tokens,
10041    /// k=7, six runs per arm alternating inside one lock window — the
10042    /// round's draft phase (median over the 34 rounds of a run) is
10043    /// 34.5 ms per round old against 30.1 new, i.e. 4.93 → 4.31 ms per
10044    /// draft step. That is the whole prize: the 7 submits cost ~0.6 ms
10045    /// each in host and submit latency and nothing else changes —
10046    /// acceptance (3.41 of 7) and tokens per round (4.41) are identical,
10047    /// and the round is 289 → 285 ms, decode 13.8 → 14.0 tok/s.
10048    fn mtp_chain_on() -> bool {
10049        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10050        *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
10051    }
10052
10053    /// The round's k greedy drafts as ONE command buffer on Metal: the MTP
10054    /// block k times back to back, each step's token embedding gathered
10055    /// on the device from the argmax the step before it wrote, the head
10056    /// over the round's shortlist (or the full head during a full-head
10057    /// streak — decided once, before the chain, exactly as the per-step
10058    /// path decides it per step, since `draft_full_streak` only moves on
10059    /// a commit). One wait, then the k ids and the k appended K/V rows
10060    /// come back; the CPU MTP cache ends where k `mtp_step_metal` calls
10061    /// would have left it. `Err(false)` = declined before anything was
10062    /// committed (the per-step path takes the round); `Err(true)` = the
10063    /// command buffer failed after commit.
10064    #[cfg(target_os = "macos")]
10065    fn mtp_draft_chain_metal(
10066        &mut self,
10067        m: &mut MtpModule,
10068        hidden: &[f32],
10069        t_next: u32,
10070        position: usize,
10071        k: usize,
10072    ) -> Result<Vec<u32>, bool> {
10073        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
10074        if k == 0
10075            || k > 64
10076            || !Self::mtp_chain_on()
10077            || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
10078            || !crate::gpu::q1_force()
10079            || !crate::gpu::enabled_here()
10080            || self.attn_softcap > 0.0
10081            || self.attention_heads_per_layer.is_some()
10082            || m.kv.mode != crate::kv_cache::KvMode::F32
10083            || m.kv.o1.is_some()
10084            // the chain gathers embeddings itself: only the plain table
10085            || self.dsv4.is_some()
10086            || self.dsv41.is_some()
10087            || self.qwen4_exp.is_some()
10088            || self.g3n.is_some()
10089        {
10090            return Err(false);
10091        }
10092        let AttnKind::Full {
10093            wq,
10094            wk,
10095            wv,
10096            wo,
10097            q_norm,
10098            k_norm,
10099            output_gate,
10100            softplus_gate: None,
10101            bias: None,
10102        } = &m.layer.attn
10103        else {
10104            return Err(false);
10105        };
10106        let FfnKind::Dense(d) = &m.layer.ffn else {
10107            return Err(false);
10108        };
10109        if d.act != Act::Silu || !d.segs.is_empty() {
10110            return Err(false);
10111        }
10112        let (Some(pq), Some(pk), Some(pv), Some(po)) =
10113            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
10114        else {
10115            return Err(false);
10116        };
10117        let (Some(g), Some(u), Some(dn)) = (
10118            d.gate_proj.q1_parts(),
10119            d.up_proj.q1_parts(),
10120            d.down_proj.q1_parts(),
10121        ) else {
10122            return Err(false);
10123        };
10124        let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
10125            return Err(false);
10126        };
10127        let QTensor::Mapped { model, .. } = wq else {
10128            return Err(false);
10129        };
10130        let model = model.clone();
10131        // the embedding table: a q4tp tensor of the SAME blob, no Prism
10132        // inverse-embedding post-pass
10133        let QTensor::Mapped {
10134            model: em,
10135            idx: eidx,
10136            dtype: cortiq_core::TensorDtype::Q4TiledP,
10137            ..
10138        } = &self.weights.embed_tokens
10139        else {
10140            return Err(false);
10141        };
10142        if !std::sync::Arc::ptr_eq(em, &model)
10143            || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
10144        {
10145            return Err(false);
10146        }
10147        let embed = (
10148            *eidx,
10149            self.weights.embed_tokens.rows(),
10150            self.weights.embed_tokens.cols(),
10151        );
10152        if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
10153            return Err(false);
10154        }
10155        let dims = GraphDims {
10156            hidden: self.hidden_size,
10157            eps: self.rms_eps as f32,
10158            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
10159        };
10160        let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
10161            return Err(false);
10162        };
10163        if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
10164            return Err(false);
10165        }
10166        let l = AttnGpuLayer {
10167            attn_norm: &m.layer.input_norm,
10168            post_norm: &m.layer.post_norm,
10169            wq: pq,
10170            wk: pk,
10171            wv: pv,
10172            wo: po,
10173            ffn: MetalFfn::Dense {
10174                gate: g,
10175                up: u,
10176                down: dn,
10177            },
10178        };
10179        let (nh, nkv, hd, rd) = (
10180            self.num_heads,
10181            self.num_kv_heads,
10182            self.head_dim,
10183            self.rotary_dim,
10184        );
10185        let inv_freq = self.inv_freq.clone();
10186        let draft_rows = self.draft_head_rows(lm.1);
10187        let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
10188        if n_arg == 0 {
10189            return Err(false);
10190        }
10191        // `CMF_MTP_CHAIN_SPLIT=1` commits each step as it is encoded, so
10192        // the GPU starts on step 0 while the host is still encoding step
10193        // 1 — a probe for whether the host encode is on the critical
10194        // path. It is not: three runs each, draft 30.0 ms per round split
10195        // against 30.1 whole, and the whole chain's host encode measures
10196        // 0.3 ms against a 29.7 ms wait. Kept as a probe, off by default.
10197        let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
10198        let t_chain = std::time::Instant::now();
10199        graph.chain_ids_init(t_next, k);
10200        let cpu_stored;
10201        {
10202            let cache = &m.kv;
10203            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
10204            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
10205            cpu_stored = cpu_k[0].len() / hd;
10206            for j in 0..k {
10207                if !graph.encode_chain_input(
10208                    embed,
10209                    j as u32,
10210                    &m.enorm,
10211                    &m.hnorm,
10212                    self.embed_multiplier,
10213                    eh,
10214                ) {
10215                    return Err(false);
10216                }
10217                // step j's mirror row: the mirror is re-pointed at the CPU
10218                // rows before step 0 and advances by one per step; its
10219                // resync (never taken past step 0) reads the CPU rows
10220                let p = AttnDeviceParams {
10221                    kv_id: self.mtp_kv_id(),
10222                    layer: Self::MTP_LAYER_BASE,
10223                    nh,
10224                    nkv,
10225                    hd,
10226                    rd,
10227                    position: position + j,
10228                    scale: self.attn_scale,
10229                    eps: self.rms_eps as f32,
10230                    gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
10231                    late_qk_norm: self.qk_norm_after_rope,
10232                    output_gate: *output_gate,
10233                    q_norm: q_norm.as_deref(),
10234                    k_norm: k_norm.as_deref(),
10235                    inv_freq: &inv_freq,
10236                    cpu_k: cpu_k.clone(),
10237                    cpu_v: cpu_v.clone(),
10238                    cpu_stored: cpu_stored + j,
10239                    o1: None,
10240                };
10241                if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
10242                    return Err(false);
10243                }
10244                if draft_rows < lm.1 {
10245                    if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
10246                        return Err(false);
10247                    }
10248                } else {
10249                    graph.encode_lm_head(&m.final_norm, lm);
10250                }
10251                if !graph.encode_argmax(n_arg, j as u32 + 1) {
10252                    return Err(false);
10253                }
10254                if split {
10255                    // CMF_MTP_CHAIN_SPLIT=1: commit every step so the GPU
10256                    // starts on step 0 while the host encodes the rest
10257                    graph.commit();
10258                }
10259            }
10260        }
10261        let t_enc = t_chain.elapsed();
10262        if graph.sync_checked().is_err() {
10263            return Err(true);
10264        }
10265        if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
10266            eprintln!(
10267                "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
10268                t_enc.as_secs_f64() * 1e3,
10269                (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
10270                if split { ", split" } else { "" }
10271            );
10272        }
10273        let mut ids = vec![0u32; k];
10274        if !graph.chain_ids_read(&mut ids) {
10275            return Err(true);
10276        }
10277        let mut kbuf = vec![0f32; k * nkv * hd];
10278        let mut vbuf = vec![0f32; k * nkv * hd];
10279        if !crate::gpu_metal::kv_mirror_read_rows(
10280            self.mtp_kv_id(),
10281            Self::MTP_LAYER_BASE,
10282            nkv,
10283            hd,
10284            cpu_stored,
10285            k,
10286            &mut kbuf,
10287            &mut vbuf,
10288        ) {
10289            return Err(true);
10290        }
10291        for r in 0..k {
10292            m.kv.append(
10293                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
10294                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
10295                &[],
10296            );
10297        }
10298        Ok(ids)
10299    }
10300
10301    fn try_batch_graph_wgpu(
10302        &self,
10303        hiddens: &mut [f32],
10304        positions: &[usize],
10305        k: usize,
10306        spec: Option<crate::gpu::SpecTail<'_>>,
10307    ) -> crate::gpu::BatchGraphOutcome {
10308        let _tb = std::time::Instant::now();
10309        let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
10310        if self.attn_softcap > 0.0 {
10311            return crate::gpu::BatchGraphOutcome::Declined; // capped scores: no graph kernel — CPU path
10312        }
10313        let nh = self.num_heads;
10314        let (nkv, hd, rd) = self.layer_geom(0);
10315        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10316        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
10317            if let Some((m, i, kind, rs)) = t
10318                .graph_weight()
10319                .or_else(|| t.graph_weight_descriptor())
10320            {
10321                let name = &m.tensors[i].name;
10322                let prism = if crate::prism::is_inverse_embedding(m, name) {
10323                    crate::gpu::GraphPrismOp::InverseEmbedding
10324                } else if crate::prism::is_forward_weight(m, name) {
10325                    crate::gpu::GraphPrismOp::Forward
10326                } else {
10327                    crate::gpu::GraphPrismOp::None
10328                };
10329                return Some(crate::gpu::GraphW {
10330                    idx: i,
10331                    kind,
10332                    row_scale: rs,
10333                    data: &[],
10334                    prism,
10335                    affine: crate::prism::is_affine_target(m, name),
10336                });
10337            }
10338            if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
10339                eprintln!(
10340                    "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
10341                    t.rows(),
10342                    t.cols()
10343                );
10344            }
10345            t.as_f32().map(|d| crate::gpu::GraphW {
10346                idx: 0,
10347                kind: 4,
10348                row_scale: &[],
10349                data: d,
10350                prism: crate::gpu::GraphPrismOp::None,
10351                affine: false,
10352            })
10353        }
10354        let built: Option<(
10355            Vec<crate::gpu::GraphLayer<'_>>,
10356            std::sync::Arc<cortiq_core::CmfModel>,
10357        )> = (|| {
10358            let mut layers = Vec::with_capacity(self.num_layers);
10359            let mut model = None;
10360            for li in 0..self.num_layers {
10361                let lw = &self.weights.layers[self.phys_layer(li)];
10362                // MoE routes per token, so its experts are encoded token by
10363                // token inside the batched submit while attention and the
10364                // projections stay GEMMs. Refusing MoE here is what left
10365                // prefill running one position at a time: 33 tok/s against
10366                // 54 on decode, i.e. reading the prompt was slower than
10367                // writing the answer.
10368                let gffn = match &lw.ffn {
10369                    FfnKind::Dense(d) if !d.segs.is_empty() => {
10370                        if batch_debug {
10371                            eprintln!("batch graph: dense segmented FFN at layer {li}");
10372                        }
10373                        return None;
10374                    }
10375                    FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
10376                        gate: gw(&d.gate_proj)?,
10377                        up: gw(&d.up_proj)?,
10378                        down: gw(&d.down_proj)?,
10379                    },
10380                    FfnKind::Moe(m) => {
10381                        // Adaptive τ and expert masks stay on the CPU path.
10382                        // Sigmoid scores, the selection bias, a routed scale
10383                        // ≠ 1 and an ungated shared expert (hy_v3) ride the
10384                        // same flags word as the token graph — before, this
10385                        // refusal sent every Hy-MT2-30B prompt to the chunked
10386                        // fallback (8 tok/s of ingest against 53 of decode).
10387                        if m.route_tau.is_some() || m.mask.is_some() {
10388                            return None;
10389                        }
10390                        // The batch MoE kernels need the shared slot (k+1
10391                        // rows); gated or not is a flag on the select kernel.
10392                        let (se, sg) = m.shared.as_ref()?;
10393                        let shared_gated = sg.is_some();
10394                        let sgate = match sg {
10395                            Some(sg) => gw(sg)?,
10396                            // Ungated: the router plane stands in so the
10397                            // plumbing stays total; the kernel pins weight 1.
10398                            None => gw(&m.router)?,
10399                        };
10400                        let router = gw(&m.router)?;
10401                        // The batch MoE kernels still consume raw per-token
10402                        // rows and do not carry the descriptor-aware Prism
10403                        // transform/affine bit for router or shared-gate
10404                        // planes.  Refuse rather than route an untransformed
10405                        // source activation.
10406                        if router.prism != crate::gpu::GraphPrismOp::None
10407                            || router.affine
10408                            || sgate.prism != crate::gpu::GraphPrismOp::None
10409                            || sgate.affine
10410                        {
10411                            return None;
10412                        }
10413                        let inter = m.experts.first()?.gate_proj.rows();
10414                        let mut experts = Vec::with_capacity(m.experts.len() + 1);
10415                        let mut q4tp: Option<bool> = None;
10416                        let mut gu_q2: Option<bool> = None;
10417                        for e in m.experts.iter().chain(std::iter::once(se)) {
10418                            if !matches!(e.act, Act::Silu)
10419                                || e.gate_proj.rows() != inter
10420                                || e.up_proj.rows() != inter
10421                            {
10422                                return None;
10423                            }
10424                            // Same ladder as the token graph: q4t → q2tp
10425                            // (mixed profile: 2-bit gate/up over a q4tp
10426                            // down) → q4tp. Uniform across the layer.
10427                            let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
10428                                Some((mm, gi)) => (
10429                                    mm,
10430                                    gi,
10431                                    e.up_proj.mapped_q4t()?.1,
10432                                    e.down_proj.mapped_q4t()?.1,
10433                                    false,
10434                                    false,
10435                                ),
10436                                None => match e.gate_proj.mapped_q2tp() {
10437                                    Some((mm, gi)) => (
10438                                        mm,
10439                                        gi,
10440                                        e.up_proj.mapped_q2tp()?.1,
10441                                        e.down_proj.mapped_q4tp()?.1,
10442                                        true,
10443                                        true,
10444                                    ),
10445                                    None => {
10446                                        let (mm, gi) = e.gate_proj.mapped_q4tp()?;
10447                                        (
10448                                            mm,
10449                                            gi,
10450                                            e.up_proj.mapped_q4tp()?.1,
10451                                            e.down_proj.mapped_q4tp()?.1,
10452                                            true,
10453                                            false,
10454                                        )
10455                                    }
10456                                },
10457                            };
10458                            if *q4tp.get_or_insert(is_p) != is_p
10459                                || *gu_q2.get_or_insert(is_q2) != is_q2
10460                            {
10461                                return None;
10462                            }
10463                            if [gi, ui, di].into_iter().any(|idx| {
10464                                mm.tensors
10465                                    .get(idx)
10466                                    .is_some_and(|t| {
10467                                        crate::prism::is_forward_weight(mm, &t.name)
10468                                            || crate::prism::is_affine_target(mm, &t.name)
10469                                    })
10470                            }) {
10471                                return None;
10472                            }
10473                            model.get_or_insert_with(|| mm.clone());
10474                            experts.push((gi, ui, di));
10475                        }
10476                        crate::gpu::GraphFfn::Moe {
10477                            router,
10478                            shared_gate: sgate,
10479                            experts,
10480                            n_exp: m.experts.len(),
10481                            top_k: m.top_k,
10482                            inter,
10483                            norm_topk: m.norm_topk_prob,
10484                            q4tp: q4tp?,
10485                            gu_q2: gu_q2.unwrap_or(false),
10486                            sigmoid: m.router_sigmoid,
10487                            bias: m.expert_bias.as_deref(),
10488                            has_shared: true,
10489                            shared_gated,
10490                            route_scale: m.routed_scaling,
10491                        }
10492                    }
10493                    _ => return None,
10494                };
10495                let attn = match &lw.attn {
10496                    AttnKind::Full {
10497                        wq,
10498                        wk,
10499                        wv,
10500                        wo,
10501                        q_norm,
10502                        k_norm,
10503                        output_gate,
10504                        softplus_gate,
10505                        bias,
10506                    } => {
10507                        if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
10508                            if batch_debug {
10509                                eprintln!(
10510                                    "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
10511                                    softplus_gate.is_some(),
10512                                    self.attention_heads_per_layer.is_some()
10513                                );
10514                            }
10515                            return None;
10516                        }
10517                        let (m, _, _, _) = wq
10518                            .graph_weight()
10519                            .or_else(|| wq.graph_weight_descriptor())?;
10520                        model = Some(m.clone());
10521                        crate::gpu::GraphAttn::Full {
10522                            wq: gw(wq)?,
10523                            wk: gw(wk)?,
10524                            wv: gw(wv)?,
10525                            wo: gw(wo)?,
10526                            q_norm: q_norm.as_deref(),
10527                            k_norm: k_norm.as_deref(),
10528                            late_qk_norm: self.qk_norm_after_rope,
10529                            bias: bias
10530                                .as_ref()
10531                                .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10532                            output_gate: *output_gate,
10533                            cpu_k: self.kv_cache.layers[li].k_heads(),
10534                            cpu_v: self.kv_cache.layers[li].v_heads(),
10535                        }
10536                    }
10537                    AttnKind::LinearGdn(w) => {
10538                        let Some(cfg) = self.gdn_cfg else {
10539                            if batch_debug {
10540                                eprintln!("batch graph: no GDN config at layer {li}");
10541                            }
10542                            return None;
10543                        };
10544                        let (m, _, _, _) = w
10545                            .in_proj_qkv
10546                            .graph_weight()
10547                            .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
10548                        model = Some(m.clone());
10549                        crate::gpu::GraphAttn::Gdn {
10550                            qkv: gw(&w.in_proj_qkv)?,
10551                            z: gw(&w.in_proj_z)?,
10552                            a: gw(&w.in_proj_a)?,
10553                            b: gw(&w.in_proj_b)?,
10554                            out: gw(&w.out_proj)?,
10555                            conv1d: &w.conv1d,
10556                            a_log: &w.a_log,
10557                            dt_bias: &w.dt_bias,
10558                            norm: &w.norm,
10559                            nv: cfg.num_v_heads,
10560                            nk: cfg.num_k_heads,
10561                            dk: cfg.key_head_dim,
10562                            dv: cfg.value_head_dim,
10563                            kk: cfg.conv_kernel,
10564                            cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
10565                        }
10566                    }
10567                    _ => return None,
10568                };
10569                layers.push(crate::gpu::GraphLayer {
10570                    input_norm: &lw.input_norm,
10571                    attn,
10572                    post_norm: &lw.post_norm,
10573                    ffn: gffn,
10574                });
10575            }
10576            Some((layers, model?))
10577        })();
10578        let Some((layers, model)) = built else {
10579            {
10580                use std::sync::atomic::{AtomicBool, Ordering};
10581                static SAID: AtomicBool = AtomicBool::new(false);
10582                if !SAID.swap(true, Ordering::Relaxed) {
10583                    tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
10584                }
10585            }
10586            return crate::gpu::BatchGraphOutcome::Declined;
10587        };
10588        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
10589            eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
10590        }
10591        crate::gpu::forward_batch_graph(
10592            &model,
10593            self.graph_kv_id,
10594            &layers,
10595            &self.inv_freq,
10596            hiddens,
10597            nh,
10598            nkv,
10599            hd,
10600            rd,
10601            self.hidden_size,
10602            self.intermediate_size,
10603            positions,
10604            self.kv_cache.max_seq_len,
10605            gemma,
10606            self.rms_eps as f32,
10607            self.attn_scale,
10608            k,
10609            &(0..self.num_layers)
10610                .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
10611                .collect::<Vec<_>>(),
10612            self.o1_epoch,
10613            spec,
10614        )
10615    }
10616
10617    /// Same, stopping after layer `upto` inclusive (routing probe φ).
10618    /// `CMF_DSV4_DRAFT_PROBE=1` — grade the draft against what the trunk goes on
10619    /// to produce. Off by default; it runs a whole draft per decoded token.
10620    fn draft_probe() -> bool {
10621        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10622        *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
10623    }
10624
10625    /// `CMF_DSV4_DRAFT_PROBE=1`: measure how much of the draft the trunk
10626    /// would have agreed with, WITHOUT verifying or rolling anything back.
10627    ///
10628    /// The number this produces decides the whole speculation design — at
10629    /// acceptance a, a block of B positions yields 1 + a + a² + ... tokens
10630    /// per trunk pass — so it is worth measuring before any of the machinery
10631    /// that would exploit it exists. Each draft is parked with the position
10632    /// it was made at, and graded as the real tokens arrive.
10633    /// `CMF_DSV4_SPEC=1` — the DeepSeek-V4 speculative decode: draft five
10634    /// on the card, verify them in one batched trunk pass, commit the
10635    /// accepted prefix, roll the rest back.
10636    #[cfg(feature = "gpu")]
10637    fn dsv4_spec_on() -> bool {
10638        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10639        *ON.get_or_init(|| {
10640            // Test-only runtime gate: model loading still performs the same
10641            // reservation and trunk packing, which gives rollback parity a
10642            // topology-identical non-speculative control arm.
10643            if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
10644                return v != "0";
10645            }
10646            // An explicit value is a diagnostic force/escape hatch.  With no
10647            // knob, speculation is eligible only when model loading reserved
10648            // its bounded pack.  On small q4tp cards the geometric reserve
10649            // gate deliberately leaves this at zero: trying to build DSpark
10650            // after the exact trunk filled VRAM is both slower and a device
10651            // OOM (measured on A40).
10652            std::env::var("CMF_DSV4_SPEC")
10653                .map(|v| v != "0")
10654                .unwrap_or_else(|_| {
10655                    crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
10656                })
10657        })
10658    }
10659
10660    /// One speculative round at the decode tip. `t_next` is the token the
10661    /// sampler just committed for `next_pos`. Returns the EXTRA accepted
10662    /// tokens (possibly none) and the new position, with `graph_logits`
10663    /// left holding the last accepted position's logits — exactly what the
10664    /// loop top expects. `None` means "speculate not this round": nothing
10665    /// was committed, the caller forwards normally.
10666    #[cfg(feature = "gpu")]
10667    fn dsv4_spec_step(
10668        &mut self,
10669        tip_token: u32,
10670        t_next: u32,
10671        next_pos: usize,
10672        max_extra: usize,
10673        drafted: &mut usize,
10674        accepted_ctr: &mut usize,
10675    ) -> Option<(Vec<u32>, usize)> {
10676        let t_all = std::time::Instant::now();
10677        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
10678            thread_local! {
10679                static LAST: std::cell::Cell<Option<std::time::Instant>> =
10680                    const { std::cell::Cell::new(None) };
10681            }
10682            LAST.with(|l| {
10683                if let Some(prev) = l.get() {
10684                    eprintln!(
10685                        "между раундами {:.1} мс",
10686                        prev.elapsed().as_secs_f64() * 1e3
10687                    );
10688                }
10689                l.set(Some(std::time::Instant::now()));
10690            });
10691        }
10692        if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
10693            eprintln!("spec_step: вход pos={next_pos}");
10694        }
10695        let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
10696        let cfg = self.dsv4.as_ref().map(|b| b.2)?;
10697        // The draft state and its capture, armed exactly as the probe does.
10698        if self.dspark.is_none() {
10699            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
10700            if t.is_empty() {
10701                return None;
10702            }
10703            crate::dsv4::dspark_arm(&t, cfg.dim);
10704            self.dspark = Some(crate::dsv4::DsparkState::new(
10705                self.dsv4_mtp.len(),
10706                &cfg,
10707                t.len(),
10708            ));
10709        }
10710        let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
10711        let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
10712        if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
10713            eprintln!("spec_step: пак не построился (targets {targets:?})");
10714        }
10715        let pack = pack?;
10716        let block = crate::dsv4::dspark_block();
10717        let b_box = self.dsv4.as_mut()?;
10718        let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
10719        let ds = self.dspark.as_mut()?;
10720        // The tip's captures: either this token ran on a normal path that
10721        // filled the thread-local, or the previous spec round left them.
10722        let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
10723        if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
10724            if dbg {
10725                eprintln!("spec_step: нет захвата");
10726            }
10727            return None;
10728        }
10729        ds.have_hidden = true;
10730        let tip_pos = next_pos.checked_sub(1)?;
10731        let draft_started = std::time::Instant::now();
10732        let mut conf = Vec::new();
10733        let props = crate::dsv4::dspark_draft_gpu(
10734            g,
10735            &self.dsv4_mtp,
10736            &cfg,
10737            ds,
10738            pack,
10739            st.kv_id,
10740            tip_token,
10741            tip_pos,
10742            self.pool.as_deref(),
10743            &mut conf,
10744        );
10745        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
10746        *drafted += block;
10747        if props.is_empty() || props[0] != t_next {
10748            if dbg {
10749                eprintln!(
10750                    "spec_step: черновик {} (props0={:?} t_next={t_next})",
10751                    if props.is_empty() {
10752                        "пуст"
10753                    } else {
10754                        "мимо"
10755                    },
10756                    props.first()
10757                );
10758            }
10759            return None;
10760        }
10761        // `fed[0]` is `t_next`, which the outer loop has already committed;
10762        // only `fed[1..]` become additional output tokens. Cap the verify
10763        // transaction itself to the caller's remaining output budget instead
10764        // of merely truncating the returned vector: otherwise the KV/state
10765        // would advance past `max_tokens` and a 64-token request could return
10766        // 66 tokens (and poison a reused session with two invisible steps).
10767        let mut k_verify = crate::dsv4::dspark_verify_k()
10768            .min(props.len())
10769            .min(max_extra.saturating_add(1));
10770        // Adaptive depth: positions the draft itself doubts are paid for on
10771        // every verify and delivered almost never (natural-text survival
10772        // [.67 .50 .29 .08 .04]). `CMF_DSPARK_CONF_MIN=p` trims the fed
10773        // prefix at the first proposal whose confidence drops below p; on
10774        // predictable text the confidences stay high and nothing changes.
10775        let conf_min = {
10776            static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
10777            *M.get_or_init(|| {
10778                std::env::var("CMF_DSPARK_CONF_MIN")
10779                    .ok()
10780                    .and_then(|v| v.parse().ok())
10781                    .unwrap_or(0.0)
10782            })
10783        };
10784        if conf_min > 0.0 && conf.len() >= props.len() {
10785            let mut keep = 1usize;
10786            while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
10787                keep += 1;
10788            }
10789            k_verify = k_verify.min(keep.max(2));
10790        }
10791        if k_verify < 2 {
10792            return None;
10793        }
10794        let mut fed = Vec::with_capacity(k_verify);
10795        fed.push(t_next);
10796        fed.extend_from_slice(&props[1..k_verify]);
10797        let mut argmax = Vec::new();
10798        let mut logits_all = Vec::new();
10799        let mut walked = Vec::new();
10800        let txn = crate::dsv4::dsv4_verify_chunk(
10801            g,
10802            layers,
10803            &cfg,
10804            st,
10805            &fed,
10806            next_pos,
10807            &self.inv_freq,
10808            self.pool.as_deref(),
10809            &targets,
10810            &mut argmax,
10811            &mut logits_all,
10812            &mut walked,
10813        );
10814        if txn.is_none() && dbg {
10815            eprintln!("spec_step: verify отказал");
10816        }
10817        let txn = txn?;
10818        let spec_gpu_end = txn.gpu_end;
10819        let b = fed.len();
10820        let mut accepted = 1usize;
10821        while accepted < b && fed[accepted] == argmax[accepted - 1] {
10822            accepted += 1;
10823        }
10824        // `CMF_DSV4_SPEC_FORCE_REJECT=1` — accept nothing beyond the known
10825        // token, every round: the pure rollback exerciser. The output must
10826        // stay byte-identical to the plain walk; anything else is a
10827        // transaction bug, isolated from the acceptance logic.
10828        if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
10829            accepted = 1;
10830        }
10831        if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
10832            eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
10833        }
10834        let t_fin = std::time::Instant::now();
10835        if !crate::dsv4::dsv4_spec_finish(
10836            g,
10837            layers,
10838            &cfg,
10839            st,
10840            txn,
10841            accepted,
10842            &fed,
10843            &self.inv_freq,
10844            self.pool.as_deref(),
10845        ) {
10846            tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
10847            return None;
10848        }
10849        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
10850            eprintln!(
10851                "finish(k={accepted}): {:.1} мс",
10852                t_fin.elapsed().as_secs_f64() * 1e3
10853            );
10854        }
10855        *accepted_ctr += accepted - 1;
10856        // Captures per accepted token: device targets photographed by the
10857        // batch, host targets from the verify's own walk. The last one
10858        // becomes the new tip's draft input; every one owes the ring an
10859        // entry for its position.
10860        let (hc, dim) = (cfg.hc_mult, cfg.dim);
10861        // Complete-chain layers are photographed by the fused submission;
10862        // partial device layers overwrite that slot after exact host cold-
10863        // expert correction.  Thus every target in the contiguous device
10864        // prefix has a valid per-token capture.
10865        let dev_caps: Vec<usize> = targets
10866            .iter()
10867            .copied()
10868            .filter(|&t| t < spec_gpu_end)
10869            .collect();
10870        let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
10871        if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
10872            return None;
10873        }
10874        for t in 0..accepted {
10875            let tip = t + 1 == accepted;
10876            for (slot, &tl) in targets.iter().enumerate() {
10877                if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
10878                    let lo = (di * b + t) * hc * dim;
10879                    crate::dsv4::dspark_capture(
10880                        &caps_all[lo..lo + hc * dim],
10881                        &cfg,
10882                        slot,
10883                        &mut ds.main_hidden,
10884                    );
10885                } else if tip
10886                    && crate::dsv4::dspark_peek_slot(slot, dim, {
10887                        let lo = slot * dim;
10888                        &mut ds.main_hidden[lo..lo + dim]
10889                    })
10890                {
10891                    // The tip's host-layer captures are the walk's own
10892                    // per-layer notes — exact. (The walk that ran last ended
10893                    // on exactly this token, on both the accept-all and the
10894                    // rollback path.)
10895                } else {
10896                    // Intermediate tokens: the post-tail state stands in for
10897                    // the per-layer capture on host targets below the last
10898                    // layer. Ring-entry quality only; the tip is exact.
10899                    crate::dsv4::dspark_capture(
10900                        &walked[t * hc * dim..(t + 1) * hc * dim],
10901                        &cfg,
10902                        slot,
10903                        &mut ds.main_hidden,
10904                    );
10905                }
10906            }
10907            crate::dsv4::dspark_ring_append(
10908                g,
10909                &self.dsv4_mtp,
10910                &cfg,
10911                ds,
10912                next_pos + t,
10913                self.pool.as_deref(),
10914            );
10915        }
10916        let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
10917        self.graph_logits = Some(row);
10918        // The speculative loop never runs the probe, so the trunk tally has
10919        // no other place to cycle. Armed only when someone asked for the
10920        // dump; the host tail is the only tallying path here, which is
10921        // precisely the population a partial pack would serve.
10922        if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
10923            crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
10924            crate::dsv4::pick_tally_arm();
10925        }
10926        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
10927            eprintln!(
10928                "spec_step total {:.1} мс (k={accepted})",
10929                t_all.elapsed().as_secs_f64() * 1e3
10930            );
10931        }
10932        Some((fed[1..accepted].to_vec(), next_pos + accepted))
10933    }
10934
10935    fn dspark_probe(&mut self, position: usize, token_id: u32) {
10936        if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
10937            return;
10938        }
10939        // What the trunk just routed to, for this token.
10940        let trunk_now = crate::dsv4::pick_tally_take();
10941        crate::dsv4::trunk_freq_note(&trunk_now);
10942        if !trunk_now.is_empty() {
10943            self.dspark_trunk_picks.push(trunk_now);
10944            let keep = crate::dsv4::dspark_block();
10945            if self.dspark_trunk_picks.len() > keep {
10946                self.dspark_trunk_picks.remove(0);
10947            }
10948        }
10949        // Grade whatever is waiting: the token just decoded sits at
10950        // `position`, so it answers the draft made at `position - 1 - i`.
10951        for p in std::mem::take(&mut self.dspark_pending) {
10952            let Some(i) = position.checked_sub(p.0 + 1) else {
10953                continue;
10954            };
10955            let mut p = p;
10956            if i < p.1.len() {
10957                if p.2 && p.1[i] == token_id {
10958                    p.3 = i + 1;
10959                } else {
10960                    p.2 = false;
10961                }
10962                if i + 1 < p.1.len() {
10963                    self.dspark_pending.push(p);
10964                    continue;
10965                }
10966            }
10967            self.dspark_hist.push(p.3);
10968            self.dspark_real.push(token_id);
10969        }
10970        let Some(b) = &mut self.dsv4 else { return };
10971        let (g, layers, cfg) = (&b.0, &b.1, b.2);
10972        let n_layers = layers.len();
10973        if self.dspark.is_none() {
10974            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
10975            if t.is_empty() {
10976                return;
10977            }
10978            eprintln!(
10979                "DSpark: захват со слоёв {t:?}, блок {}",
10980                crate::dsv4::dspark_block()
10981            );
10982            crate::dsv4::dspark_arm(&t, cfg.dim);
10983            self.dspark = Some(crate::dsv4::DsparkState::new(
10984                self.dsv4_mtp.len(),
10985                &cfg,
10986                t.len(),
10987            ));
10988        }
10989        let ds = self.dspark.as_mut().unwrap();
10990        if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
10991            return; // this token ran on a path that captures nothing
10992        }
10993        let mut conf = Vec::new();
10994        crate::dsv4::pick_tally_arm();
10995        // The trunk has already consumed the adaptive VRAM budget. Until the
10996        // draft owns an explicit bounded device pack, its tensors are an
10997        // out-of-core CPU/disk tier by contract: never let per-op probes try
10998        // to squeeze another multi-gigabyte MTP expert cache onto the card.
10999        let draft_started = std::time::Instant::now();
11000        #[cfg(feature = "gpu")]
11001        let gpu_draft = crate::dsv4::dspark_gpu_on();
11002        #[cfg(not(feature = "gpu"))]
11003        let gpu_draft = false;
11004        let props = if gpu_draft {
11005            #[cfg(feature = "gpu")]
11006            {
11007                let kv_id = b.3.kv_id;
11008                match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
11009                    Some(pk) => crate::dsv4::dspark_draft_gpu(
11010                        g,
11011                        &self.dsv4_mtp,
11012                        &cfg,
11013                        ds,
11014                        pk,
11015                        kv_id,
11016                        token_id,
11017                        position,
11018                        self.pool.as_deref(),
11019                        &mut conf,
11020                    ),
11021                    None => Vec::new(),
11022                }
11023            }
11024            #[cfg(not(feature = "gpu"))]
11025            Vec::new()
11026        } else {
11027            crate::gpu::cpu_scope(|| {
11028                crate::dsv4::dspark_draft(
11029                    g,
11030                    &self.dsv4_mtp,
11031                    &cfg,
11032                    ds,
11033                    token_id,
11034                    position,
11035                    self.pool.as_deref(),
11036                    &mut conf,
11037                )
11038            })
11039        };
11040        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
11041        let draft_picks = crate::dsv4::pick_tally_take();
11042        crate::dsv4::dspark_freq_note(&draft_picks);
11043        // Re-arm for the NEXT trunk token; the probe runs after the forward,
11044        // so this is the only place that can.
11045        crate::dsv4::pick_tally_arm();
11046        if !props.is_empty() {
11047            // Two ratios, side by side: what a batched verify over the trunk
11048            // would read against what it asks for, and the same for the
11049            // draft's three stages. Near 1.0 means a batch amortises nothing.
11050            let (tu, tt) = {
11051                let flat: Vec<(usize, Vec<usize>)> = self
11052                    .dspark_trunk_picks
11053                    .iter()
11054                    .flat_map(|v| v.iter().cloned())
11055                    .collect();
11056                // Per layer, across the window of tokens.
11057                let mut per: std::collections::HashMap<usize, Vec<usize>> =
11058                    std::collections::HashMap::new();
11059                for (li, picks) in flat {
11060                    per.entry(li).or_default().extend(picks);
11061                }
11062                let n = per.len().max(1);
11063                let mut u = 0usize;
11064                let mut t = 0usize;
11065                for (_, v) in per {
11066                    t += v.len();
11067                    u += v.iter().collect::<std::collections::HashSet<_>>().len();
11068                }
11069                (u / n, t / n)
11070            };
11071            let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
11072            self.dspark_exp.push((tu, tt, du, dt));
11073            self.dspark_pending.push((position, props, true, 0));
11074        }
11075        if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
11076            let n = self.dspark_hist.len() as f32;
11077            let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
11078            let block = crate::dsv4::dspark_block();
11079            let mut at = vec![0usize; block + 1];
11080            for &k in &self.dspark_hist {
11081                at[k] += 1;
11082            }
11083            // Prefix survival: S_i = P(the first i positions all held).
11084            let mut surv = Vec::with_capacity(block);
11085            for i in 1..=block {
11086                let k = at[i..].iter().sum::<usize>() as f32 / n;
11087                surv.push(format!("{k:.2}"));
11088            }
11089            let distinct = self
11090                .dspark_real
11091                .iter()
11092                .collect::<std::collections::HashSet<_>>()
11093                .len();
11094            let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
11095                (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
11096            });
11097            let m = self.dspark_exp.len().max(1);
11098            eprintln!(
11099                "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
11100                 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
11101                self.dspark_hist.len(),
11102                mean + 1.0,
11103                surv.join(" ")
11104            );
11105            eprintln!(
11106                "DSpark: разных токенов {distinct} из {} (вырожденность), \
11107                 эксперты ствол {}/{} на слой за {block} токенов, \
11108                 черновик {}/{} за блок, draft {:.2} мс/блок",
11109                self.dspark_real.len(),
11110                tu / m,
11111                tt / m,
11112                du / m,
11113                dt / m,
11114                self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
11115            );
11116        }
11117    }
11118
11119    fn forward_layers_upto(
11120        &mut self,
11121        hidden: &[f32],
11122        position: usize,
11123        task_mask: Option<&TaskMask>,
11124        upto: Option<usize>,
11125    ) -> Vec<f32> {
11126        // In-process multi-GPU: each segment runs pinned to its card,
11127        // and the only thing crossing the boundary is one hidden vector
11128        // that never leaves this address space. Same layer split the
11129        // network mode does, minus the second process, the socket, the
11130        // serialization and the dir_hash handshake.
11131        if let Some(plan) = self.gpu_plan.clone() {
11132            if upto.is_none() && plan.len() > 1 {
11133                let mut h = hidden.to_vec();
11134                for &(dev, from, upto_incl) in plan.iter() {
11135                    h = crate::gpu::with_device(dev, || {
11136                        self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
11137                    });
11138                }
11139                return h;
11140            }
11141        }
11142        self.forward_layers_span(hidden, position, task_mask, 0, upto)
11143    }
11144
11145    /// Split this pipeline's layer stack across local GPUs: segment i
11146    /// runs on `devices[i]`. Contiguous and even by layer count — the
11147    /// VRAM-weighted planner is the next step, and an uneven card pair
11148    /// is why it will be needed. `None` clears the plan.
11149    pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
11150        self.set_gpu_plan_at(devices, None)
11151    }
11152
11153    /// The same, with an explicit first boundary (`--peer-split`): card
11154    /// 0 takes layers `[0..at)`, the rest split what remains. Uneven
11155    /// cards, or an attention-heavy head, are why this knob exists.
11156    pub fn set_gpu_plan_at(
11157        &mut self,
11158        devices: Option<&[usize]>,
11159        at: Option<usize>,
11160    ) -> Result<(), String> {
11161        let Some(devs) = devices.filter(|d| d.len() > 1) else {
11162            self.gpu_plan = None;
11163            return Ok(());
11164        };
11165        self.split_supported()?;
11166        let n = self.num_layers;
11167        if devs.len() > n {
11168            return Err(format!("{} devices for {n} layers", devs.len()));
11169        }
11170        if let Some(k) = at {
11171            if k == 0 || k >= n {
11172                return Err(format!("split at {k}: the model has {n} layers"));
11173            }
11174            if devs.len() == 2 {
11175                self.gpu_plan = Some(std::sync::Arc::new(vec![
11176                    (devs[0], 0, k - 1),
11177                    (devs[1], k, n - 1),
11178                ]));
11179                return Ok(());
11180            }
11181            return Err(format!(
11182                "an explicit split point takes exactly 2 devices, got {}",
11183                devs.len()
11184            ));
11185        }
11186        let per = n.div_ceil(devs.len());
11187        let mut plan = Vec::with_capacity(devs.len());
11188        let mut from = 0usize;
11189        for &d in devs {
11190            if from >= n {
11191                break;
11192            }
11193            let upto = (from + per - 1).min(n - 1);
11194            plan.push((d, from, upto));
11195            from = upto + 1;
11196        }
11197        self.gpu_plan = Some(std::sync::Arc::new(plan));
11198        Ok(())
11199    }
11200
11201    /// The active in-process split, if any: (device, first layer, last).
11202    pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
11203        self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
11204    }
11205
11206    /// Layer span [from ..= upto] (upto None = last layer): the building
11207    /// block the network pipeline-split rides on. `from > 0` skips the
11208    /// arch escape hatches (the pub `forward_span` refuses those archs
11209    /// first) and the whole-token graph — the plain per-layer loop is
11210    /// the canonical executor for a partial stack.
11211    fn forward_layers_span(
11212        &mut self,
11213        hidden: &[f32],
11214        position: usize,
11215        task_mask: Option<&TaskMask>,
11216        from: usize,
11217        upto: Option<usize>,
11218    ) -> Vec<f32> {
11219        debug_assert!(
11220            from == 0
11221                || (self.dsv4.is_none()
11222                    && self.dsv41.is_none()
11223                    && self.qwen4_exp.is_none()
11224                    && self.g3n.is_none())
11225        );
11226        // Every plain forward — the whole-token Metal graph (`q1_graph_gpu`
11227        // wraps the GDN owners zero-copy and reallocates them on a size
11228        // change) and the CPU layer loop (reads/swaps `linear_state`) —
11229        // must see the previous speculative commit's asynchronous replay
11230        // complete. One mutex probe when nothing is pending.
11231        #[cfg(target_os = "macos")]
11232        if !crate::gpu_metal::wait_replay() {
11233            self.fail_metal_graph("the pending async replay failed before a plain forward");
11234            return vec![0.0; self.hidden_size];
11235        }
11236        if let Some(b) = &mut self.qwen4_exp {
11237            let _ = (task_mask, upto);
11238            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
11239            let mut logits = Vec::new();
11240            crate::qwen4_exp::forward_token(
11241                &b.0,
11242                &b.1,
11243                &b.2,
11244                &mut b.3,
11245                token_id,
11246                position,
11247                &self.inv_freq,
11248                self.pool.as_deref(),
11249                &mut logits,
11250                true,
11251            );
11252            self.graph_logits = Some(logits);
11253            return vec![0.0; self.hidden_size];
11254        }
11255        // DeepSeek-V4 runs its own stack: the state is hc_mult copies, and
11256        // the forward returns LOGITS, not a hidden — the head is inside it
11257        // (the final fold sits between the last layer and the norm). The
11258        // token id rides in `hidden[0]`, written by embed_single, because
11259        // the hash layers route by id rather than by content.
11260        if let Some(b) = &mut self.dsv4 {
11261            let _ = (task_mask, upto);
11262            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
11263            let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
11264            st.pos = position;
11265            let mut logits = Vec::new();
11266            crate::dsv4::forward_token(
11267                g,
11268                layers,
11269                &cfg,
11270                st,
11271                token_id,
11272                &self.inv_freq,
11273                self.pool.as_deref(),
11274                &mut logits,
11275            );
11276            self.graph_logits = Some(logits);
11277            self.dspark_probe(position, token_id);
11278            // The caller expects a hidden; the logits went out of band, as
11279            // with the fused lm_head path.
11280            return vec![0.0; self.hidden_size];
11281        }
11282        // DeepSeek-V4.1 owns its complete stack and emits logits out of band.
11283        if let Some(b) = &mut self.dsv41 {
11284            let _ = (task_mask, upto);
11285            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
11286            let mut logits = Vec::new();
11287            crate::dsv41::forward_token(
11288                &b.0,
11289                &b.1,
11290                &b.2,
11291                &mut b.3,
11292                token_id,
11293                position,
11294                self.pool.as_deref(),
11295                &mut logits,
11296            );
11297            self.graph_logits = Some(logits);
11298            return vec![0.0; self.hidden_size];
11299        }
11300        // Gemma-3n runs its own stack (4 AltUp replicas don't fit this
11301        // loop); `hidden` is the extended embedding from embed_single.
11302        if let Some(b) = &self.g3n {
11303            let _ = (task_mask, upto);
11304            return crate::g3n::g3n_forward(
11305                &b.0,
11306                &b.1,
11307                hidden,
11308                position,
11309                &mut self.kv_cache.layers,
11310                self.num_heads,
11311                self.num_kv_heads,
11312                self.head_dim,
11313                self.pool.as_deref(),
11314            );
11315        }
11316        let mut h = hidden.to_vec();
11317        // Split borrows: copy scalars / clone handles so the per-layer
11318        // cfg does not hold `&self` while the KV cache is `&mut`.
11319        let (nh, _nkv, _hd, hs, _rd, eps) = (
11320            self.num_heads,
11321            self.num_kv_heads,
11322            self.head_dim,
11323            self.hidden_size,
11324            self.rotary_dim,
11325            self.rms_eps,
11326        );
11327        let pool = self.pool.clone();
11328        // Opt-in wgpu token-graph attention (discrete Vulkan/DX12): the whole
11329        // attention sub-block runs resident in one submit. Off by default.
11330        // Whole-token wgpu graph: eligibility + arbitration.
11331        //  - explicit CMF_GPU_WGPU_GRAPH forces it on/off;
11332        //  - discrete adapters (4090: decode 76 -> 137 tok/s) and GDN
11333        //    hybrids (recurrent state device-resident, no CPU twin to
11334        //    race) TRUST it;
11335        //  - integrated/mobile adapters RACE it against the normal path
11336        //    at generation granularity (gpu::graph_race_*) — tiled
11337        //    mobile GPUs can turn the ~300-dispatch graph into seconds
11338        //    per token, while a fast phone GPU keeps its win.
11339        let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
11340        let graph_on = match graph_env.as_deref() {
11341            Some("0") => false,
11342            Some("prefill") => false, // decode keeps the per-op path
11343            Some(_) => true,
11344            // Unset: same discrete-only default as every other graph
11345            // site. "Is the GPU on" used to stand in here — which made
11346            // the 0.2 tok/s whole-token graph race-eligible on mobile
11347            // adapters and cost 12-14× on first tokens (cmfmobile
11348            // TUNING.md); integrated GPUs keep the per-op probe path.
11349            None => crate::gpu::wgpu_graph_default(),
11350        };
11351        let graph_trusted =
11352            graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
11353        let race_eligible = graph_on
11354            && upto.is_none()
11355            && task_mask.is_none()
11356            && from == 0
11357            && !crate::gpu::graph_unsupported();
11358        let mut tail_start = 0usize;
11359        if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
11360            let t_graph = std::time::Instant::now();
11361            let mut lg = Vec::new();
11362            let mut gl = 0usize;
11363            let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
11364            let declined = built.is_none();
11365            let built = match built {
11366                Some(Ok(hh)) => Some(hh),
11367                Some(Err(())) => {
11368                    // O(1) state was admitted before the device failure; the
11369                    // CPU mirrors are stale by construction.  Clear the whole
11370                    // sequence and stop rather than walking that stale state.
11371                    self.clear_sequence_state();
11372                    self.graph_failed
11373                        .store(true, std::sync::atomic::Ordering::Relaxed);
11374                    self.cancel
11375                        .store(true, std::sync::atomic::Ordering::Relaxed);
11376                    tracing::error!("token graph failed after admission; sequence state cleared");
11377                    return vec![0.0; self.hidden_size];
11378                }
11379                None => None,
11380            };
11381            // Past the transient guards (o1 still collecting, a softcap)
11382            // a refusal is about the weights and will never change —
11383            // remember it instead of walking every layer again next
11384            // token.
11385            if declined && !self.o1_active() && self.attn_softcap == 0.0 {
11386                crate::gpu::graph_mark_unsupported();
11387            }
11388            graph_note(built.is_some(), gl, self.num_layers);
11389            if let Some(hh) = built {
11390                let dur = t_graph.elapsed();
11391                if std::env::var("CMF_GRAPH_PROF").is_ok() {
11392                    eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
11393                }
11394                if gl > 0 && gl < self.num_layers {
11395                    // Device prefix: the graph ran layers 0..gl and handed
11396                    // back the boundary hidden — the loop below owns the
11397                    // tail. The prefix layers' KV/state advanced on the
11398                    // device; the tail's advances on the host below. One
11399                    // boundary crossing per token.
11400                    h = hh;
11401                    tail_start = gl;
11402                } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
11403                    if !graph_trusted {
11404                        crate::gpu::graph_race_record(true, dur);
11405                    }
11406                    if !lg.is_empty() {
11407                        // Graph produced logits (final-norm + lm_head folded in) —
11408                        // pad/cap to vocab and hand them to the sampler directly.
11409                        lg.resize(self.vocab_size, 0.0);
11410                        if let Some(c) = self.final_softcap {
11411                            for l in lg.iter_mut() {
11412                                *l = c * (*l / c).tanh();
11413                            }
11414                        }
11415                        self.graph_logits = Some(lg);
11416                    }
11417                    return hh;
11418                }
11419                // Hopeless first graph token: discard it and fall through
11420                // to the normal path. Safe exactly here — the prompt KV is
11421                // still CPU-owned (chunked prefill), so recomputing this
11422                // position is exact; the mirror's extra row is never read
11423                // (the race just settled on the normal path).
11424            }
11425        }
11426        // KIMI-LINEAR HAS NO SPLIT BUG. The 2.6× reported from the
11427        // model rotation (12.2 tok/s on one card against 4.6 on two)
11428        // was a single measurement of a model whose arm arbitration is
11429        // borderline, and it did not survive repetition. Three runs an
11430        // arm, same binary, back to back:
11431        //   probe on : 1 GPU 9.5 / 5.7 / 5.9   2 GPU 7.8 / 13.0 / 13.3
11432        //   pinned   : 1 GPU 5.6 / 5.3 / 5.2   2 GPU 3.5 / 4.2 / 3.4
11433        // With the arms pinned the split costs about 1.45×, which is
11434        // what a layer split costs. With the probe free, TWO CARDS RUN
11435        // FASTER — because for this model the CPU arm wins some op
11436        // classes and the probe finds that.
11437        //
11438        // Two things do stand, and both are measured. The token graph
11439        // builds NOTHING here (`covered 0 of 14 layers [0..14)`), so
11440        // every layer walks per-op on either arm — that is where the
11441        // headroom is, not in the split. And this model's benchmark is
11442        // unusable without `CMF_GPU_PROBE=0`: the arbitration alone
11443        // moves it by more than 2×.
11444        //
11445        // Span runs (network split): the graph covers exactly [from..=upto]
11446        // — one submit per SEGMENT per token. No race: its state is global
11447        // and calibrated on full stacks, so spans take the graph only where
11448        // it is trusted by default (discrete adapters / CMF_GPU_WGPU_GRAPH).
11449        let span = from > 0 || upto.is_some();
11450        if span && graph_on && task_mask.is_none() && graph_trusted {
11451            let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
11452            let mut lg = Vec::new();
11453            let mut gl = 0usize;
11454            let span_res =
11455                self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
11456            let span_res = match span_res {
11457                Some(Ok(hh)) => Some(hh),
11458                Some(Err(())) => {
11459                    self.clear_sequence_state();
11460                    self.graph_failed
11461                        .store(true, std::sync::atomic::Ordering::Relaxed);
11462                    self.cancel
11463                        .store(true, std::sync::atomic::Ordering::Relaxed);
11464                    tracing::error!(
11465                        "span token graph failed after admission; sequence state cleared"
11466                    );
11467                    return vec![0.0; self.hidden_size];
11468                }
11469                None => None,
11470            };
11471            graph_note(span_res.is_some(), gl, upto_excl - from);
11472            if std::env::var("CMF_GPU_DEBUG").is_ok() {
11473                // How much of the span the graph actually covered. A
11474                // prefix of nothing means every layer walks per-op and
11475                // the split's extra cost is elsewhere.
11476                static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
11477                if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
11478                    eprintln!(
11479                        "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
11480                        upto_excl - from,
11481                        span_res.is_some()
11482                    );
11483                }
11484            }
11485            if let Some(hh) = span_res {
11486                if gl == upto_excl - from {
11487                    if !lg.is_empty() {
11488                        lg.resize(self.vocab_size, 0.0);
11489                        if let Some(c) = self.final_softcap {
11490                            for l in lg.iter_mut() {
11491                                *l = c * (*l / c).tanh();
11492                            }
11493                        }
11494                        self.graph_logits = Some(lg);
11495                    }
11496                    crate::gpu::set_layer(-1);
11497                    return hh;
11498                }
11499                // Partial device prefix of the span: CPU owns the tail.
11500                h = hh;
11501                tail_start = from + gl;
11502            }
11503        }
11504        let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
11505
11506        // A partial graph is an explicit GPU-prefix / CPU-tail split. Keep
11507        // the tail PURE host-side: letting its QTensor hooks re-enter the
11508        // residency arena streams every omitted layer through Vulkan and the
11509        // driver's freed-allocation cache can grow to the full model size
11510        // (25.4 GiB observed with a 14 GiB budget on Granite 30B Q8_2F).
11511        let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
11512        let automatic_gpu_prefix = self.automatic_gpu_prefix();
11513
11514        #[cfg(target_os = "macos")]
11515        let mut gpu_skip_until = 0usize;
11516        for li in tail_start.max(from)..self.num_layers {
11517            let _capacity_tail = automatic_gpu_prefix
11518                .filter(|&prefix| li >= prefix)
11519                .map(|_| crate::gpu::enter_cpu_scope());
11520            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU (CMF_GPU_LAYERS)
11521            if let Some(u) = upto {
11522                if li > u {
11523                    break;
11524                }
11525            }
11526            if let Some(mask) = task_mask {
11527                if !mask.layer_alive(li) {
11528                    continue; // dead layer: residual pass-through
11529                }
11530            }
11531            // Whole-block q1 token graph: a run of consecutive q1
11532            // layers — GDN and full attention — executes with one sync
11533            // per CPU attend instead of per op (macOS/Metal).
11534            #[cfg(target_os = "macos")]
11535            {
11536                if li < gpu_skip_until {
11537                    continue;
11538                }
11539                if task_mask.is_none() {
11540                    let end = self.q1_graph_gpu(li, upto, position, &mut h);
11541                    if self
11542                        .graph_failed
11543                        .load(std::sync::atomic::Ordering::Relaxed)
11544                    {
11545                        // The graph may have mutated device state before a
11546                        // command-buffer error. Never continue with a CPU
11547                        // tail or read a stale host mirror after admission.
11548                        return vec![0.0; self.hidden_size];
11549                    }
11550                    if end > li {
11551                        gpu_skip_until = end;
11552                        // Looped Transformer: the graph stopped at a loop
11553                        // boundary — apply final norm before the next iteration.
11554                        if self.is_loop_end(end - 1) && end < self.num_layers {
11555                            h = inference::rms_norm(
11556                                &h,
11557                                &self.weights.final_norm,
11558                                self.rms_eps,
11559                                self.norm_style,
11560                            );
11561                        }
11562                        continue;
11563                    }
11564                }
11565            }
11566
11567            let lw = &self.weights.layers[self.phys_layer(li)];
11568            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11569                if tp.parse::<usize>().ok() == Some(position) {
11570                    let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
11571                    eprintln!(
11572                        "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
11573                        h[0], h[1]
11574                    );
11575                }
11576            }
11577            // Norm into the pipeline scratch — the returning rms_norm
11578            // allocated twice per layer per token (roadmap §3 P0).
11579            inference::rms_norm_into(
11580                &h,
11581                &lw.input_norm,
11582                self.rms_eps,
11583                self.norm_style,
11584                &mut self.ws.n1,
11585            );
11586
11587            let attn_out = match &lw.attn {
11588                AttnKind::Mla(w) => {
11589                    let inv_freq_l = self.layer_inv_freq(li);
11590                    let rs = self.layer_rope_scale(li);
11591                    let eps = self.rms_eps;
11592                    let pool = self.pool.clone();
11593                    mla_attention(
11594                        w,
11595                        &self.ws.n1,
11596                        &mut self.kv_cache.layers[li],
11597                        position,
11598                        &inv_freq_l,
11599                        rs,
11600                        eps,
11601                        pool.as_deref(),
11602                    )
11603                }
11604                AttnKind::Linear(w) => {
11605                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
11606                    vmf_phase_forward(
11607                        &self.ws.n1,
11608                        w,
11609                        &cfg,
11610                        &mut self.kv_cache.layers[li].linear_state,
11611                        self.pool.as_deref(),
11612                    )
11613                }
11614                AttnKind::Kda(w) => {
11615                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
11616                    crate::linear_core::kda_forward(
11617                        &self.ws.n1,
11618                        w,
11619                        &cfg,
11620                        &mut self.kv_cache.layers[li].linear_state,
11621                        self.pool.as_deref(),
11622                    )
11623                }
11624                AttnKind::LinearGdn(w) => {
11625                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
11626                    gdn_forward(
11627                        &self.ws.n1,
11628                        w,
11629                        &cfg,
11630                        &mut self.kv_cache.layers[li].linear_state,
11631                        self.pool.as_deref(),
11632                    )
11633                }
11634                AttnKind::ShortConv(w) => {
11635                    let cfg = self
11636                        .short_conv_cfg
11637                        .expect("short-conv layer without short_conv_cfg");
11638                    short_conv_forward(
11639                        &self.ws.n1,
11640                        w,
11641                        &cfg,
11642                        &mut self.kv_cache.layers[li].linear_state,
11643                        self.pool.as_deref(),
11644                    )
11645                }
11646                AttnKind::Full {
11647                    wq,
11648                    wk,
11649                    wv,
11650                    wo,
11651                    q_norm,
11652                    k_norm,
11653                    output_gate,
11654                    softplus_gate,
11655                    bias,
11656                } if self.kv_cache.layers[li].o1_sealed() => {
11657                    // O(1) override: decode on the sealed Nyström state
11658                    // instead of the growing KV cache.
11659                    let inv_freq_l = self.layer_inv_freq(li);
11660                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
11661                    let cfg = QwenAttnCfg {
11662                        num_heads: self.layer_num_heads(li),
11663                        num_kv_heads: nkv_l,
11664                        head_dim: hd_l,
11665                        hidden_size: hs,
11666                        position,
11667                        inv_freq: &inv_freq_l,
11668                        rotary_dim: rd_l,
11669                        scale: self.attn_scale,
11670                        softcap: self.attn_softcap,
11671                        window: None,
11672                        v_norm: self.attn_v_norm,
11673                        qk_norm_after_rope: self.qk_norm_after_rope,
11674                        q_norm: q_norm.as_deref(),
11675                        k_norm: k_norm.as_deref(),
11676                        output_gate: *output_gate,
11677                        softplus_gate: softplus_gate
11678                            .as_ref()
11679                            .map(|(gate, per_head)| (gate, *per_head)),
11680                        rope_scale: self.layer_rope_scale(li),
11681                        bias: bias
11682                            .as_ref()
11683                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
11684                        rms_eps: eps,
11685                        norm_style: self.norm_style,
11686                        pool: pool.as_deref(),
11687                    };
11688                    attention::qwen_attention_nystrom(
11689                        &self.ws.n1,
11690                        wq,
11691                        wk,
11692                        wv,
11693                        wo,
11694                        &mut self.kv_cache.layers[li],
11695                        &cfg,
11696                    )
11697                }
11698                AttnKind::Full {
11699                    wq,
11700                    wk,
11701                    wv,
11702                    wo,
11703                    q_norm,
11704                    k_norm,
11705                    output_gate,
11706                    softplus_gate,
11707                    bias,
11708                } => 'attn: {
11709                    // wgpu token-graph attention (opt-in): whole sub-block in
11710                    // one submit, device K/V mirror. q1 only, no gate/bias/mask.
11711                    if graph_on
11712                        && !*output_gate
11713                        && softplus_gate.is_none()
11714                        && self.attention_heads_per_layer.is_none()
11715                        && bias.is_none()
11716                        && task_mask.is_none()
11717                    {
11718                        let inv_freq_l = self.layer_inv_freq(li);
11719                        let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
11720                        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
11721                        if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
11722                            wq.mapped_q1(),
11723                            wk.mapped_q1(),
11724                            wv.mapped_q1(),
11725                            wo.mapped_q1(),
11726                        ) {
11727                            let gm = gm.clone();
11728                            let mut out = vec![0f32; hs];
11729                            let cache = &self.kv_cache.layers[li];
11730                            if crate::gpu::attn_dropin(
11731                                &gm,
11732                                self.graph_kv_id,
11733                                li,
11734                                &self.ws.n1,
11735                                qi,
11736                                ki,
11737                                vi,
11738                                oi,
11739                                q_norm.as_deref(),
11740                                k_norm.as_deref(),
11741                                self.qk_norm_after_rope,
11742                                &inv_freq_l,
11743                                nh,
11744                                nkv_l,
11745                                hd_l,
11746                                rd_l,
11747                                hs,
11748                                position,
11749                                self.kv_cache.max_seq_len,
11750                                gemma,
11751                                eps as f32,
11752                                cache.k_heads(),
11753                                cache.v_heads(),
11754                                &mut out,
11755                            ) {
11756                                break 'attn out;
11757                            }
11758                        }
11759                    }
11760                    let masked = task_mask
11761                        .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
11762                        .unwrap_or(false);
11763                    let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
11764                    match (masked, f32_view) {
11765                        // Historical masked path (f32 slices; the loader
11766                        // keeps masked models in f32).
11767                        (true, (Some(q), Some(k), Some(v), Some(o))) => {
11768                            let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
11769                            attention::multi_head_attention(
11770                                &self.ws.n1,
11771                                q,
11772                                k,
11773                                v,
11774                                o,
11775                                &mut self.kv_cache.layers[li],
11776                                self.num_heads,
11777                                self.num_kv_heads,
11778                                self.head_dim,
11779                                self.hidden_size,
11780                                position,
11781                                &active_heads,
11782                                &self.inv_freq,
11783                            )
11784                        }
11785                        (masked, _) => {
11786                            if masked {
11787                                tracing::warn!(
11788                                    "layer {li}: head mask on quantized weights not \
11789                                     supported yet — executing dense"
11790                                );
11791                            }
11792                            let inv_freq_l = self.layer_inv_freq(li);
11793                            let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
11794                            let cfg = QwenAttnCfg {
11795                                num_heads: self.layer_num_heads(li),
11796                                num_kv_heads: nkv_l,
11797                                head_dim: hd_l,
11798                                hidden_size: hs,
11799                                position,
11800                                inv_freq: &inv_freq_l,
11801                                rotary_dim: rd_l,
11802                                scale: self.attn_scale,
11803                                softcap: self.attn_softcap,
11804                                window: self.layer_window(li),
11805                                v_norm: self.attn_v_norm,
11806                                qk_norm_after_rope: self.qk_norm_after_rope,
11807                                q_norm: q_norm.as_deref(),
11808                                k_norm: k_norm.as_deref(),
11809                                output_gate: *output_gate,
11810                                softplus_gate: softplus_gate
11811                                    .as_ref()
11812                                    .map(|(gate, per_head)| (gate, *per_head)),
11813                                rope_scale: self.layer_rope_scale(li),
11814                                bias: bias
11815                                    .as_ref()
11816                                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
11817                                rms_eps: eps,
11818                                norm_style: self.norm_style,
11819                                pool: pool.as_deref(),
11820                            };
11821                            attention::qwen_attention(
11822                                &self.ws.n1,
11823                                wq,
11824                                wk,
11825                                wv,
11826                                wo,
11827                                &mut self.kv_cache.layers[li],
11828                                &cfg,
11829                            )
11830                        }
11831                    }
11832                }
11833            };
11834            // Gemma sandwich norm: normalize the attention branch before
11835            // it joins the residual stream.
11836            let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
11837                Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
11838                None => attn_out,
11839            };
11840            let lw = &self.weights.layers[self.phys_layer(li)];
11841            inference::add_rmsnorm_fused_into(
11842                &mut h,
11843                &attn_out,
11844                &lw.post_norm,
11845                self.rms_eps,
11846                self.norm_style,
11847                &mut self.ws.p1,
11848            );
11849            let mut attn_out = attn_out;
11850            attention::recycle_buf(&mut attn_out);
11851            let post_normed = &self.ws.p1;
11852
11853            let ffn_masked = task_mask
11854                .map(|m| m.ffn_active_count(li) < self.intermediate_size)
11855                .unwrap_or(false);
11856            // One masked dense CONTRACT, dispatched by cost. The
11857            // activation-zeroing arm (the batched sweep's, validated
11858            // against the replica to 0.8%) computes the FULL fused FFN
11859            // and zeroes the dead — right whenever most neurons live.
11860            // The sparse arm reads ONLY active rows and down columns —
11861            // per-row dots are slower per element than the fused kernel,
11862            // so it pays only once the mask is deep enough. The 0.5
11863            // crossover is first-principles (fused kernels run ~2x the
11864            // per-row dot throughput); a shallow specialist (95% alive)
11865            // stays fused, a --target-sparsity bake flips arms on its
11866            // own weight.
11867            let ffn_out = match (ffn_masked, &lw.ffn) {
11868                // A defragged tube layer answers its own mask: the core
11869                // always runs, each tube runs when its bit is on, and
11870                // the tubes that are off are never read from the mmap.
11871                (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
11872                    let row = task_mask
11873                        .and_then(|tm| tm.ffn_masks.get(li))
11874                        .map(|v| v.as_slice());
11875                    tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
11876                }
11877                (true, FfnKind::Dense(d)) => {
11878                    let tm = task_mask.unwrap();
11879                    let alive = tm.ffn_active_count(li);
11880                    let deep = alive * 2 <= self.intermediate_size;
11881                    if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
11882                        let active = tm.ffn_active_indices(li);
11883                        sparse_ffn_quant(
11884                            d,
11885                            post_normed,
11886                            &active,
11887                            self.hidden_size,
11888                            self.pool.as_deref(),
11889                        )
11890                    } else if deep
11891                        && let (Some(g), Some(u), Some(dn)) = (
11892                            d.gate_proj.as_f32(),
11893                            d.up_proj.as_f32(),
11894                            d.down_proj.as_f32(),
11895                        )
11896                    {
11897                        let active = tm.ffn_active_indices(li);
11898                        inference::sparse_ffn_forward(
11899                            post_normed,
11900                            g,
11901                            u,
11902                            dn,
11903                            self.hidden_size,
11904                            self.intermediate_size,
11905                            &active,
11906                            self.pool.as_deref(),
11907                        )
11908                    } else {
11909                        let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
11910                        dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
11911                    }
11912                }
11913                (true, FfnKind::Moe(m)) => {
11914                    // MoE is sparse by expert selection; a task mask
11915                    // narrows the ROUTABLE set via its expert fields
11916                    // (spec §5) when it carries them.
11917                    let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
11918                    ffn_forward(
11919                        &lw.ffn,
11920                        post_normed,
11921                        self.pool.as_deref(),
11922                        allowed.as_deref(),
11923                    )
11924                }
11925                (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
11926                    dm,
11927                    post_normed,
11928                    &h,
11929                    self.rms_eps,
11930                    self.norm_style,
11931                    self.pool.as_deref(),
11932                ),
11933                (false, _) => match &lw.ffn {
11934                    FfnKind::DenseMoe(dm) => dense_moe_ffn(
11935                        dm,
11936                        post_normed,
11937                        &h,
11938                        self.rms_eps,
11939                        self.norm_style,
11940                        self.pool.as_deref(),
11941                    ),
11942                    _ => {
11943                        let allowed = match (&lw.ffn, task_mask) {
11944                            (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
11945                            _ => None,
11946                        };
11947                        ffn_forward(
11948                            &lw.ffn,
11949                            post_normed,
11950                            self.pool.as_deref(),
11951                            allowed.as_deref(),
11952                        )
11953                    }
11954                },
11955            };
11956            let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
11957                Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
11958                None => ffn_out,
11959            };
11960            for (i, &f) in ffn_out.iter().enumerate() {
11961                h[i] += f;
11962            }
11963            let mut ffn_out = ffn_out;
11964            attention::recycle_buf(&mut ffn_out);
11965
11966            // Gemma-4: the layer output is scaled by a learned scalar.
11967            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
11968                for v in h.iter_mut() {
11969                    *v *= sc;
11970                }
11971            }
11972
11973            // Looped Transformer: apply final norm at the end of each loop iteration.
11974            // Nanbeige 4.2: after layer 21 (virtual), apply norm before looping back to layer 0.
11975            if self.is_loop_end(li) && li + 1 < self.num_layers {
11976                h = inference::rms_norm(
11977                    &h,
11978                    &self.weights.final_norm,
11979                    self.rms_eps,
11980                    self.norm_style,
11981                );
11982            }
11983
11984            // Dynamic routing φ capture (on-policy): the
11985            // EMA of the post-residual hidden at the router's phi_layer,
11986            // updated as the context evolves during decode.
11987            if self.dyn_phi_layer == Some(li) {
11988                self.update_dyn_phi(&h);
11989            }
11990        }
11991        crate::gpu::set_layer(-1); // layers done — lm_head outside layer-split
11992        if let Some(t) = t_race_cpu {
11993            crate::gpu::graph_race_record(false, t.elapsed());
11994        }
11995
11996        h
11997    }
11998
11999    /// EMA of φ at the router layer (rolling, weight 0.2 = ~5-token
12000    /// horizon). First observation seeds it exactly.
12001    fn update_dyn_phi(&mut self, h: &[f32]) {
12002        const A: f32 = 0.2;
12003        if self.dyn_phi_ema.len() != h.len() {
12004            self.dyn_phi_ema = vec![0.0; h.len()];
12005            self.dyn_phi_seen = 0;
12006        }
12007        if self.dyn_phi_seen == 0 {
12008            self.dyn_phi_ema.copy_from_slice(h);
12009        } else {
12010            for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
12011                *e = (1.0 - A) * *e + A * v;
12012            }
12013        }
12014        self.dyn_phi_seen += 1;
12015    }
12016
12017    /// Current router φ (EMA at phi_layer); empty until first capture.
12018    pub fn dyn_phi(&self) -> &[f32] {
12019        &self.dyn_phi_ema
12020    }
12021
12022    /// Enable/disable φ capture at the router layer, reset the EMA.
12023    pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
12024        self.dyn_phi_layer = layer;
12025        self.dyn_phi_ema.clear();
12026        self.dyn_phi_seen = 0;
12027    }
12028
12029    /// Skills eligible for dynamic switching: (index, id, phi_layer).
12030    pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
12031        let Some(model) = &self.model else {
12032            return Vec::new();
12033        };
12034        model
12035            .header
12036            .skills
12037            .iter()
12038            .enumerate()
12039            .filter_map(|(i, sk)| {
12040                let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
12041                let sel = sk.selection.as_ref()?;
12042                (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
12043            })
12044            .collect()
12045    }
12046
12047    /// Index of the currently overlaid skill (None = backbone).
12048    pub fn active_skill(&self) -> Option<usize> {
12049        self.dyn_active
12050    }
12051
12052    /// Enable dynamic per-token skill routing: build the hysteresis
12053    /// router from the container's routable skills, start φ capture at
12054    /// their (shared) phi_layer. Returns the number of routable skills
12055    /// (0 = nothing to route; router stays off). Idempotent.
12056    pub fn enable_dynamic_routing(&mut self) -> usize {
12057        use crate::swarm::{DynRouter, RoutableSkill};
12058        let Some(model) = self.model.clone() else {
12059            return 0;
12060        };
12061        // A blend materialized f32 working tensors into the layers; there
12062        // is no single skill index to revert from → refuse (honest).
12063        if self.dyn_blend_loaded {
12064            tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
12065            return 0;
12066        }
12067        // A statically-overlaid skill that is NOT FFN-eligible can't be
12068        // cheaply reverted at generation start → refuse rather than
12069        // silently keep it overlaid.
12070        if let Some(a) = self.dyn_active {
12071            if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
12072                tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
12073                return 0;
12074            }
12075        }
12076        let hidden = self.hidden_size;
12077        let mut skills = Vec::new();
12078        for (idx, id, _phi) in self.dynamic_skills() {
12079            if let Some(sel) = model.header.skills[idx].selection.as_ref() {
12080                if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
12081                    skills.push(rs);
12082                }
12083            }
12084        }
12085        if skills.is_empty() {
12086            return 0;
12087        }
12088        // Skills should share a phi_layer; warn (not fail) if they don't.
12089        let phi = skills[0].phi_layer;
12090        if skills.iter().any(|s| s.phi_layer != phi) {
12091            tracing::warn!("routable skills disagree on phi_layer; using {phi}");
12092        }
12093        let n = skills.len();
12094        self.set_dyn_phi_layer(Some(phi));
12095        self.dyn_router = Some(DynRouter::new(skills));
12096        n
12097    }
12098
12099    /// Human-readable switch log from the last dynamic-routed generation.
12100    pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
12101        self.dyn_router
12102            .as_ref()
12103            .map(|r| r.switches.clone())
12104            .unwrap_or_default()
12105    }
12106
12107    /// LM head: hidden → logits [vocab_size]. The dominant matvec of
12108    /// every decode step — row-parallel on the worker pool.
12109    fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
12110        let rows = self.weights.lm_head.rows();
12111        let mut logits = attention::take_buf(rows.min(self.vocab_size));
12112        self.weights
12113            .lm_head
12114            .matvec(hidden, &mut logits, self.pool.as_deref());
12115        logits.resize(self.vocab_size, 0.0);
12116        if let Some(m) = self.logit_multiplier {
12117            for l in logits.iter_mut() {
12118                *l *= m;
12119            }
12120        }
12121        if let Some(c) = self.final_softcap {
12122            for l in logits.iter_mut() {
12123                *l = c * (*l / c).tanh();
12124            }
12125        }
12126        if let Some(cm) = self.head_clusters.as_ref() {
12127            self.hierarchical_head_logprobs(hidden, cm, &mut logits);
12128        }
12129        logits
12130    }
12131
12132    /// Two-level head (Cortiq Embryo): in place, logits[v] ← log p(v) =
12133    /// (lc[c] − lse(lc)) + (logit[v] − lse over v's cluster block), c = v / S.
12134    fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
12135        let h = hidden.len();
12136        let ncl = cm.len() / h.max(1);
12137        if ncl == 0 || logits.len() % ncl != 0 {
12138            return;
12139        }
12140        let cs = logits.len() / ncl;
12141        // cluster logits + log-softmax
12142        let mut lc = vec![0.0f32; ncl];
12143        for c in 0..ncl {
12144            let row = &cm[c * h..(c + 1) * h];
12145            let mut s = 0.0f32;
12146            for j in 0..h {
12147                s += row[j] * hidden[j];
12148            }
12149            lc[c] = s;
12150        }
12151        let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
12152        let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
12153        for c in 0..ncl {
12154            let blk = &mut logits[c * cs..(c + 1) * cs];
12155            let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
12156            let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
12157            let add = lc[c] - lse - bl;
12158            for v in blk.iter_mut() {
12159                *v += add;
12160            }
12161        }
12162    }
12163
12164    /// Prefill `ids` and return the next-token logits — what the model
12165    /// would predict next, WITHOUT committing to generation (introspection
12166    /// for `cortiq explain`). Clears and repopulates the KV cache; leaves
12167    /// the active overlay untouched.
12168    pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
12169        self.clear_sequence_state();
12170        // This helper is used by the pooled classification endpoint, where
12171        // every request is a fresh sequence. The shared reset also clears the
12172        // wgpu token graph's device-side recurrent state.
12173        crate::gpu::graph_race_begin_generation();
12174        if task_mask.is_none() {
12175            self.o1_begin();
12176        }
12177        let mut hidden = vec![0.0f32; self.hidden_size];
12178        for (pos, &id) in ids.iter().enumerate() {
12179            let emb = self.embed_single(id);
12180            hidden = self.forward_layers(&emb, pos, task_mask);
12181        }
12182        if let Err(err) = self.o1_seal_checked() {
12183            self.o1_fail(err);
12184        }
12185        inference::rms_norm_into(
12186            &hidden,
12187            &self.weights.final_norm,
12188            self.rms_eps,
12189            self.norm_style,
12190            &mut self.ws.n1,
12191        );
12192        self.lm_head_forward(&self.ws.n1)
12193    }
12194}
12195
12196/// Convenience: deterministic tiny pipeline for tests.
12197pub fn create_test_pipeline(
12198    hidden_size: usize,
12199    intermediate_size: usize,
12200    num_heads: usize,
12201    num_kv_heads: usize,
12202    head_dim: usize,
12203    num_layers: usize,
12204    vocab_size: usize,
12205) -> Pipeline {
12206    // Small pseudo-random weights: constant weights make attention
12207    // degenerate and hide indexing bugs.
12208    let synth = |n: usize, salt: usize| -> Vec<f32> {
12209        (0..n)
12210            .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
12211            .collect()
12212    };
12213    let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
12214        QTensor::from_f32(synth(rows * cols, salt), rows, cols)
12215    };
12216    let layer_weights: Vec<LayerWeights> = (0..num_layers)
12217        .map(|li| LayerWeights {
12218            input_norm: vec![1.0; hidden_size],
12219            post_norm: vec![1.0; hidden_size],
12220            attn_out_norm: None,
12221            ffn_out_norm: None,
12222            layer_scale: None,
12223            ffn: FfnKind::Dense(DenseFfn {
12224                gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
12225                up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
12226                down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
12227                act: Act::Silu,
12228                down_t: None,
12229                segs: Vec::new(),
12230            }),
12231            attn: AttnKind::Full {
12232                bias: None,
12233                wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
12234                wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
12235                wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
12236                wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
12237                q_norm: None,
12238                k_norm: None,
12239                output_gate: false,
12240                softplus_gate: None,
12241            },
12242        })
12243        .collect();
12244
12245    Pipeline::new(
12246        Tokenizer::byte_level(),
12247        PipelineWeights {
12248            embed_tokens: qt(vocab_size, hidden_size, 100),
12249            layers: layer_weights,
12250            lm_head: qt(vocab_size, hidden_size, 200),
12251            final_norm: vec![1.0; hidden_size],
12252        },
12253        hidden_size,
12254        intermediate_size,
12255        num_heads,
12256        num_kv_heads,
12257        head_dim,
12258        num_layers,
12259        num_layers, // physical_layers = num_layers (non-looped)
12260        false,      // loop_final_norm
12261        vocab_size,
12262        1e-6,
12263        10_000.0,
12264        NormStyle::Qwen,
12265        4096,
12266        SamplerConfig {
12267            seed: Some(42),
12268            ..Default::default()
12269        },
12270    )
12271}
12272
12273/// Batched dense-FFN: gate/up/down via matmat (element-wise the same
12274/// math as b × dense_ffn — the same dot kernels).
12275/// One mask bit, LSB-first per byte — `TaskMask::ffn_active_indices`'s
12276/// convention.
12277#[inline]
12278fn mask_bit(row: &[u8], j: usize) -> bool {
12279    (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
12280}
12281
12282/// Zero the CLOSED neurons' activations in a [rows × inter] panel — the
12283/// masked-inference fast path's whole trick: full fused quant compute,
12284/// then the mask lands on the ACTIVATIONS, which is arithmetically the
12285/// pruned network without touching a quantized weight byte. Whole open
12286/// bytes (0xFF = 8 open neurons) skip in one test.
12287/// `CMF_FFN_MASK_GAIN` — Patent 12 FIG. 4, variance-preserving
12288/// rescaling: truncation removes a share of the layer's output energy,
12289/// so the survivors are scaled up to put the variance back where the
12290/// downstream norm expects it. A scalar here; per layer it is
12291/// `sqrt(total energy / kept energy)`.
12292fn mask_gain() -> f32 {
12293    static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
12294    *G.get_or_init(|| {
12295        std::env::var("CMF_FFN_MASK_GAIN")
12296            .ok()
12297            .and_then(|v| v.parse().ok())
12298            .unwrap_or(1.0)
12299    })
12300}
12301
12302fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
12303    // With CMF_FFN_MEANFILL a closed neuron contributes its average
12304    // instead of nothing — same bytes read, one constant restored.
12305    let fill = meanfill().and_then(|(i, v)| {
12306        let li = crate::gpu::cur_layer();
12307        (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
12308    });
12309    for r in 0..rows {
12310        let base = r * inter;
12311        for (bi, &byte) in row.iter().enumerate() {
12312            if byte == 0xFF {
12313                continue;
12314            }
12315            let j0 = bi * 8;
12316            for bit in 0..8 {
12317                let j = j0 + bit;
12318                if j < inter && byte & (1 << bit) == 0 {
12319                    g[base + j] = fill.map_or(0.0, |f| f[j]);
12320                }
12321            }
12322        }
12323    }
12324    let gain = mask_gain();
12325    if gain != 1.0 {
12326        for v in g[..rows * inter].iter_mut() {
12327            *v *= gain;
12328        }
12329    }
12330}
12331
12332/// True when neuron `i`'s bit is set (no mask = everything runs).
12333#[inline]
12334fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
12335    row.is_none_or(|r| mask_bit(r, i))
12336}
12337
12338/// Every bit below `n` set — the common case for a tube file's CORE,
12339/// where only the tube bits vary per task.
12340fn all_bits_on(row: &[u8], n: usize) -> bool {
12341    (0..n).all(|i| mask_bit(row, i))
12342}
12343
12344/// `CMF_TUBE_TOPK` — how many tubes a TOKEN may open (0 = the task mask
12345/// decides alone). This is the dense FFN read as a mixture: the tubes
12346/// are the experts a k-means over `gate_proj` rows found, and the token
12347/// picks among them. `CMF_TUBE_SCORE=gate` scores a tube by its own
12348/// gate (realizable: only `up`/`down` of the losers go unread),
12349/// `=oracle` scores by the true `silu(gate)·up` mass (the ceiling —
12350/// only `down` is saved, and the selection has read what it predicts).
12351fn tube_topk() -> usize {
12352    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12353    *K.get_or_init(|| {
12354        std::env::var("CMF_TUBE_TOPK")
12355            .ok()
12356            .and_then(|v| v.parse().ok())
12357            .unwrap_or(0)
12358    })
12359}
12360
12361fn tube_score_oracle() -> bool {
12362    static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12363    *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
12364}
12365
12366/// The routed arm of `tube_ffn`: a token opens only its best `k` tubes.
12367/// At `b == 1` (decode) the losers are genuinely never read — that is
12368/// the speed. At `b > 1` (the scoring sweep) every tube is computed and
12369/// the losers' activations are zeroed instead: same arithmetic, so the
12370/// perplexity is the routed model's, measured without a per-token
12371/// gather in the middle of a GEMM.
12372fn tube_ffn_routed(
12373    d: &DenseFfn,
12374    xs: &[f32],
12375    b: usize,
12376    pool: Option<&Pool>,
12377    mask_row: Option<&[u8]>,
12378    k: usize,
12379) -> Vec<f32> {
12380    let hidden = d.down_proj.rows();
12381    let core = d.gate_proj.rows();
12382    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
12383    let mut out = match (b, core_full, mask_row) {
12384        (1, true, _) => dense_ffn(d, xs, pool),
12385        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
12386        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
12387        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
12388    };
12389    let cand: Vec<usize> = (0..d.segs.len())
12390        .filter(|&i| tube_bit(mask_row, d.segs[i].start))
12391        .collect();
12392    if cand.is_empty() {
12393        return out;
12394    }
12395    // gate (and, where the score or the batch needs it, up) per tube.
12396    // The SCORE is taken at the point the serving path could take it:
12397    // off the gate alone, or off the finished activation for the oracle.
12398    let oracle = tube_score_oracle();
12399    let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
12400    let mut scores = vec![0f32; b * cand.len()];
12401    for (ci, &i) in cand.iter().enumerate() {
12402        let seg = &d.segs[i];
12403        let w = seg.width;
12404        let mut g = vec![0.0f32; b * w];
12405        if b == 1 {
12406            seg.gate.matvec(xs, &mut g, pool);
12407        } else {
12408            seg.gate.matmat(xs, b, &mut g, pool);
12409        }
12410        for v in g.iter_mut() {
12411            *v = Act::Silu.combine(*v, 1.0);
12412        }
12413        if !oracle {
12414            for t in 0..b {
12415                scores[t * cand.len() + ci] =
12416                    g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
12417            }
12418        }
12419        if oracle || b > 1 {
12420            let mut u = vec![0.0f32; b * w];
12421            if b == 1 {
12422                seg.up.matvec(xs, &mut u, pool);
12423            } else {
12424                seg.up.matmat(xs, b, &mut u, pool);
12425            }
12426            for (a, &v) in g.iter_mut().zip(u.iter()) {
12427                *a *= v;
12428            }
12429            if oracle {
12430                for t in 0..b {
12431                    scores[t * cand.len() + ci] =
12432                        g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
12433                }
12434            }
12435        }
12436        acts.push(g);
12437    }
12438    // per-token scores and the winners
12439    let keep = k.min(cand.len());
12440    let mut scratch: Vec<f32> = Vec::new();
12441    for t in 0..b {
12442        let mut sc: Vec<(f32, usize)> = (0..cand.len())
12443            .map(|ci| (scores[t * cand.len() + ci], ci))
12444            .collect();
12445        sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
12446        let mut alive = vec![false; cand.len()];
12447        for &(_, ci) in sc.iter().take(keep) {
12448            alive[ci] = true;
12449        }
12450        if b > 1 {
12451            for (ci, a) in acts.iter_mut().enumerate() {
12452                if !alive[ci] {
12453                    let w = d.segs[cand[ci]].width;
12454                    a[t * w..(t + 1) * w].fill(0.0);
12455                }
12456            }
12457        } else {
12458            // decode: finish only the winners — the losers' up/down
12459            // (and, with the gate score, everything but their gate)
12460            // are never touched.
12461            for (ci, &i) in cand.iter().enumerate() {
12462                if !alive[ci] {
12463                    continue;
12464                }
12465                let seg = &d.segs[i];
12466                let w = seg.width;
12467                let g = &mut acts[ci];
12468                if !tube_score_oracle() {
12469                    scratch.clear();
12470                    scratch.resize(w, 0.0);
12471                    seg.up.matvec(xs, &mut scratch, pool);
12472                    for (a, &v) in g.iter_mut().zip(scratch.iter()) {
12473                        *a *= v;
12474                    }
12475                }
12476                let mut acc = vec![0.0f32; hidden];
12477                seg.down.matvec(g, &mut acc, pool);
12478                for (o, a) in out.iter_mut().zip(&acc) {
12479                    *o += *a;
12480                }
12481            }
12482        }
12483    }
12484    if b > 1 {
12485        for (ci, &i) in cand.iter().enumerate() {
12486            let seg = &d.segs[i];
12487            let mut acc = vec![0.0f32; b * hidden];
12488            seg.down.matmat(&acts[ci], b, &mut acc, pool);
12489            for (o, a) in out.iter_mut().zip(&acc) {
12490                *o += *a;
12491            }
12492        }
12493    }
12494    out
12495}
12496
12497/// FFN of a defragged tube layer: the always-on core plus the tubes the
12498/// task mask switches on. Each tube is a normal tensor triple, so the
12499/// same kernels run it and an inactive tube's bytes are never read —
12500/// that is the whole point of the defrag (a scattered mask cannot skip
12501/// bytes; a contiguous one is just a smaller matrix).
12502fn tube_ffn(
12503    d: &DenseFfn,
12504    xs: &[f32],
12505    b: usize,
12506    pool: Option<&Pool>,
12507    mask_row: Option<&[u8]>,
12508) -> Vec<f32> {
12509    if tube_topk() > 0 {
12510        return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
12511    }
12512    let hidden = d.down_proj.rows();
12513    let core = d.gate_proj.rows();
12514    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
12515    let mut out = match (b, core_full, mask_row) {
12516        (1, true, _) => dense_ffn(d, xs, pool),
12517        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
12518        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
12519        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
12520    };
12521    TUBE_SCRATCH.with(|sc| {
12522        let mut sc = sc.borrow_mut();
12523        let [g, u, acc] = &mut *sc;
12524        for seg in &d.segs {
12525            if !tube_bit(mask_row, seg.start) {
12526                continue;
12527            }
12528            let w = seg.width;
12529            g.resize(b * w, 0.0);
12530            if b == 1
12531                && d.act == Act::Silu
12532                && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
12533            {
12534                // g holds silu(gate)·up.
12535            } else {
12536                u.resize(b * w, 0.0);
12537                if b == 1 {
12538                    QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
12539                } else {
12540                    seg.gate.matmat(xs, b, g, pool);
12541                    seg.up.matmat(xs, b, u, pool);
12542                }
12543                for i in 0..b * w {
12544                    g[i] = d.act.combine(g[i], u[i]);
12545                }
12546            }
12547            acc.resize(b * hidden, 0.0);
12548            acc.fill(0.0);
12549            if b == 1 {
12550                seg.down.matvec(g, acc, pool);
12551            } else {
12552                seg.down.matmat(g, b, acc, pool);
12553            }
12554            for (o, a) in out.iter_mut().zip(acc.iter()) {
12555                *o += *a;
12556            }
12557        }
12558        out
12559    })
12560}
12561
12562thread_local! {
12563    /// gate / up / down-accumulator scratch for the tube loop — a tube
12564    /// runs once per layer per token, and a fresh Vec each time is a
12565    /// malloc per tube per layer per token.
12566    static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
12567        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
12568}
12569
12570fn dense_ffn_batch(
12571    d: &DenseFfn,
12572    xs: &[f32],
12573    b: usize,
12574    pool: Option<&Pool>,
12575    mask_row: Option<&[u8]>,
12576) -> Vec<f32> {
12577    let inter = d.gate_proj.rows();
12578    let hidden = d.down_proj.rows();
12579    // Fused on-device SwiGLU when the device is in play: three separate
12580    // `matmat` calls are three round trips per layer, and the gate/up
12581    // panels (b × inter — 22 MB each at a 512-token chunk) cross the bus
12582    // twice for nothing. The kernel already existed for the image DiT;
12583    // the LLM prefill was simply never wired to it. A task mask needs the
12584    // activations on the host between the halves, so it keeps the CPU
12585    // arm below.
12586    if mask_row.is_none()
12587        && d.act == Act::Silu
12588        && b >= 32
12589        && crate::gpu::enabled_here()
12590        && !crate::gpu::mm_killed()
12591        // The refit pass needs this layer's activations on the host; the
12592        // fused chain keeps them on the device. Refusing it here costs
12593        // one round trip and keeps every GEMM on the card — the
12594        // alternative was running the whole calibration on the CPU.
12595        && refit_dir().is_none()
12596        // Same for the mass/hit probes. The accumulator at the bottom of
12597        // this function only sees `g` when `g` came back to the host, so
12598        // a fused batch would leave it summing nothing — a probe that
12599        // reports zeros rather than failing, which is worse.
12600        && !ffn_probe_active()
12601    {
12602        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
12603            d.gate_proj.mapped_q4t(),
12604            d.up_proj.mapped_q4t(),
12605            d.down_proj.mapped_q4t(),
12606        ) {
12607            let mut out = vec![0.0f32; b * hidden];
12608            if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
12609                return out;
12610            }
12611        }
12612        // The q4tp twin (same kernel family, scale from the row ladder) —
12613        // the DiT has run it in production since the pipeline containers;
12614        // the LLM prefill was simply never wired to it, so a q4tp model's
12615        // prefill panels stayed on the CPU.
12616        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
12617            d.gate_proj.mapped_q4tp(),
12618            d.up_proj.mapped_q4tp(),
12619            d.down_proj.mapped_q4tp(),
12620        ) {
12621            let mut out = vec![0.0f32; b * hidden];
12622            if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
12623                return out;
12624            }
12625        }
12626    }
12627    let mut g = vec![0.0f32; b * inter];
12628    d.gate_proj.matmat(xs, b, &mut g, pool);
12629    let mut u = vec![0.0f32; b * inter];
12630    d.up_proj.matmat(xs, b, &mut u, pool);
12631    if gate_topk() > 0 && d.act == Act::Silu {
12632        for t in 0..b {
12633            let row = &mut g[t * inter..(t + 1) * inter];
12634            for v in row.iter_mut() {
12635                *v = Act::Silu.combine(*v, 1.0);
12636            }
12637            keep_top_k(row, gate_topk());
12638        }
12639        for i in 0..b * inter {
12640            g[i] *= u[i];
12641        }
12642    } else {
12643        for i in 0..b * inter {
12644            g[i] = d.act.combine(g[i], u[i]);
12645        }
12646    }
12647    if let Some(row) = mask_row {
12648        zero_masked_cols(&mut g, b, inter, row);
12649    }
12650    if oracle_topk() > 0 {
12651        for t in 0..b {
12652            keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
12653        }
12654    }
12655    let mut out = vec![0.0f32; b * hidden];
12656    d.down_proj.matmat(&g, b, &mut out, pool);
12657    if refit_dir().is_some() {
12658        let li = crate::gpu::cur_layer();
12659        if li >= 0 {
12660            refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
12661        }
12662    }
12663    // The DTG-MA probe, on the batched path: one prefill sweep gives the
12664    // same per-neuron statistic the per-position probe does, and on a 27B
12665    // that is minutes instead of hours.
12666    FFN_PROBE.with(|pr| {
12667        if let Some(acc) = pr.borrow_mut().as_mut() {
12668            let li = crate::gpu::cur_layer();
12669            if li < 0 {
12670                return;
12671            }
12672            let Some(row) = acc.get_mut(li as usize) else {
12673                return;
12674            };
12675            let sq = probe_sq();
12676            for t in 0..b {
12677                for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
12678                    *a += if sq {
12679                        (v as f64) * (v as f64)
12680                    } else {
12681                        (v as f64).abs()
12682                    };
12683                }
12684            }
12685        }
12686    });
12687    out
12688}
12689
12690/// Batched MoE-FFN: router batched, positions are GROUPED by expert —
12691/// an expert's weights are read once for all its positions in the chunk
12692/// (the main prefill-GEMM win on MoE: 960MB/token of 35B experts).
12693/// Accumulate per-channel activation energy for `CMF_RMS_TRACE`.
12694fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
12695    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12696    static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12697    let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
12698    let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
12699    if (!on && !dump) || b == 0 {
12700        return;
12701    }
12702    let hidden = xs.len() / b;
12703    if on {
12704        let mut acc = m.act_sq.borrow_mut();
12705        if acc.len() < hidden {
12706            acc.resize(hidden, 0.0);
12707        }
12708        for t in 0..b {
12709            let row = &xs[t * hidden..(t + 1) * hidden];
12710            for (a, &v) in acc.iter_mut().zip(row) {
12711                *a += (v as f64) * (v as f64);
12712            }
12713        }
12714    }
12715    if dump {
12716        // Cap the capture: the covariance needs a few thousand rows, and a
12717        // whole prefill of every layer would be gigabytes for no extra rank.
12718        let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
12719            .ok()
12720            .and_then(|v| v.parse().ok())
12721            .unwrap_or(4096);
12722        let mut rows = m.act_rows.borrow_mut();
12723        if rows.len() < cap * hidden {
12724            let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
12725            rows.extend_from_slice(&xs[..take * hidden]);
12726        }
12727    }
12728}
12729
12730/// Send-able cursor over a Vec-of-Vecs: each pool worker writes only its
12731/// own slots (disjoint by construction in the caller).
12732#[derive(Clone, Copy)]
12733struct SendVecs(*mut Vec<f32>);
12734unsafe impl Send for SendVecs {}
12735unsafe impl Sync for SendVecs {}
12736impl SendVecs {
12737    #[inline]
12738    fn at(self, i: usize) -> *mut Vec<f32> {
12739        unsafe { self.0.add(i) }
12740    }
12741}
12742
12743fn moe_ffn_batch(
12744    m: &MoeFfn,
12745    xs: &[f32],
12746    b: usize,
12747    hidden: usize,
12748    pool: Option<&Pool>,
12749    allowed: Option<&[bool]>,
12750) -> Vec<f32> {
12751    accumulate_act(m, xs, b);
12752    let ne = m.experts.len();
12753    let mut logits = vec![0.0f32; b * ne];
12754    match &m.resonance {
12755        Some(r) => {
12756            let hdim = xs.len() / b.max(1);
12757            for bi in 0..b {
12758                r.scores(
12759                    &xs[bi * hdim..(bi + 1) * hdim],
12760                    &mut logits[bi * ne..(bi + 1) * ne],
12761                );
12762            }
12763        }
12764        None => m.router.matmat(xs, b, &mut logits, pool),
12765    }
12766
12767    // Assignments: expert → [(position, weight)] — same routing as
12768    // moe_ffn, per position (see `moe_route`).
12769    let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
12770    {
12771        let mut st = m.stats.borrow_mut();
12772        if st.len() < ne {
12773            st.resize(ne, 0);
12774        }
12775        for bi in 0..b {
12776            let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
12777            for &e in &idx {
12778                st[e] += 1;
12779                assign[e].push((bi, p[e] / wsum));
12780            }
12781        }
12782    }
12783
12784    let mut out = vec![0.0f32; b * hidden];
12785    let cols = m.experts[0].gate_proj.cols();
12786    let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
12787        let sb = list.len();
12788        let mut sub = vec![0.0f32; sb * cols];
12789        for (k, &(bi, _)) in list.iter().enumerate() {
12790            sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
12791        }
12792        let eo = dense_ffn_batch(d, &sub, sb, pool, None);
12793        for (k, &(bi, w)) in list.iter().enumerate() {
12794            for i in 0..hidden {
12795                out[bi * hidden + i] += w * eo[k * hidden + i];
12796            }
12797        }
12798    };
12799    // Routed experts: the panels are TINY (b·top_k spread over every
12800    // expert — a few positions each), so a pool dispatch per expert is
12801    // pure barrier cost. Invert the parallelism: workers take WHOLE
12802    // experts (serial math inside), then one deterministic scatter in
12803    // expert order — the exact accumulation order the serial loop had.
12804    let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
12805    if pool.is_some() && active.len() >= 8 {
12806        let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
12807        {
12808            let panel_ptr = SendVecs(panels.as_mut_ptr());
12809            // Capture only the expert table: `m` itself carries RefCell
12810            // stats and must not cross the pool boundary.
12811            let experts = &m.experts;
12812            let (active_r, assign_r) = (&active, &assign);
12813            let run = |start: usize, end: usize| {
12814                for ai in start..end {
12815                    let e = active_r[ai];
12816                    let list = &assign_r[e];
12817                    let sb = list.len();
12818                    let mut sub = vec![0.0f32; sb * cols];
12819                    for (k, &(bi, _)) in list.iter().enumerate() {
12820                        sub[k * cols..(k + 1) * cols]
12821                            .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
12822                    }
12823                    // SAFETY: each worker owns a disjoint panels[ai].
12824                    unsafe {
12825                        *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
12826                    }
12827                }
12828            };
12829            match pool {
12830                Some(p) => p.run_rows(active.len(), &run),
12831                None => run(0, active.len()),
12832            }
12833        }
12834        for (ai, &e) in active.iter().enumerate() {
12835            for (k, &(bi, w)) in assign[e].iter().enumerate() {
12836                let eo = &panels[ai][k * hidden..(k + 1) * hidden];
12837                for i in 0..hidden {
12838                    out[bi * hidden + i] += w * eo[i];
12839                }
12840            }
12841        }
12842    } else {
12843        for &e in &active {
12844            run_expert(&m.experts[e], &assign[e], &mut out);
12845        }
12846    }
12847    if let Some((se, gate)) = &m.shared {
12848        let all: Vec<(usize, f32)> = if let Some(gate) = gate {
12849            let mut gl = vec![0.0f32; b];
12850            gate.matmat(xs, b, &mut gl, pool);
12851            (0..b)
12852                .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
12853                .collect()
12854        } else {
12855            (0..b).map(|bi| (bi, 1.0)).collect()
12856        };
12857        run_expert(se, &all, &mut out);
12858    }
12859    out
12860}
12861
12862thread_local! {
12863    /// gate/up activation scratch for the dense FFN paths (single uses
12864    /// two slots, the fused pair all four) — these were fresh
12865    /// intermediate-size Vecs on every layer of every token.
12866    static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
12867        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
12868}
12869
12870/// Dense SwiGLU FFN through QTensor matvecs (any storage).
12871fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
12872    // Per-token sparsity, when the file was built for it: gate first,
12873    // then only the chosen neurons' up/down rows leave the mmap.
12874    if gate_topk() > 0
12875        && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
12876    {
12877        return out;
12878    }
12879    // Whole-FFN GPU submit (этап 4.2 increment): gate → silu·up → down
12880    // chained in ONE command buffer with the intermediate activations
12881    // resident on the device — 3 per-op polls become 1 per layer. The
12882    // moe_block backend already implements exactly this chain; a dense
12883    // FFN is one expert with weight 1. Runtime probe: the chain still
12884    // pays one submit+poll per layer — alternate it against the pure-CPU
12885    // FFN and keep whichever is faster on this machine.
12886    // q1 FFNs offload at any practical size: the q1 CPU kernel is
12887    // compute-bound, so the UMA threshold logic does not apply — the
12888    // probe measures and decides either way.
12889    // The fused GPU block has no descriptor-aware Prism path: it would either
12890    // consume an unrotated activation or decline after inspecting the mixed
12891    // q2tp/q4tp tensors.  Do not let that structural refusal enter the FFN
12892    // probe's CPU_ONLY scope; the ordinary body below dispatches each matrix
12893    // through QTensor::matvec, which owns the signed FWHT + affine q2tp route.
12894    let prism_body = d.gate_proj.has_prism_contract()
12895        || d.up_proj.has_prism_contract()
12896        || d.down_proj.has_prism_contract();
12897    if !prism_body
12898        && crate::gpu::enabled_here()
12899        && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
12900    {
12901        let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
12902            crate::gpu::ProbeArm::Gpu
12903        } else {
12904            crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
12905        };
12906        match arm {
12907            crate::gpu::ProbeArm::Gpu => {
12908                let t0 = std::time::Instant::now();
12909                if let Some(out) = dense_ffn_gpu(d, x, pool) {
12910                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
12911                    return out;
12912                }
12913                // Declined: no timing exists, so say so. Silence here is
12914                // what left `ffn` undecided for 9000 calls and cost a
12915                // failed device attempt on half of them.
12916                crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
12917            }
12918            crate::gpu::ProbeArm::CpuTimed => {
12919                let t0 = std::time::Instant::now();
12920                let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
12921                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
12922                return out;
12923            }
12924            crate::gpu::ProbeArm::Cpu => {
12925                return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
12926            }
12927        }
12928    }
12929    dense_ffn_cpu(d, x, pool)
12930}
12931
12932/// The pure-CPU dense-FFN body (also the fallback of every GPU refusal).
12933fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
12934    let inter = d.gate_proj.rows();
12935    FFN_SCRATCH.with(|s| {
12936        let mut s = s.borrow_mut();
12937        let [g, u, ..] = &mut *s;
12938        g.resize(inter, 0.0);
12939        // Fused gate+up+silu: one dispatch, no separate silu pass.
12940        // Falls back to matvec_many + silu loop for unsupported dtypes.
12941        if gate_topk() > 0 {
12942            // Gate first, select, and only then pay for `up`: the
12943            // measurement arm computes both and zeroes the losers, which
12944            // is the same arithmetic.
12945            u.resize(inter, 0.0);
12946            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12947            for i in 0..inter {
12948                g[i] = Act::Silu.combine(g[i], 1.0);
12949            }
12950            keep_top_k(g, gate_topk());
12951            for i in 0..inter {
12952                g[i] *= u[i];
12953            }
12954        } else if d.act == Act::Silu
12955            && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
12956        {
12957            // g now holds silu(gate)·up directly.
12958        } else {
12959            u.resize(inter, 0.0);
12960            // Multi-matrix job: gate+up under one pool dispatch.
12961            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12962            for i in 0..inter {
12963                g[i] = d.act.combine(g[i], u[i]);
12964            }
12965        }
12966        // DTG-MA bake probe (Patent 2): accumulate this layer's
12967        // per-neuron activation mass while a probe pass is active.
12968        // `CMF_FFN_PROBE_TOPK=k` switches the statistic from mass to a
12969        // HIT COUNT — how many tokens rank the neuron in their own top
12970        // k. Mass asks "how loud is this neuron overall", the count
12971        // asks "how often does this task actually need it", and the two
12972        // rank neurons differently whenever a few tokens are loud.
12973        FFN_PROBE.with(|pr| {
12974            if let Some(acc) = pr.borrow_mut().as_mut() {
12975                let li = crate::gpu::cur_layer();
12976                if li >= 0 {
12977                    if let Some(row) = acc.get_mut(li as usize) {
12978                        match probe_topk() {
12979                            0 if probe_sq() => {
12980                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12981                                    *a += (v as f64) * (v as f64);
12982                                }
12983                            }
12984                            0 if probe_signed() => {
12985                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12986                                    *a += v as f64;
12987                                }
12988                            }
12989                            0 => {
12990                                for (a, &v) in row.iter_mut().zip(g.iter()) {
12991                                    *a += (v as f64).abs();
12992                                }
12993                            }
12994                            k => {
12995                                let n = g.len();
12996                                let k = k.min(n);
12997                                let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12998                                let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12999                                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
13000                                });
13001                                let thr = *kth;
13002                                for (a, &v) in row.iter_mut().zip(g.iter()) {
13003                                    if v.abs() >= thr {
13004                                        *a += 1.0;
13005                                    }
13006                                }
13007                            }
13008                        }
13009                    }
13010                }
13011            }
13012        });
13013        if oracle_topk() > 0 {
13014            keep_top_k(g, oracle_topk());
13015        }
13016        {
13017            let li = crate::gpu::cur_layer();
13018            if li >= 0 {
13019                adump_row(li as usize, g);
13020            }
13021        }
13022        let mut out = attention::take_buf(d.down_proj.rows());
13023        d.down_proj.matvec(g, &mut out, pool);
13024        out
13025    })
13026}
13027
13028/// Online accumulators for the AWNP refit of a narrowed FFN.
13029///
13030/// The refit needs `Gss = A_SᵀA_S` and `YA = YᵀA_S` per layer, where `A_S`
13031/// are the calibration activations of the KEPT neurons and `Y` the full
13032/// FFN output. Both are small enough to hold; the thing that is not is
13033/// the activations they are built from — a 27B layer would dump a
13034/// gigabyte per thousand tokens. So they are accumulated as the
13035/// calibration runs and written once at the end.
13036///
13037/// `CMF_FFN_REFIT=<dir>` holds `support.<L>.u32` (a u32 count then the
13038/// kept indices) for every layer to accumulate; `CMF_FFN_REFIT_FROM/TO`
13039/// bound the layer span so the accumulators fit in RAM.
13040pub struct RefitAcc {
13041    pub support: Vec<u32>,
13042    pub gss: Vec<f32>,
13043    pub ya: Vec<f32>,
13044    pub hidden: usize,
13045    pub tokens: u64,
13046    /// Activations staged transposed ([ns, t] and [hidden, t]) until the
13047    /// batch is worth a GEMM. The product costs `ns²` to move and add
13048    /// REGARDLESS of how many tokens went into it, so folding 16 chunks
13049    /// into one call cuts that cost 16× — it was 15 TB of traffic per
13050    /// calibration pass at one call per 256 tokens.
13051    pub buf_g: Vec<f32>,
13052    pub buf_o: Vec<f32>,
13053    pub buf_t: usize,
13054}
13055
13056/// The product buffer is SHARED across layers — one 473 MB allocation,
13057/// not one per layer (that was 30 GB of nothing on a 64-layer model).
13058/// It lives under the same lock as the accumulators.
13059type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
13060
13061static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
13062    std::sync::OnceLock::new();
13063
13064/// Is an FFN probe accumulator installed on this thread? The fused GPU
13065/// FFN must decline while one is, or the probe silently measures zero.
13066fn ffn_probe_active() -> bool {
13067    FFN_PROBE.with(|p| p.borrow().is_some())
13068}
13069
13070fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
13071    REFIT
13072        .get_or_init(|| {
13073            std::env::var("CMF_FFN_REFIT").ok().map(|d| {
13074                (
13075                    d,
13076                    std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
13077                )
13078            })
13079        })
13080        .as_ref()
13081}
13082
13083/// Accumulate one prefill panel into the layer's refit statistics.
13084fn refit_accumulate(
13085    li: usize,
13086    g: &[f32],
13087    b: usize,
13088    inter: usize,
13089    out: &[f32],
13090    hidden: usize,
13091    pool: Option<&Pool>,
13092) {
13093    let Some((dir, map)) = refit_dir() else {
13094        return;
13095    };
13096    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
13097    let (from, to) = *SPAN.get_or_init(|| {
13098        let g = |k: &str, d: usize| {
13099            std::env::var(k)
13100                .ok()
13101                .and_then(|v| v.parse().ok())
13102                .unwrap_or(d)
13103        };
13104        (
13105            g("CMF_FFN_REFIT_FROM", 0),
13106            g("CMF_FFN_REFIT_TO", usize::MAX),
13107        )
13108    });
13109    if li < from || li > to {
13110        return;
13111    }
13112    let mut guard = map.lock().unwrap();
13113    let (map, shared) = &mut *guard;
13114    let acc = match map.entry(li) {
13115        std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
13116        std::collections::hash_map::Entry::Vacant(e) => {
13117            let path = format!("{dir}/support.{li}.u32");
13118            let Ok(bytes) = std::fs::read(&path) else {
13119                eprintln!("refit: no {path} — layer {li} skipped");
13120                return;
13121            };
13122            let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
13123            let support: Vec<u32> = bytes[4..4 + n * 4]
13124                .chunks_exact(4)
13125                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
13126                .collect();
13127            eprintln!(
13128                "refit: layer {li} support {n} ({:.0} MB of accumulator)",
13129                (n * n + hidden * n) as f64 * 4.0 / 1e6
13130            );
13131            e.insert(RefitAcc {
13132                gss: vec![0.0; n * n],
13133                ya: vec![0.0; hidden * n],
13134                buf_g: Vec::new(),
13135                buf_o: Vec::new(),
13136                buf_t: 0,
13137                support,
13138                hidden,
13139                tokens: 0,
13140            })
13141        }
13142    };
13143    let ns = acc.support.len();
13144    // Stage this chunk transposed; the GEMM fires once the batch is full.
13145    let cap = refit_batch();
13146    if acc.buf_g.is_empty() {
13147        acc.buf_g = vec![0.0; ns * cap];
13148        acc.buf_o = vec![0.0; hidden * cap];
13149    }
13150    let take = b.min(cap - acc.buf_t);
13151    for t in 0..take {
13152        let col = acc.buf_t + t;
13153        for (j, &n) in acc.support.iter().enumerate() {
13154            acc.buf_g[j * cap + col] = g[t * inter + n as usize];
13155        }
13156        for h in 0..hidden {
13157            acc.buf_o[h * cap + col] = out[t * hidden + h];
13158        }
13159    }
13160    acc.buf_t += take;
13161    acc.tokens += take as u64;
13162    if acc.buf_t < cap {
13163        return;
13164    }
13165    let bt = acc.buf_t;
13166    acc.buf_t = 0;
13167    // The GEMM WRITES its C (it zeroes the accumulators it uses), so the
13168    // chunk product lands in scratch and is added on — the one thing that
13169    // silently turns a Gram over 13 000 tokens into a Gram over 256.
13170    // Both products are `C[n, m] += X[n, b] · Yᵀ[b, m]` with X and Y
13171    // stored row-major [·, b] — exactly `gemm_nt_f32`'s shape, so the
13172    // card does them when it is up (this is the whole calibration's
13173    // cost: O(|S|²) per token, 2.9 PFLOP for a 27B pass). The tiled CPU
13174    // loop stays as the fallback. Neither accumulates, so the product
13175    // lands in scratch and is added on.
13176    let RefitAcc {
13177        gss,
13178        ya,
13179        buf_g,
13180        buf_o,
13181        ..
13182    } = acc;
13183    let need = (ns * ns).max(hidden * ns);
13184    if shared.len() < need {
13185        shared.resize(need, 0.0);
13186    }
13187    let scratch = &mut shared[..];
13188    let _ = bt;
13189    if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
13190        add_into(gss, &scratch[..ns * ns], pool);
13191        if crate::gpu::gemm_nt_f32_transient(
13192            buf_o,
13193            buf_g,
13194            &mut scratch[..hidden * ns],
13195            hidden,
13196            cap,
13197            ns,
13198        ) {
13199            add_into(ya, &scratch[..hidden * ns], pool);
13200        } else {
13201            accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
13202        }
13203    } else {
13204        accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
13205        accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
13206    }
13207    // No zeroing: the batch is always filled exactly (cap is a multiple
13208    // of the prefill chunk), and a memset of 178 MB a layer would cost
13209    // more than the GEMM.
13210}
13211
13212/// `CMF_FFN_REFIT_BATCH` — tokens staged before each GEMM (default 4096).
13213fn refit_batch() -> usize {
13214    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13215    *B.get_or_init(|| {
13216        std::env::var("CMF_FFN_REFIT_BATCH")
13217            .ok()
13218            .and_then(|v| v.parse().ok())
13219            .unwrap_or(4096)
13220    })
13221}
13222
13223/// `c[m, n] += Σ_t left[m, t]·right[n, t]` — both operands transposed,
13224/// the CPU fallback for the staged batch.
13225fn accum_outer_t(
13226    c: &mut [f32],
13227    m: usize,
13228    n: usize,
13229    b: usize,
13230    left: &[f32],
13231    right: &[f32],
13232    pool: Option<&Pool>,
13233) {
13234    let ptr = SendMut(c.as_mut_ptr());
13235    let body = |i: usize| {
13236        let ptr = &ptr;
13237        let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
13238        for t in 0..b {
13239            let a = left[i * b + t];
13240            if a == 0.0 {
13241                continue;
13242            }
13243            for (j, o) in row.iter_mut().enumerate() {
13244                *o += a * right[j * b + t];
13245            }
13246        }
13247    };
13248    match pool {
13249        Some(p) if m > 1 => p.run_rows(m, &|s, e| {
13250            for i in s..e {
13251                body(i);
13252            }
13253        }),
13254        _ => {
13255            for i in 0..m {
13256                body(i);
13257            }
13258        }
13259    }
13260}
13261
13262/// `dst += src`, spread over the pool — at 118 M floats a layer this is
13263/// not a loop to leave on one core.
13264fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
13265    let n = dst.len().min(src.len());
13266    match pool {
13267        Some(p) if n >= 1 << 16 => {
13268            let ptr = SendMut(dst.as_mut_ptr());
13269            let f = |s: usize, e: usize| {
13270                let ptr = &ptr;
13271                for blk in s..e {
13272                    let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
13273                    for i in a..b {
13274                        unsafe { *ptr.0.add(i) += src[i] };
13275                    }
13276                }
13277            };
13278            p.run_rows(n.div_ceil(4096), &f);
13279        }
13280        _ => {
13281            for (d, v) in dst.iter_mut().zip(&src[..n]) {
13282                *d += *v;
13283            }
13284        }
13285    }
13286}
13287
13288/// `c[m, n] += Σ_t left[t, m]·right[t, n]`, with `left` stored [m, t] and
13289/// `right` [t, n]. Tiled over the rows of `c` so a tile stays in cache
13290/// while each token's `right` row streams past it once, and parallel
13291/// over tiles.
13292fn accum_outer(
13293    c: &mut [f32],
13294    m: usize,
13295    n: usize,
13296    b: usize,
13297    left: &[f32],
13298    right: &[f32],
13299    pool: Option<&Pool>,
13300) {
13301    const TILE: usize = 32;
13302    let tiles = m.div_ceil(TILE);
13303    let cp = SendMut(c.as_mut_ptr());
13304    let body = |ti: usize| {
13305        let cp = &cp;
13306        let i0 = ti * TILE;
13307        let i1 = (i0 + TILE).min(m);
13308        for t in 0..b {
13309            let r = &right[t * n..t * n + n];
13310            for i in i0..i1 {
13311                let a = left[i * b + t];
13312                if a == 0.0 {
13313                    continue;
13314                }
13315                // SAFETY: tiles partition c's rows; workers never overlap.
13316                let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
13317                for (o, v) in row.iter_mut().zip(r) {
13318                    *o += a * *v;
13319                }
13320            }
13321        }
13322    };
13323    match pool {
13324        Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
13325            for ti in s..e {
13326                body(ti);
13327            }
13328        }),
13329        _ => {
13330            for ti in 0..tiles {
13331                body(ti);
13332            }
13333        }
13334    }
13335}
13336
13337/// Write what the calibration accumulated: `gss.<L>.f32` and `ya.<L>.f32`.
13338pub fn refit_flush() -> usize {
13339    let Some((dir, map)) = refit_dir() else {
13340        return 0;
13341    };
13342    let guard = map.lock().unwrap();
13343    let mut n = 0;
13344    for (li, acc) in guard.0.iter() {
13345        // A silently truncated write here is a Gram that reshapes to
13346        // nothing an hour later — say it out loud instead.
13347        let w = |name: &str, v: &[f32]| {
13348            let path = format!("{dir}/{name}.{li}.f32");
13349            let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
13350            match std::fs::write(&path, &bytes) {
13351                Ok(()) => {}
13352                Err(e) => eprintln!(
13353                    "refit: FAILED to write {path} ({} MB): {e}",
13354                    bytes.len() / 1_000_000
13355                ),
13356            }
13357        };
13358        w("gss", &acc.gss);
13359        w("ya", &acc.ya);
13360        println!(
13361            "refit L{li}: {} support, {} tokens, hidden {}",
13362            acc.support.len(),
13363            acc.tokens,
13364            acc.hidden
13365        );
13366        n += 1;
13367    }
13368    n
13369}
13370
13371/// `CMF_FFN_ADUMP=<prefix>` — append every probed token's FFN activation
13372/// row to `<prefix>.<layer>.f16`. The co-activation record: which
13373/// neurons fire together, which is what a tube has to group if a token
13374/// is ever going to open one tube instead of sixteen.
13375fn adump_row(li: usize, g: &[f32]) {
13376    use std::io::Write as _;
13377    static FILES: std::sync::OnceLock<
13378        Option<(
13379            String,
13380            std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
13381        )>,
13382    > = std::sync::OnceLock::new();
13383    let Some((prefix, map)) = FILES
13384        .get_or_init(|| {
13385            std::env::var("CMF_FFN_ADUMP")
13386                .ok()
13387                .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
13388        })
13389        .as_ref()
13390    else {
13391        return;
13392    };
13393    // `CMF_FFN_ADUMP_FROM/_TO` narrow the dump to a layer span, so a big
13394    // calibration run fits on disk in a few passes instead of one.
13395    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
13396    let (from, to) = *SPAN.get_or_init(|| {
13397        let g = |k: &str, d: usize| {
13398            std::env::var(k)
13399                .ok()
13400                .and_then(|v| v.parse().ok())
13401                .unwrap_or(d)
13402        };
13403        (
13404            g("CMF_FFN_ADUMP_FROM", 0),
13405            g("CMF_FFN_ADUMP_TO", usize::MAX),
13406        )
13407    });
13408    if li < from || li > to {
13409        return;
13410    }
13411    let mut map = map.lock().unwrap();
13412    let f = map.entry(li).or_insert_with(|| {
13413        std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
13414    });
13415    let mut bytes = Vec::with_capacity(g.len() * 2);
13416    for v in g {
13417        bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
13418    }
13419    let _ = f.write_all(&bytes);
13420}
13421
13422/// `CMF_FFN_ORACLE_TOPK` — keep only the k largest |silu(g)·u| of each
13423/// token and zero the rest. Not a serving mode: it is the CEILING of
13424/// contextual sparsity — what a per-token router would be chasing —
13425/// measured by cheating, since the selection reads the very activations
13426/// it would have to predict.
13427fn oracle_topk() -> usize {
13428    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13429    *K.get_or_init(|| {
13430        std::env::var("CMF_FFN_ORACLE_TOPK")
13431            .ok()
13432            .and_then(|v| v.parse().ok())
13433            .unwrap_or(0)
13434    })
13435}
13436
13437/// `CMF_FFN_GATE_TOPK` — the REALIZABLE cousin of the oracle: rank the
13438/// neurons by their gate alone (which the kernel has computed anyway
13439/// before it reads `up`), keep the k best, and drop the rest. Every
13440/// dropped neuron's `up` row and `down` column stay unread, so this is
13441/// the sparsity a serving path can actually take without a router.
13442fn gate_topk() -> usize {
13443    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13444    *K.get_or_init(|| {
13445        std::env::var("CMF_FFN_GATE_TOPK")
13446            .ok()
13447            .and_then(|v| v.parse().ok())
13448            .unwrap_or(0)
13449    })
13450}
13451
13452/// `CMF_FFN_GATE_BLOCK` — select in blocks of B neurons instead of one
13453/// by one. A scattered per-neuron choice cannot be read efficiently (a
13454/// row at a time, no prefetch runway); a block of 32 is a contiguous
13455/// 32-row slab of `up` and of the transposed `down`, which the ordinary
13456/// kernels stream. The question the measurement answers is what the
13457/// block costs in quality.
13458fn gate_block() -> usize {
13459    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13460    *B.get_or_init(|| {
13461        std::env::var("CMF_FFN_GATE_BLOCK")
13462            .ok()
13463            .and_then(|v| v.parse().ok())
13464            .unwrap_or(1)
13465    })
13466}
13467
13468/// Zero all but the `k` largest BLOCKS (by summed square) of a row.
13469fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
13470    let n = g.len();
13471    let nb = n.div_ceil(block);
13472    let kb = (keep_n.div_ceil(block)).clamp(1, nb);
13473    if kb >= nb {
13474        return;
13475    }
13476    let mut score: Vec<f32> = (0..nb)
13477        .map(|b| {
13478            g[b * block..((b + 1) * block).min(n)]
13479                .iter()
13480                .map(|v| v * v)
13481                .sum::<f32>()
13482        })
13483        .collect();
13484    let mut ord = score.clone();
13485    let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
13486        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
13487    });
13488    let thr = *kth;
13489    for b in 0..nb {
13490        if score[b] < thr {
13491            g[b * block..((b + 1) * block).min(n)].fill(0.0);
13492        }
13493    }
13494    score.clear();
13495}
13496
13497/// Zero all but the `k` largest magnitudes of one token's activation row.
13498fn keep_top_k(g: &mut [f32], k: usize) {
13499    if gate_block() > 1 {
13500        return keep_top_blocks(g, k, gate_block());
13501    }
13502    let n = g.len();
13503    if k == 0 || k >= n {
13504        return;
13505    }
13506    let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
13507    let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
13508        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
13509    });
13510    let thr = *kth;
13511    for v in g.iter_mut() {
13512        if v.abs() < thr {
13513            *v = 0.0;
13514        }
13515    }
13516}
13517
13518/// `CMF_FFN_PROBE_SQ` — accumulate Σa², so the dump divided by the token
13519/// count and square-rooted is the RMS activation trace Patent 12 weights
13520/// its matrices by.
13521fn probe_sq() -> bool {
13522    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13523    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
13524}
13525
13526/// `CMF_FFN_PROBE_SIGNED` — accumulate the SIGNED activation sum
13527/// instead of its magnitude: what a dropped neuron contributes ON
13528/// AVERAGE, which is the bias a narrowed FFN can add back for free.
13529fn probe_signed() -> bool {
13530    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13531    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
13532}
13533
13534/// `CMF_FFN_MEANFILL=<file>` — a masked-out neuron contributes its MEAN
13535/// activation instead of zero (`u32 layers, u32 inter, f32[…]`, the mass
13536/// dump layout, holding per-neuron means). Dropping a neuron outright
13537/// also drops its average contribution, which shifts the layer output by
13538/// a constant; filling the mean back is one add per layer and costs no
13539/// bytes off the bus. This is the measurement arm — in a tube file the
13540/// same correction ships as a per-task bias vector.
13541fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
13542    static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
13543    M.get_or_init(|| {
13544        let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
13545        let b = std::fs::read(&p).ok()?;
13546        let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
13547        let vals: Vec<f32> = b[8..]
13548            .chunks_exact(4)
13549            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
13550            .collect();
13551        eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
13552        Some((inter, vals))
13553    })
13554    .as_ref()
13555}
13556
13557/// `CMF_FFN_PROBE_TOPK` — 0 (default) = accumulate mass, k>0 = count
13558/// how often a neuron lands in a token's top k.
13559fn probe_topk() -> usize {
13560    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13561    *K.get_or_init(|| {
13562        std::env::var("CMF_FFN_PROBE_TOPK")
13563            .ok()
13564            .and_then(|v| v.parse().ok())
13565            .unwrap_or(0)
13566    })
13567}
13568
13569thread_local! {
13570    /// DTG-MA activation probe: per-layer per-neuron Σ|silu(g)·u|
13571    /// accumulator, alive only during `Pipeline::probe_ffn_mass`.
13572    static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
13573        const { std::cell::RefCell::new(None) };
13574}
13575
13576/// Per-token structured sparsity, paid for in bytes.
13577///
13578/// The gate is the cheapest third of an FFN and it already says which
13579/// neurons matter: `silu(gate)` near zero means the neuron contributes
13580/// nothing whatever `up` says. So compute every gate, keep the `k`
13581/// loudest, and read ONLY those neurons' `up` rows and `down` rows —
13582/// the latter needs `down_proj` stored transposed, otherwise a neuron's
13583/// down weights are a strided column and "reading only those" costs a
13584/// full cache line each.
13585///
13586/// Returns `None` when the file has no transposed `down` (the caller
13587/// then runs the ordinary dense path).
13588fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
13589    // The scatter path reads individual rows/columns and cannot express the
13590    // per-matrix signed FWHT boundary.  Let the descriptor-aware dense path
13591    // handle Prism files rather than silently running an unrotated sparse
13592    // approximation.
13593    if d.gate_proj.has_prism_contract()
13594        || d.up_proj.has_prism_contract()
13595        || d.down_proj.has_prism_contract()
13596    {
13597        return None;
13598    }
13599    let dt = d.down_t.as_ref()?;
13600    let inter = d.gate_proj.rows();
13601    let hidden = dt.cols();
13602    if k == 0 || k >= inter || d.act != Act::Silu {
13603        return None;
13604    }
13605    DYN_SCRATCH.with(|sc| {
13606        let mut sc = sc.borrow_mut();
13607        let DynScratch {
13608            g,
13609            mag,
13610            live,
13611            parts,
13612        } = &mut *sc;
13613        g.resize(inter, 0.0);
13614        d.gate_proj.matvec(x, g, pool);
13615        for v in g.iter_mut() {
13616            *v = inference::silu(*v);
13617        }
13618        // The k-th largest |silu(gate)| is the threshold; ties keep more,
13619        // which is the safe side.
13620        mag.clear();
13621        mag.extend(g.iter().map(|v| v.abs()));
13622        let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
13623            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
13624        });
13625        let thr = *kth;
13626        live.clear();
13627        live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
13628        let mut out = vec![0.0f32; hidden];
13629        match pool {
13630            Some(p) if live.len() >= 64 => {
13631                let nw = p.n_workers() + 1;
13632                parts.clear();
13633                parts.resize(nw * hidden, 0.0);
13634                let ptr = SendMut(parts.as_mut_ptr());
13635                let n = live.len();
13636                let live_ref: &[u32] = live;
13637                let g_ref: &[f32] = g;
13638                p.run(&|w, workers| {
13639                    let chunk = n.div_ceil(workers);
13640                    let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
13641                    if s >= e {
13642                        return;
13643                    }
13644                    WORKER_SCRATCH.with(|ws| {
13645                        let mut ws = ws.borrow_mut();
13646                        let [scratch, acc] = &mut *ws;
13647                        scratch.resize(hidden.max(x.len()), 0.0);
13648                        acc.clear();
13649                        acc.resize(hidden, 0.0);
13650                        for (o, &nrm) in live_ref[s..e].iter().enumerate() {
13651                            // One neuron of runway: the next row's lines
13652                            // start moving while this one is multiplied.
13653                            if let Some(&nx) = live_ref[s..e].get(o + 1) {
13654                                d.up_proj.prefetch_row(nx as usize);
13655                                dt.prefetch_row(nx as usize);
13656                            }
13657                            let idx = nrm as usize;
13658                            let up = d.up_proj.row_dot(idx, x, scratch);
13659                            let a = g_ref[idx] * up;
13660                            if a != 0.0 {
13661                                dt.add_row_scaled(idx, a, acc, scratch);
13662                            }
13663                        }
13664                        for (j, v) in acc.iter().enumerate() {
13665                            unsafe { *ptr.at(w * hidden + j) = *v };
13666                        }
13667                    });
13668                });
13669                for w in 0..nw {
13670                    for (j, o) in out.iter_mut().enumerate() {
13671                        *o += parts[w * hidden + j];
13672                    }
13673                }
13674            }
13675            _ => {
13676                WORKER_SCRATCH.with(|ws| {
13677                    let mut ws = ws.borrow_mut();
13678                    let [scratch, _acc] = &mut *ws;
13679                    scratch.resize(hidden.max(x.len()), 0.0);
13680                    for &nrm in live.iter() {
13681                        let idx = nrm as usize;
13682                        let up = d.up_proj.row_dot(idx, x, scratch);
13683                        let a = g[idx] * up;
13684                        if a != 0.0 {
13685                            dt.add_row_scaled(idx, a, &mut out, scratch);
13686                        }
13687                    }
13688                });
13689            }
13690        }
13691        Some(out)
13692    })
13693}
13694
13695/// Caller-side scratch of the dynamic path — one allocation per thread,
13696/// not one per layer per token (that alone cost a third of the decode).
13697struct DynScratch {
13698    g: Vec<f32>,
13699    mag: Vec<f32>,
13700    live: Vec<u32>,
13701    parts: Vec<f32>,
13702}
13703
13704thread_local! {
13705    static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
13706        std::cell::RefCell::new(DynScratch {
13707            g: Vec::new(),
13708            mag: Vec::new(),
13709            live: Vec::new(),
13710            parts: Vec::new(),
13711        })
13712    };
13713    /// Pool-worker scratch: the row buffer and this worker's partial sum.
13714    static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
13715        const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
13716}
13717
13718/// `dense_ffn_cpu` with a per-visit mask landing on the activations —
13719/// the masked-inference fast path's decode arm. Full fused quant
13720/// compute, closed neurons zeroed before down: arithmetically the
13721/// pruned network, no dequant, no weight bytes touched.
13722fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
13723    let inter = d.gate_proj.rows();
13724    FFN_SCRATCH.with(|s| {
13725        let mut s = s.borrow_mut();
13726        let [g, u, ..] = &mut *s;
13727        g.resize(inter, 0.0);
13728        if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
13729            // g holds silu(gate)·up.
13730        } else {
13731            u.resize(inter, 0.0);
13732            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
13733            for i in 0..inter {
13734                g[i] = d.act.combine(g[i], u[i]);
13735            }
13736        }
13737        zero_masked_cols(g, 1, inter, mask_row);
13738        let mut out = attention::take_buf(d.down_proj.rows());
13739        d.down_proj.matvec(g, &mut out, pool);
13740        out
13741    })
13742}
13743
13744/// Dense FFN as one GPU submission via the MoE block path (single
13745/// expert, weight 1.0): gate → silu·up → down chained in one command
13746/// buffer, intermediate activations device-resident. None → weights
13747/// not q8-mapped in the primary shard / over the VRAM budget / backend
13748/// refusal → honest CPU path.
13749fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
13750    if d.gate_proj.has_prism_contract()
13751        || d.up_proj.has_prism_contract()
13752        || d.down_proj.has_prism_contract()
13753    {
13754        return None;
13755    }
13756    // The GPU block hardcodes SiLU; GeLU FFNs (Gemma) stay on CPU.
13757    if d.act != Act::Silu {
13758        return None;
13759    }
13760    // Threshold: tiny FFNs are not worth a submission (q1 excepted —
13761    // see the caller's gate).
13762    if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
13763        return None;
13764    }
13765    let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
13766    let mut model_ref = None;
13767    moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
13768    let model = model_ref?;
13769    let hidden = jobs[0].down.1;
13770    let mut out = attention::take_buf(hidden);
13771    if crate::gpu::moe_block(&model, &jobs, &mut out) {
13772        Some(out)
13773    } else {
13774        let mut out = out;
13775        attention::recycle_buf(&mut out);
13776        None
13777    }
13778}
13779
13780/// q8-mapped primary-shard tensor parts for a GPU job: q8_2f carries
13781/// its column field, q8_row runs with empty col slices (the backend
13782/// skips the multiply). Shared by the MoE block and the dense-FFN
13783/// single-job path.
13784#[allow(clippy::type_complexity)]
13785#[allow(clippy::type_complexity)]
13786pub(crate) fn moe_parts(
13787    t: &QTensor,
13788) -> Option<(
13789    &std::sync::Arc<cortiq_core::CmfModel>,
13790    usize,
13791    usize,
13792    usize,
13793    &[f32],
13794    &[f32],
13795    bool,
13796    bool,
13797    bool,
13798)> {
13799    match t {
13800        QTensor::Mapped {
13801            model,
13802            idx,
13803            dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
13804            rows,
13805            cols,
13806            row_scale,
13807            col_field,
13808            ..
13809        } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
13810            model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
13811        )),
13812        // q1: tile-embedded scales — empty rs/col slices, raw xs.
13813        QTensor::Mapped {
13814            model,
13815            idx,
13816            dtype: cortiq_core::TensorDtype::Q1,
13817            rows,
13818            cols,
13819            ..
13820        } => Some((
13821            model,
13822            *idx,
13823            *rows,
13824            *cols,
13825            &[][..],
13826            &[][..],
13827            true,
13828            false,
13829            false,
13830        )),
13831        // q4_tiled: 18-byte tiles with embedded f16 scales — raw xs.
13832        QTensor::Mapped {
13833            model,
13834            idx,
13835            dtype: cortiq_core::TensorDtype::Q4Tiled,
13836            rows,
13837            cols,
13838            ..
13839        } => Some((
13840            model,
13841            *idx,
13842            *rows,
13843            *cols,
13844            &[][..],
13845            &[][..],
13846            false,
13847            true,
13848            false,
13849        )),
13850        // q4tp: same raw-xs contract, different stride and scale plane.
13851        QTensor::Mapped {
13852            model,
13853            idx,
13854            dtype: cortiq_core::TensorDtype::Q4TiledP,
13855            rows,
13856            cols,
13857            ..
13858        } => Some((
13859            model,
13860            *idx,
13861            *rows,
13862            *cols,
13863            &[][..],
13864            &[][..],
13865            false,
13866            true,
13867            false,
13868        )),
13869        // q2tp: the 2-bit expert plane of the mixed profile — q4 family
13870        // for stride bookkeeping, flagged q2 so the trio validation can
13871        // demand a q4tp down.
13872        QTensor::Mapped {
13873            model,
13874            idx,
13875            dtype: cortiq_core::TensorDtype::Q2TiledP,
13876            rows,
13877            cols,
13878            ..
13879        } => Some((
13880            model,
13881            *idx,
13882            *rows,
13883            *cols,
13884            &[][..],
13885            &[][..],
13886            false,
13887            true,
13888            true,
13889        )),
13890        _ => None,
13891    }
13892}
13893
13894/// Map a MoE onto the Metal token graph's contract: f32 router, a
13895/// shared expert (gated — Qwen — or ungated at weight 1 — DeepSeek-V3 /
13896/// HunYuan hy_v3), softmax or sigmoid scores with an optional selection
13897/// bias and routed scale, experts uniformly q4tp (or the mixed profile:
13898/// q2tp gate/up over a q4tp down). τ routers, masks, per-expert scales
13899/// and Gemma's router-input norm refuse here — those semantics stay on
13900/// the CPU path.
13901#[cfg(target_os = "macos")]
13902fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
13903    if m.router_input_norm
13904        || m.route_tau.is_some()
13905        || m.mask.is_some()
13906        || m.per_expert_scale.is_some()
13907        || m.experts.is_empty()
13908        || m.top_k == 0
13909        || m.resonance.is_some()
13910    {
13911        return None;
13912    }
13913    // The select kernel always fills the shared slot: a model without a
13914    // shared expert (LFM2-MoE) stays on the CPU path here.
13915    let (sh, sg) = match &m.shared {
13916        Some((sh, sg)) => (sh, sg.as_ref()),
13917        None => return None,
13918    };
13919    let (rf, rr, rc) = m.router.f32_parts()?;
13920    if rr != m.experts.len() || rc != hidden {
13921        return None;
13922    }
13923    let shared_gated = sg.is_some();
13924    let sf = match sg {
13925        Some(sg) => {
13926            let (sf, sr, sc) = sg.f32_parts()?;
13927            if sr * sc != hidden {
13928                return None;
13929            }
13930            sf
13931        }
13932        // Ungated: the router's first row stands in for the gate matvec
13933        // (its logit is never read — the kernel pins weight 1).
13934        None => &rf[..hidden],
13935    };
13936    if let Some(b) = &m.expert_bias {
13937        if b.len() != m.experts.len() {
13938            return None;
13939        }
13940    }
13941    let inter = m.experts[0].gate_proj.rows();
13942    // The first expert's gate decides the profile; every trio (shared
13943    // included) must agree — the jobs ladder flips ONE kernel for all.
13944    let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
13945    let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
13946        if e.act != Act::Silu
13947            || e.gate_proj.rows() != inter
13948            || e.gate_proj.cols() != hidden
13949            || e.up_proj.rows() != inter
13950            || e.up_proj.cols() != hidden
13951            || e.down_proj.rows() != hidden
13952            || e.down_proj.cols() != inter
13953        {
13954            return None;
13955        }
13956        let pick = |t: &QTensor| -> Option<usize> {
13957            if gu_q2 {
13958                t.mapped_q2tp().map(|(_, i)| i)
13959            } else {
13960                t.mapped_q4tp().map(|(_, i)| i)
13961            }
13962        };
13963        Some((
13964            pick(&e.gate_proj)?,
13965            pick(&e.up_proj)?,
13966            e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
13967        ))
13968    };
13969    let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
13970    let shared = trio(sh)?;
13971    Some(crate::gpu::GpuMoe {
13972        router: rf,
13973        sgate: sf,
13974        experts,
13975        shared,
13976        n_exp: m.experts.len(),
13977        top_k: m.top_k,
13978        inter,
13979        norm_topk: m.norm_topk_prob,
13980        route_scale: m.routed_scaling,
13981        gu_q2,
13982        sigmoid: m.router_sigmoid,
13983        bias: m.expert_bias.as_deref(),
13984        shared_gated,
13985    })
13986}
13987
13988/// Build one gate/up/down GPU job from three tensors. `moe_push_job` is the
13989/// DenseFfn-shaped caller; architectures that keep their experts in their own
13990/// structs (DeepSeek-V4) come here directly.
13991pub(crate) fn moe_push_job_parts<'a>(
13992    gate: &'a QTensor,
13993    up: &'a QTensor,
13994    down: &'a QTensor,
13995    x: &[f32],
13996    w: f32,
13997    swiglu_limit: f32,
13998    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13999    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
14000) -> Option<()> {
14001    use crate::qtensor::prescale;
14002    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
14003    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
14004    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
14005    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
14006        return None; // mixed-dtype trio — honest CPU path
14007    }
14008    // The 2-bit profile is gate/up q2tp over a PLAIN q4tp down; any other
14009    // 2-bit arrangement stays on the CPU.
14010    if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
14011        return None;
14012    }
14013    if !gq2 && dq2 {
14014        return None;
14015    }
14016    model_ref.get_or_insert_with(|| gm.clone());
14017    let dt = |cf: &[f32]| {
14018        if cf.is_empty() {
14019            cortiq_core::TensorDtype::Q8Row
14020        } else {
14021            cortiq_core::TensorDtype::Q8_2f
14022        }
14023    };
14024    jobs.push(crate::gpu::MoeJob {
14025        gate: (gi, gr, gc, grs),
14026        up: (ui, ur, uc, urs),
14027        down: (di, dr, dc, drs),
14028        xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
14029        xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
14030        down_col: dcf,
14031        w,
14032        q1: gq1,
14033        q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
14034        q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
14035        gu_q2: gq2,
14036        swiglu_limit,
14037    });
14038    Some(())
14039}
14040
14041/// Build one gate/up/down GPU job (see `moe_parts`).
14042fn moe_push_job<'a>(
14043    d: &'a DenseFfn,
14044    x: &[f32],
14045    w: f32,
14046    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
14047    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
14048) -> Option<()> {
14049    use crate::qtensor::prescale;
14050    if d.act != Act::Silu {
14051        return None; // GPU block hardcodes SiLU
14052    }
14053    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
14054    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
14055    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
14056    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
14057        return None; // mixed-dtype trio — honest CPU path
14058    }
14059    if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
14060        return None;
14061    }
14062    if !gq2 && dq2 {
14063        return None;
14064    }
14065    model_ref.get_or_insert_with(|| gm.clone());
14066    let gdt = if gcf.is_empty() {
14067        cortiq_core::TensorDtype::Q8Row
14068    } else {
14069        cortiq_core::TensorDtype::Q8_2f
14070    };
14071    let udt = if ucf.is_empty() {
14072        cortiq_core::TensorDtype::Q8Row
14073    } else {
14074        cortiq_core::TensorDtype::Q8_2f
14075    };
14076    jobs.push(crate::gpu::MoeJob {
14077        gate: (gi, gr, gc, grs),
14078        up: (ui, ur, uc, urs),
14079        down: (di, dr, dc, drs),
14080        xs_gate: prescale(x, gcf, gdt).into_owned(),
14081        xs_up: prescale(x, ucf, udt).into_owned(),
14082        down_col: dcf,
14083        w,
14084        q1: gq1,
14085        q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
14086        q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
14087        gu_q2: gq2,
14088        swiglu_limit: 0.0,
14089    });
14090    Some(())
14091}
14092
14093/// Sparse dense-FFN directly on QUANTIZED weights (mask × mmap): reads
14094/// ONLY the active neurons' gate/up rows and down columns from the mmap
14095/// — no full-matrix dequant, no f32 model copy. This is what lets a
14096/// masked big model run at quantized RSS (the historical mask path
14097/// forced the whole model to f32). Semantics identical to the f32
14098/// sparse path within quant tolerance.
14099fn sparse_ffn_quant(
14100    d: &DenseFfn,
14101    x: &[f32],
14102    active: &[u16],
14103    hidden: usize,
14104    pool: Option<&Pool>,
14105) -> Vec<f32> {
14106    let n = active.len();
14107    let inter = d.gate_proj.rows();
14108    let mut act = vec![0.0f32; n];
14109    // Scratch is needed if EITHER projection is group-packed (q4/vbit);
14110    // gate/up normally share a dtype but sizing on both is robust.
14111    let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
14112    let compute = |ai: usize| -> f32 {
14113        let idx = active[ai] as usize;
14114        if idx >= inter {
14115            return 0.0; // defensive parity with the f32 sparse path
14116        }
14117        let mut s = if need_scratch {
14118            vec![0.0f32; hidden]
14119        } else {
14120            Vec::new()
14121        };
14122        let gate = d.gate_proj.row_dot(idx, x, &mut s);
14123        let up = d.up_proj.row_dot(idx, x, &mut s);
14124        d.act.combine(gate, up)
14125    };
14126    match pool {
14127        Some(p) if n >= 256 => {
14128            let ptr = SendMut(act.as_mut_ptr());
14129            p.run(&|widx, nw| {
14130                let chunk = n.div_ceil(nw);
14131                let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
14132                for ai in s..e {
14133                    unsafe { *ptr.at(ai) = compute(ai) };
14134                }
14135            });
14136        }
14137        _ => {
14138            for (ai, a) in act.iter_mut().enumerate() {
14139                *a = compute(ai);
14140            }
14141        }
14142    }
14143    // Scatter through active down columns (reads only those columns).
14144    let mut out = vec![0.0f32; hidden];
14145    for (ai, &idx) in active.iter().enumerate() {
14146        let w = act[ai];
14147        if w.abs() >= 1e-12 && (idx as usize) < inter {
14148            d.down_proj.add_col_scaled(idx as usize, w, &mut out);
14149        }
14150    }
14151    out
14152}
14153
14154/// Test-only re-export of the private sparse-quant FFN (mask × mmap gate).
14155#[doc(hidden)]
14156pub fn sparse_ffn_quant_for_test(
14157    d: &DenseFfn,
14158    x: &[f32],
14159    active: &[u16],
14160    hidden: usize,
14161) -> Vec<f32> {
14162    sparse_ffn_quant(d, x, active, hidden, None)
14163}
14164
14165/// Dequantize a DenseFfn's three matrices to f32 (transient; only the
14166/// q4/vbit-masked fallback uses it — the memory-lean path is
14167/// sparse_ffn_quant). Reuses row_f32 row-by-row.
14168fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
14169    let deq = |t: &QTensor| -> Vec<f32> {
14170        let (rows, cols) = (t.rows(), t.cols());
14171        let mut out = vec![0.0f32; rows * cols];
14172        for r in 0..rows {
14173            t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
14174        }
14175        out
14176    };
14177    (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
14178}
14179
14180/// Pointer wrapper for the worker-pool scatter (same pattern as qtensor).
14181struct SendMut(*mut f32);
14182unsafe impl Send for SendMut {}
14183unsafe impl Sync for SendMut {}
14184impl SendMut {
14185    #[inline]
14186    // Deliberate unsynchronized scatter: pool workers write disjoint indices
14187    // in parallel, so returning `&mut` from `&self` is intentional here.
14188    #[allow(clippy::mut_from_ref)]
14189    unsafe fn at(&self, i: usize) -> &mut f32 {
14190        unsafe { &mut *self.0.add(i) }
14191    }
14192}
14193
14194/// Router → (selected experts in torch.topk order, per-expert score
14195/// vector, normalizer). The final weight of expert `e` is `p[e] / wsum`.
14196///
14197/// Two regimes share this. Qwen: softmax over ALL experts, top-k of the
14198/// probabilities, optional renorm — `router_sigmoid=false`, no bias,
14199/// scale 1 → bit-identical to the historical path. LFM2-MoE /
14200/// DeepSeek-V3 `noaux_tc`: per-expert sigmoid scores, an optional
14201/// selection bias (top-k CHOICE only; weights stay unbiased), a 1e-6 renorm
14202/// floor and a routed scale.
14203pub(crate) fn moe_route(
14204    logits: &[f32],
14205    m: &MoeFfn,
14206    allowed: Option<&[bool]>,
14207) -> (Vec<usize>, Vec<f32>, f32) {
14208    let ne = logits.len();
14209    let p: Vec<f32> = if m.router_sigmoid {
14210        logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
14211    } else {
14212        let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
14213        let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
14214        let s: f32 = e.iter().sum();
14215        for v in &mut e {
14216            *v /= s;
14217        }
14218        e
14219    };
14220    // Expert restriction: the static env mask (CMF_MOE_MASK) AND the
14221    // active task mask's expert fields (spec §5) both narrow the
14222    // candidate set; selection happens over the admitted experts only.
14223    // With norm_topk the kept weights renormalize below; without it
14224    // the excluded mass is honestly dropped.
14225    let admit = |e: usize| {
14226        m.mask.as_ref().is_none_or(|mk| mk[e])
14227            && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
14228    };
14229    let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
14230    // Descending by selection score, lower index wins ties (torch.topk).
14231    match &m.expert_bias {
14232        Some(b) => idx.sort_unstable_by(|&x, &y| {
14233            (p[y] + b[y])
14234                .partial_cmp(&(p[x] + b[x]))
14235                .unwrap()
14236                .then(x.cmp(&y))
14237        }),
14238        None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
14239    }
14240    idx.truncate(m.top_k);
14241    // Adaptive τ-routing: trim the tail experts once the kept mass is
14242    // enough. wsum below renormalizes over the KEPT set, so the output
14243    // stays a proper weighted average.
14244    if let Some(tau) = m.route_tau {
14245        let total: f32 = idx.iter().map(|&e| p[e]).sum();
14246        if total > 0.0 {
14247            let mut acc = 0.0f32;
14248            let mut keep = idx.len();
14249            for (i, &e) in idx.iter().enumerate() {
14250                acc += p[e];
14251                if acc >= tau * total {
14252                    keep = i + 1;
14253                    break;
14254                }
14255            }
14256            idx.truncate(keep);
14257        }
14258    }
14259    let wsum: f32 = if m.norm_topk_prob {
14260        let s: f32 = idx.iter().map(|&e| p[e]).sum();
14261        // LFM2 floors the denom (matches HF `+ 1e-6`); the softmax path's
14262        // probs already sum near 1, so it stays exactly as before.
14263        (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
14264    } else {
14265        1.0 / m.routed_scaling
14266    };
14267    (idx, p, wsum)
14268}
14269
14270/// See the call site: one `layer:e1,e2,…` line per routed token.
14271fn moe_trace(idx: &[usize]) {
14272    moe_trace_at(crate::gpu::cur_layer() as i32, idx)
14273}
14274
14275/// The same, for callers that know their layer (DSV4 owns its layers and
14276/// never sets the pipeline's current-layer marker).
14277pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
14278    use std::io::Write;
14279    static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
14280        std::sync::OnceLock::new();
14281    let Some(f) = F.get_or_init(|| {
14282        let p = std::env::var("CMF_MOE_TRACE").ok()?;
14283        Some(std::sync::Mutex::new(
14284            std::fs::OpenOptions::new()
14285                .create(true)
14286                .append(true)
14287                .open(p)
14288                .ok()?,
14289        ))
14290    }) else {
14291        return;
14292    };
14293    let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
14294    let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
14295}
14296
14297/// MoE FFN: router → top-k experts (see `moe_route`). Only selected
14298/// experts' pages are touched in mmap.
14299pub(crate) fn moe_ffn(
14300    m: &MoeFfn,
14301    x: &[f32],
14302    pool: Option<&Pool>,
14303    allowed: Option<&[bool]>,
14304) -> Vec<f32> {
14305    accumulate_act(m, x, 1);
14306    let ne = m.experts.len();
14307    let mut logits = vec![0.0f32; ne];
14308    match &m.resonance {
14309        Some(r) => r.scores(x, &mut logits),
14310        None => m.router.matvec(x, &mut logits, pool),
14311    }
14312    let (idx, p, wsum) = moe_route(&logits, m, allowed);
14313    {
14314        let mut st = m.stats.borrow_mut();
14315        if st.len() < ne {
14316            st.resize(ne, 0);
14317        }
14318        for &e in &idx {
14319            st[e] += 1;
14320        }
14321    }
14322    // `CMF_MOE_TRACE=<file>`: append one line per (layer, token) with the
14323    // selected expert ids. The cumulative `stats` above answer "which
14324    // experts are popular"; a residency design needs the question they
14325    // cannot answer — whether CONSECUTIVE tokens reuse experts (the
14326    // temporal locality an LRU cache lives on, FreeToken §4).
14327    moe_trace(&idx);
14328    // D5: the whole layer MoE block in one GPU command buffer (experts — the
14329    // same mmap via a no-copy buffer; intermediate activations on the GPU).
14330    // Same Ffn probe class as the dense chain: one submit per layer
14331    // either wins on this driver stack or it doesn't.
14332    if crate::gpu::enabled_here() {
14333        match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
14334            crate::gpu::ProbeArm::Gpu => {
14335                let t0 = std::time::Instant::now();
14336                if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
14337                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
14338                    return out;
14339                }
14340            }
14341            crate::gpu::ProbeArm::CpuTimed => {
14342                let t0 = std::time::Instant::now();
14343                let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
14344                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
14345                return out;
14346            }
14347            crate::gpu::ProbeArm::Cpu => {
14348                return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
14349            }
14350        }
14351    }
14352    moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
14353}
14354
14355/// One-shot report of whether the whole-token wgpu graph actually formed.
14356/// A refusal silently reverts to the per-op path, which is how a model can
14357/// look "GPU-accelerated" while every layer walks the host.  A device prefix
14358/// is tracked separately because it still pays a host boundary for the tail.
14359fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
14360    use std::sync::atomic::{AtomicBool, Ordering};
14361    if built {
14362        GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
14363        if total_layers > 0 && layers_run < total_layers {
14364            GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
14365        } else {
14366            GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
14367        }
14368    } else {
14369        GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
14370    }
14371    static SAID: AtomicBool = AtomicBool::new(false);
14372    if !SAID.swap(true, Ordering::Relaxed) {
14373        if built {
14374            tracing::info!("wgpu whole-token graph: ACTIVE");
14375        } else {
14376            tracing::warn!("wgpu whole-token graph refused — per-op path");
14377        }
14378    }
14379}
14380
14381/// Whole-token graph outcomes, process-wide: a benchmark that claims a
14382/// GPU number while MISS climbs is measuring the CPU — the honest-bench
14383/// contract makes that an error, not a footnote.
14384pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
14385pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
14386/// Graph calls that returned a hidden after running only a leading device
14387/// prefix.  These are valid hybrid executions but must not be reported as a
14388/// full GPU graph in benchmark evidence.
14389pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
14390/// Graph calls that covered the complete requested layer span.
14391pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
14392
14393/// Native Metal TokenGraph completion counters. These are incremented only
14394/// after checked command-buffer completion and successful readback, so a
14395/// fused-head NLL report can prove the route rather than infer it from env.
14396pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
14397    std::sync::atomic::AtomicU64::new(0);
14398pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
14399    std::sync::atomic::AtomicU64::new(0);
14400pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
14401    std::sync::atomic::AtomicU64::new(0);
14402pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
14403    std::sync::atomic::AtomicU64::new(0);
14404pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
14405    std::sync::atomic::AtomicU64::new(0);
14406/// Ordinary native-Metal rows-prefill admissions and completed rows.  These
14407/// counters are separate from TokenGraph token/head counts so a batch NLL
14408/// receipt cannot accidentally claim serial execution as batched.
14409pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
14410    std::sync::atomic::AtomicU64::new(0);
14411pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
14412    std::sync::atomic::AtomicU64::new(0);
14413pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
14414    std::sync::atomic::AtomicU64::new(0);
14415pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
14416    std::sync::atomic::AtomicU64::new(0);
14417
14418/// `CMF_MOE_BATCH=0` restores the per-expert serial loop — the A/B lever
14419/// for the batched kernel, and how its bit-identity is checked.
14420fn moe_batch_enabled() -> bool {
14421    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14422    *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
14423}
14424
14425/// Two-dispatch CPU MoE: every routed expert (and the shared one) fused
14426/// into one gate/up/SiLU dispatch and one down dispatch, instead of two
14427/// pool barriers per expert. Bit-identical to the serial loop below —
14428/// see `moe_gate_up_many` / `moe_down_many`. `None` = the batched kernel
14429/// does not cover this layer, walk the serial path.
14430fn moe_ffn_cpu_batched(
14431    m: &MoeFfn,
14432    x: &[f32],
14433    idx: &[usize],
14434    p: &[f32],
14435    wsum: f32,
14436    pool: Option<&Pool>,
14437) -> Option<Vec<f32>> {
14438    if idx.is_empty() || !moe_batch_enabled() {
14439        return None;
14440    }
14441    // The bake probe reads per-neuron activation mass out of the
14442    // single-expert path; batching would skip it. Rare and offline —
14443    // hand those runs to the serial loop.
14444    if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
14445        return None;
14446    }
14447    let n = idx.len() + usize::from(m.shared.is_some());
14448    let mut pairs = Vec::with_capacity(n);
14449    let mut downs = Vec::with_capacity(n);
14450    let mut ws = Vec::with_capacity(n);
14451    for &e in idx {
14452        let d = &m.experts[e];
14453        if d.act != Act::Silu {
14454            return None;
14455        }
14456        pairs.push((&d.gate_proj, &d.up_proj));
14457        downs.push(&d.down_proj);
14458        ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
14459    }
14460    // The shared expert goes last, matching the serial loop's order —
14461    // the f32 accumulation order is part of the bit-identity claim.
14462    if let Some((se, gate)) = &m.shared {
14463        if se.act != Act::Silu {
14464            return None;
14465        }
14466        let g = gate.as_ref().map_or(1.0, |gate| {
14467            let mut gl = [0.0f32; 1];
14468            gate.matvec(x, &mut gl, pool);
14469            1.0 / (1.0 + (-gl[0]).exp())
14470        });
14471        pairs.push((&se.gate_proj, &se.up_proj));
14472        downs.push(&se.down_proj);
14473        ws.push(g);
14474    }
14475    let inter = pairs[0].0.rows();
14476    let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
14477    if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
14478        return None;
14479    }
14480    let mut out = attention::take_buf(x.len());
14481    if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
14482        attention::recycle_buf(&mut out);
14483        return None;
14484    }
14485    Some(out)
14486}
14487
14488/// Exact CPU completion for the routed experts a dynamic device cache did
14489/// not contain. The weights are already the router's final normalized mix.
14490/// Keeping this independent of `MoeFfn` makes the job `Sync`: its routing
14491/// statistics live in a `RefCell`, while the immutable expert tensors can be
14492/// evaluated safely in parallel with the GPU's resident subset.
14493pub(crate) fn moe_cold_experts_cpu(
14494    experts: &[(&DenseFfn, f32)],
14495    x: &[f32],
14496    pool: Option<&Pool>,
14497) -> Vec<f32> {
14498    let mut out = attention::take_buf(x.len());
14499    if experts.is_empty() {
14500        return out;
14501    }
14502    let pairs: Vec<_> = experts
14503        .iter()
14504        .map(|(e, _)| (&e.gate_proj, &e.up_proj))
14505        .collect();
14506    let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
14507    let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
14508    let inter = experts[0].0.gate_proj.rows();
14509    let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
14510    if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
14511        && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
14512    {
14513        return out;
14514    }
14515    out.fill(0.0);
14516    for &(expert, weight) in experts {
14517        let mut one = dense_ffn(expert, x, pool);
14518        for (o, v) in out.iter_mut().zip(&one) {
14519            *o += weight * v;
14520        }
14521        attention::recycle_buf(&mut one);
14522    }
14523    out
14524}
14525
14526/// The pure-CPU MoE expert loop (also the fallback of every GPU refusal).
14527fn moe_ffn_cpu(
14528    m: &MoeFfn,
14529    x: &[f32],
14530    idx: &[usize],
14531    p: &[f32],
14532    wsum: f32,
14533    pool: Option<&Pool>,
14534) -> Vec<f32> {
14535    if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
14536        return out;
14537    }
14538    let mut out = attention::take_buf(x.len());
14539    for &e in idx {
14540        let mut eo = dense_ffn(&m.experts[e], x, pool);
14541        let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
14542        for i in 0..out.len() {
14543            out[i] += w * eo[i];
14544        }
14545        attention::recycle_buf(&mut eo);
14546    }
14547    if let Some((se, gate)) = &m.shared {
14548        let mut so = dense_ffn(se, x, pool);
14549        let g = gate.as_ref().map_or(1.0, |gate| {
14550            let mut gl = [0.0f32; 1];
14551            gate.matvec(x, &mut gl, pool);
14552            1.0 / (1.0 + (-gl[0]).exp())
14553        });
14554        for i in 0..out.len() {
14555            out[i] += g * so[i];
14556        }
14557        attention::recycle_buf(&mut so);
14558    }
14559    out
14560}
14561
14562/// DeepSeek-V2 MLA forward, expand-to-MHA form (see `AttnKind::Mla`):
14563/// per token the latent expands to every head's K/V and the ordinary
14564/// cache + grouped attend do the rest. K head layout is [rope | nope]
14565/// (rotary_dim = qk_rope rotates the shared rope key and each q head's
14566/// prefix); V rows are zero-padded to the K head_dim inside the cache
14567/// and the pad is sliced off before O. Attention importance is not
14568/// accumulated for MLA yet (no eviction interplay).
14569#[allow(clippy::too_many_arguments)]
14570fn mla_attention(
14571    w: &MlaWeights,
14572    normed: &[f32],
14573    cache: &mut crate::kv_cache::LayerKvCache,
14574    position: usize,
14575    inv_freq: &[f32],
14576    rope_scale: f32,
14577    eps: f64,
14578    pool: Option<&Pool>,
14579) -> Vec<f32> {
14580    let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
14581    let hd = dr + dn;
14582    let mut q = vec![0.0f32; nh * hd];
14583    match (&w.q_a, &w.q_a_norm) {
14584        (Some(qa), Some(qn)) => {
14585            let mut t = vec![0.0f32; qa.rows()];
14586            qa.matvec(normed, &mut t, pool);
14587            let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
14588            w.q_proj.matvec(&tn, &mut q, pool);
14589        }
14590        _ => w.q_proj.matvec(normed, &mut q, pool),
14591    }
14592    let mut ca = vec![0.0f32; lora + dr];
14593    w.kv_a.matvec(normed, &mut ca, pool);
14594    let (c_lat, k_rope) = ca.split_at_mut(lora);
14595    let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
14596    let mut kvb = vec![0.0f32; nh * (dn + dv)];
14597    w.kv_b.matvec(&latn, &mut kvb, pool);
14598    if !w.nope {
14599        attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
14600    }
14601    for h in 0..nh {
14602        if !w.nope {
14603            attention::rope_rotate_scaled(
14604                &mut q[h * hd..h * hd + dr],
14605                position,
14606                inv_freq,
14607                rope_scale,
14608            );
14609        }
14610    }
14611    let mut k = vec![0.0f32; nh * hd];
14612    let mut v = vec![0.0f32; nh * hd];
14613    for h in 0..nh {
14614        k[h * hd..h * hd + dr].copy_from_slice(k_rope);
14615        k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
14616        v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
14617    }
14618    cache.append(&k, &v, &vec![true; nh]);
14619    let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
14620    attention::recycle_buf(&mut imp);
14621    let mut ov = vec![0.0f32; nh * dv];
14622    for h in 0..nh {
14623        ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
14624    }
14625    let mut out = vec![0.0f32; w.o_proj.rows()];
14626    w.o_proj.matvec(&ov, &mut out, pool);
14627    out
14628}
14629
14630/// Gemma-4 dual-branch FFN (spec: see `FfnKind::DenseMoe`). The dense
14631/// branch reads the pre-FFN-normed activation; the router and the
14632/// expert branch read the RAW residual — the router through a
14633/// scale-less rms norm (its constant gain is folded into the weights),
14634/// the experts through `pre_norm_2`. CPU path; GPU graphs refuse the
14635/// layer kind honestly.
14636fn dense_moe_ffn(
14637    dm: &DenseMoeFfn,
14638    x_normed: &[f32],
14639    h_raw: &[f32],
14640    eps: f64,
14641    norm_style: NormStyle,
14642    pool: Option<&Pool>,
14643) -> Vec<f32> {
14644    let mut d = dense_ffn(&dm.dense, x_normed, pool);
14645    d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
14646    let m = &dm.moe;
14647    let ne = m.experts.len();
14648    let mut logits = vec![0.0f32; ne];
14649    if m.router_input_norm {
14650        let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
14651        let inv = 1.0 / (ss + eps as f32).sqrt();
14652        let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
14653        m.router.matvec(&xr, &mut logits, pool);
14654    } else {
14655        m.router.matvec(h_raw, &mut logits, pool);
14656    }
14657    let (idx, p, wsum) = moe_route(&logits, m, None);
14658    {
14659        let mut st = m.stats.borrow_mut();
14660        if st.len() < ne {
14661            st.resize(ne, 0);
14662        }
14663        for &e in &idx {
14664            st[e] += 1;
14665        }
14666    }
14667    let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
14668    let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
14669    let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
14670    for (di, mi) in d.iter_mut().zip(&mo) {
14671        *di += mi;
14672    }
14673    d
14674}
14675
14676/// Building the MoE-layer GPU jobs: all selected experts (+shared) must
14677/// be q8_2f-Mapped from the primary mapping; otherwise None → CPU path.
14678/// One-shot report of why the MoE GPU block refused. A silent `?` here
14679/// sends every expert to the CPU with nothing in the logs to say so —
14680/// which is exactly how a q4tp MoE model looked "GPU-accelerated" while
14681/// running entirely on the host.
14682fn moe_gpu_refused(why: &'static str) {
14683    use std::sync::atomic::{AtomicBool, Ordering};
14684    static SAID: AtomicBool = AtomicBool::new(false);
14685    if !SAID.swap(true, Ordering::Relaxed) {
14686        tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
14687    }
14688}
14689
14690fn moe_ffn_gpu(
14691    m: &MoeFfn,
14692    x: &[f32],
14693    idx: &[usize],
14694    p: &[f32],
14695    wsum: f32,
14696    pool: Option<&Pool>,
14697) -> Option<Vec<f32>> {
14698    use crate::gpu::MoeJob;
14699
14700    let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
14701    let mut model_ref = None;
14702    for &e in idx {
14703        if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
14704            moe_gpu_refused("push_job(expert)");
14705            return None;
14706        }
14707    }
14708    if let Some((se, gate)) = &m.shared {
14709        let g = gate.as_ref().map_or(1.0, |gate| {
14710            let mut gl = [0.0f32; 1];
14711            gate.matvec(x, &mut gl, pool);
14712            1.0 / (1.0 + (-gl[0]).exp())
14713        });
14714        if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
14715            moe_gpu_refused("push_job(shared)");
14716            return None;
14717        }
14718    }
14719    let Some(model) = model_ref else {
14720        moe_gpu_refused("no model_ref");
14721        return None;
14722    };
14723    let hidden = jobs[0].down.1;
14724    let mut out = vec![0.0f32; hidden];
14725    if crate::gpu::moe_block(&model, &jobs, &mut out) {
14726        Some(out)
14727    } else {
14728        moe_gpu_refused("gpu::moe_block");
14729        None
14730    }
14731}
14732
14733/// Single-position FFN dispatch.
14734fn ffn_forward(
14735    ffn: &FfnKind,
14736    x: &[f32],
14737    pool: Option<&Pool>,
14738    experts_allowed: Option<&[bool]>,
14739) -> Vec<f32> {
14740    match ffn {
14741        FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
14742        FfnKind::Dense(d) => dense_ffn(d, x, pool),
14743        FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
14744        // Dual-branch layers need the raw residual — their callers
14745        // dispatch dense_moe_ffn directly; the auxiliary paths that land
14746        // here (MTP draft, o1 replay) do not co-occur with gemma-4 MoE.
14747        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
14748    }
14749}
14750
14751/// Fused two-position FFN: gate/up/down streamed once (dense). MoE
14752/// falls back to two singles — expert sets differ per position, there
14753/// is nothing to fuse.
14754fn ffn_forward_pair(
14755    ffn: &FfnKind,
14756    x1: &[f32],
14757    x2: &[f32],
14758    pool: Option<&Pool>,
14759    experts_allowed: Option<&[bool]>,
14760) -> (Vec<f32>, Vec<f32>) {
14761    let d = match ffn {
14762        // A tube layer has nothing to fuse across the pair — the tubes
14763        // are separate matrices; two singles are the honest path.
14764        FfnKind::Dense(d) if !d.segs.is_empty() => {
14765            return (
14766                tube_ffn(d, x1, 1, pool, None),
14767                tube_ffn(d, x2, 1, pool, None),
14768            );
14769        }
14770        FfnKind::Dense(d) => d,
14771        FfnKind::Moe(m) => {
14772            return (
14773                moe_ffn(m, x1, pool, experts_allowed),
14774                moe_ffn(m, x2, pool, experts_allowed),
14775            );
14776        }
14777        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
14778    };
14779    let inter = d.gate_proj.rows();
14780    FFN_SCRATCH.with(|s| {
14781        let mut s = s.borrow_mut();
14782        let [g1, g2, u1, u2] = &mut *s;
14783        g1.resize(inter, 0.0);
14784        g2.resize(inter, 0.0);
14785        u1.resize(inter, 0.0);
14786        u2.resize(inter, 0.0);
14787        // Multi-matrix pair job: gate+up under one pool dispatch
14788        // (o1s = lane-1 outputs across tensors, o2s = lane-2).
14789        QTensor::matvec2_many(
14790            [&d.gate_proj, &d.up_proj],
14791            x1,
14792            x2,
14793            [g1.as_mut_slice(), u1.as_mut_slice()],
14794            [g2.as_mut_slice(), u2.as_mut_slice()],
14795            pool,
14796        );
14797        for i in 0..inter {
14798            g1[i] = d.act.combine(g1[i], u1[i]);
14799            g2[i] = d.act.combine(g2[i], u2[i]);
14800        }
14801        let mut o1 = attention::take_buf(d.down_proj.rows());
14802        let mut o2 = attention::take_buf(d.down_proj.rows());
14803        d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
14804        (o1, o2)
14805    })
14806}
14807
14808#[cfg(test)]
14809mod tests {
14810
14811    #[test]
14812    fn nll_graph_policy_scopes_only_the_fused_head() {
14813        for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
14814            // A Vulkan/Wgpu hidden-only graph remains the quality route.
14815            ("vulkan graph", true, true, false, true, false),
14816            // Native Metal adds the strict fused graph-head contract.
14817            ("native Metal graph", true, true, true, true, true),
14818            // Masked NLL and the explicit non-graph fallback remain unchanged.
14819            ("masked", false, true, false, false, false),
14820            ("graph disabled", true, false, true, false, false),
14821        ] {
14822            let (graph_quality, graph_head_required) =
14823                super::nll_graph_policy(unmasked, prefer_graph, native_metal);
14824            assert_eq!(graph_quality, want_graph, "{label}: graph quality");
14825            assert_eq!(graph_head_required, want_head, "{label}: fused head");
14826        }
14827    }
14828
14829    #[test]
14830    fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
14831        assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
14832        assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
14833        assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
14834        assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
14835        assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
14836    }
14837
14838    #[test]
14839    fn cancel_flag_stops_generation() {
14840        let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
14841        // Set before the call: the prefill loops honour it, the run
14842        // returns immediately with the cancelled reason and no tokens.
14843        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14844        let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
14845        assert_eq!(r.finish_reason, "cancelled");
14846        assert!(
14847            r.token_ids.is_empty(),
14848            "no tokens after cancel: {:?}",
14849            r.token_ids
14850        );
14851        assert_eq!(p.kv_cache.seq_len(), 0);
14852        assert!(p.kv_history.is_empty());
14853        assert!(!p.graph_want_logits);
14854        assert!(p.graph_logits.is_none());
14855        // Flag auto-cleared: the next call generates normally.
14856        let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
14857        assert_ne!(r2.finish_reason, "cancelled");
14858    }
14859    use super::*;
14860
14861    /// sparse_ffn_quant must equal a dense FFN where inactive neurons are
14862    /// zeroed (mask × mmap correctness). On F32 tensors this is EXACT —
14863    /// it validates the row_dot / add_col_scaled / scatter indexing, the
14864    /// bug-prone part. The q8 branches reuse the golden-tested linear
14865    /// The per-token sparse path reads a transposed `down`; it must
14866    /// agree with the arm that computes everything and zeroes the
14867    /// losers, or the speed measurement is measuring a different model.
14868    #[test]
14869    fn dynamic_ffn_equals_the_zeroing_arm() {
14870        let (hidden, inter) = (8usize, 32usize);
14871        let synth = |n: usize, salt: usize| -> Vec<f32> {
14872            (0..n)
14873                .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
14874                .collect()
14875        };
14876        let down = synth(hidden * inter, 3);
14877        let mut down_t = vec![0.0f32; inter * hidden];
14878        for r in 0..hidden {
14879            for c in 0..inter {
14880                down_t[c * hidden + r] = down[r * inter + c];
14881            }
14882        }
14883        let d = DenseFfn {
14884            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
14885            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
14886            down_proj: QTensor::from_f32(down.clone(), hidden, inter),
14887            act: Act::Silu,
14888            down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
14889            segs: Vec::new(),
14890        };
14891        let x = synth(hidden, 11);
14892        let k = 12usize;
14893        let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
14894        // Reference: full compute, keep the k loudest |silu(gate)|.
14895        let mut g = vec![0.0f32; inter];
14896        d.gate_proj.matvec(&x, &mut g, None);
14897        let mut u = vec![0.0f32; inter];
14898        d.up_proj.matvec(&x, &mut u, None);
14899        for v in g.iter_mut() {
14900            *v = inference::silu(*v);
14901        }
14902        keep_top_k(&mut g, k);
14903        for i in 0..inter {
14904            g[i] *= u[i];
14905        }
14906        let mut want = vec![0.0f32; hidden];
14907        d.down_proj.matvec(&g, &mut want, None);
14908        for (a, b) in want.iter().zip(&got) {
14909            assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
14910        }
14911    }
14912
14913    /// A tube layer is the same layer, re-cut. With every tube open the
14914    /// answer must equal the dense FFN over the concatenated neurons
14915    /// (the permutation is an identity on the layer's function); with a
14916    /// tube closed it must equal the dense FFN with those neurons
14917    /// zeroed — the mask semantics, now paid for in bytes not read.
14918    #[test]
14919    fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
14920        let (hidden, core, tube) = (8usize, 12usize, 8usize);
14921        let inter = core + tube;
14922        let synth = |n: usize, salt: usize| -> Vec<f32> {
14923            (0..n)
14924                .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
14925                .collect()
14926        };
14927        let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
14928        let d_all = synth(hidden * inter, 3);
14929        // The dense layer, and the same weights cut into core + tube.
14930        let dense = DenseFfn {
14931            gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
14932            up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
14933            down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
14934            act: Act::Silu,
14935            down_t: None,
14936            segs: Vec::new(),
14937        };
14938        let rows =
14939            |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
14940        let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
14941            let mut o = Vec::with_capacity(hidden * (b - a));
14942            for r in 0..hidden {
14943                o.extend_from_slice(&v[r * inter + a..r * inter + b]);
14944            }
14945            o
14946        };
14947        let tubed = DenseFfn {
14948            down_t: None,
14949            gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
14950            up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
14951            down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
14952            act: Act::Silu,
14953            segs: vec![FfnSeg {
14954                gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
14955                up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
14956                down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
14957                start: core,
14958                width: tube,
14959            }],
14960        };
14961        let x = synth(hidden, 7);
14962        let want = dense_ffn(&dense, &x, None);
14963        let got = tube_ffn(&tubed, &x, 1, None, None);
14964        for (a, b) in want.iter().zip(&got) {
14965            assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
14966        }
14967        // Closed tube: bits on for the core, off for the tube.
14968        let mut bits = vec![0u8; inter.div_ceil(8)];
14969        for n in 0..core {
14970            bits[n / 8] |= 1 << (n % 8);
14971        }
14972        let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
14973        let masked = dense_ffn_masked(&dense, &x, None, &bits);
14974        for (a, b) in masked.iter().zip(&closed) {
14975            assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
14976        }
14977        // The batched arm must agree with the single-position one.
14978        let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
14979        for (a, b) in closed.iter().zip(&batch) {
14980            assert_eq!(a, b, "batch arm disagrees with decode arm");
14981        }
14982    }
14983
14984    /// scale, structurally identical to the matvec kernels.
14985    #[test]
14986    fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
14987        let (hidden, inter) = (16usize, 40usize);
14988        let synth = |n: usize, salt: usize| -> Vec<f32> {
14989            (0..n)
14990                .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
14991                .collect()
14992        };
14993        let d = DenseFfn {
14994            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
14995            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
14996            down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
14997            act: Act::Silu,
14998            down_t: None,
14999            segs: Vec::new(),
15000        };
15001        let x = synth(hidden, 9);
15002        // Active = every 3rd neuron.
15003        let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
15004
15005        let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
15006
15007        // Reference: full dense FFN but g[i]=0 for inactive neurons.
15008        let mut g = vec![0.0f32; inter];
15009        d.gate_proj.matvec(&x, &mut g, None);
15010        let mut u = vec![0.0f32; inter];
15011        d.up_proj.matvec(&x, &mut u, None);
15012        let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
15013        for i in 0..inter {
15014            g[i] = if act_set.contains(&(i as u16)) {
15015                inference::silu(g[i]) * u[i]
15016            } else {
15017                0.0
15018            };
15019        }
15020        let mut reference = vec![0.0f32; hidden];
15021        d.down_proj.matvec(&g, &mut reference, None);
15022
15023        let max_d = sparse
15024            .iter()
15025            .zip(&reference)
15026            .map(|(a, b)| (a - b).abs())
15027            .fold(0.0f32, f32::max);
15028        assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
15029    }
15030
15031    /// Attach a synthetic MTP head (same structure as a main layer).
15032    fn attach_test_mtp(p: &mut Pipeline) {
15033        let (h, inter, heads, kv, hd) = (
15034            p.hidden_size,
15035            p.intermediate_size,
15036            p.num_heads,
15037            p.num_kv_heads,
15038            p.head_dim,
15039        );
15040        let synth = |n: usize, salt: usize| -> Vec<f32> {
15041            (0..n)
15042                .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
15043                .collect()
15044        };
15045        let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
15046            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
15047        };
15048        p.mtp = Some(MtpModule {
15049            enorm: vec![1.0; h],
15050            hnorm: vec![1.0; h],
15051            eh_proj: qt(h, 2 * h, 301),
15052            layer: LayerWeights {
15053                input_norm: vec![1.0; h],
15054                post_norm: vec![1.0; h],
15055                attn_out_norm: None,
15056                ffn_out_norm: None,
15057                layer_scale: None,
15058                ffn: FfnKind::Dense(DenseFfn {
15059                    gate_proj: qt(inter, h, 315),
15060                    up_proj: qt(inter, h, 316),
15061                    down_proj: qt(h, inter, 317),
15062                    act: Act::Silu,
15063                    down_t: None,
15064                    segs: Vec::new(),
15065                }),
15066                attn: AttnKind::Full {
15067                    bias: None,
15068                    wq: qt(heads * hd, h, 311),
15069                    wk: qt(kv * hd, h, 312),
15070                    wv: qt(kv * hd, h, 313),
15071                    wo: qt(h, heads * hd, 314),
15072                    q_norm: None,
15073                    k_norm: None,
15074                    output_gate: false,
15075                    softplus_gate: None,
15076                },
15077            },
15078            final_norm: vec![1.0; h],
15079            kv: crate::kv_cache::LayerKvCache::new(kv, hd),
15080        });
15081    }
15082
15083    #[test]
15084    fn speculative_equals_vanilla_greedy() {
15085        // Speculative decode and the wgpu token graph are mutually
15086        // exclusive; a leaked CMF_GPU=wgpu from a parallel gpu test
15087        // would silently disable drafting. Pin the graph off.
15088        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
15089        let run = |spec: bool| {
15090            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15091            p.sampler_config.temperature = 0.0;
15092            attach_test_mtp(&mut p);
15093            p.speculative = spec;
15094            let r = p.generate("abcdef", 12, None, None).unwrap();
15095            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
15096        };
15097        let (vanilla, d0, _) = run(false);
15098        let (spec, d1, a1) = run(true);
15099        assert_eq!(d0, 0, "vanilla path must not draft");
15100        assert!(d1 > 0, "speculative path must draft");
15101        assert_eq!(
15102            vanilla, spec,
15103            "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
15104        );
15105    }
15106
15107    #[test]
15108    fn speculative_accepts_constant_oracle() {
15109        // See speculative_equals_vanilla_greedy: pin the wgpu graph off.
15110        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
15111        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15112        p.sampler_config.temperature = 0.0;
15113        p.sampler_config.repetition_penalty = 1.0;
15114        // Constant lm_head → every logit equal → both the main model and
15115        // the draft head argmax to token 0: acceptance must be 100%.
15116        p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
15117        attach_test_mtp(&mut p);
15118        p.speculative = true;
15119        let r = p.generate("abcd", 10, None, None).unwrap();
15120        assert!(r.mtp_drafted > 0);
15121        assert_eq!(
15122            r.mtp_accepted, r.mtp_drafted,
15123            "constant logits → every draft accepted"
15124        );
15125        // Ties resolve to the same token in both the main and draft
15126        // heads — the sequence is one repeated token.
15127        assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
15128    }
15129
15130    #[test]
15131    fn empty_prompt_is_an_error_not_a_panic() {
15132        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
15133        let r = p.generate("", 4, None, None);
15134        assert!(r.is_err(), "empty prompt must be a clean error");
15135    }
15136
15137    #[test]
15138    fn every_token_enters_kv_exactly_once() {
15139        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15140        // Greedy so no RNG variance; byte tokenizer → 3 prompt tokens.
15141        p.sampler_config.temperature = 0.0;
15142        let r = p.generate("abc", 2, None, None).unwrap();
15143        assert_eq!(r.prompt_tokens, 3);
15144        // prompt(3) + first sampled token forwarded before second logits:
15145        // step0 samples from prefill hidden (no extra forward), then
15146        // forwards t1 → cache 4; step1 samples, loop ends (max_tokens).
15147        assert_eq!(
15148            p.kv_cache.seq_len(),
15149            3 + r.tokens_generated - 1,
15150            "each token must be cached exactly once (v1 cached the last prompt token twice)"
15151        );
15152    }
15153
15154    #[test]
15155    fn generation_is_reproducible_with_seed() {
15156        let run = || {
15157            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15158            p.generate("hello", 8, None, None).unwrap().token_ids
15159        };
15160        assert_eq!(run(), run());
15161    }
15162
15163    #[test]
15164    fn resetting_sampler_restarts_the_seeded_stream() {
15165        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15166        let config = SamplerConfig {
15167            seed: Some(1234),
15168            ..SamplerConfig::default()
15169        };
15170        p.set_sampler_config(config.clone());
15171        let first = p.generate("hello", 8, None, None).unwrap().token_ids;
15172        p.set_sampler_config(config);
15173        let second = p.generate("hello", 8, None, None).unwrap().token_ids;
15174        assert_eq!(first, second);
15175    }
15176
15177    #[test]
15178    fn eviction_bounds_the_cache() {
15179        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
15180        p.kv_cache.max_seq_len = 6;
15181        p.sampler_config.temperature = 0.0;
15182        let _ = p.generate("abcd", 12, None, None).unwrap();
15183        assert!(
15184            p.kv_cache.seq_len() <= 6 + 1,
15185            "cache must stay bounded by max_seq_len (got {})",
15186            p.kv_cache.seq_len()
15187        );
15188    }
15189
15190    #[test]
15191    fn confidence_matches_tokens_and_is_a_probability() {
15192        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15193        p.sampler_config.temperature = 0.0;
15194        p.sampler_config.repetition_penalty = 1.0;
15195        let r = p.generate("abcd", 10, None, None).unwrap();
15196        assert_eq!(
15197            r.token_confidence.len(),
15198            r.token_ids.len(),
15199            "one confidence per emitted token"
15200        );
15201        for &c in &r.token_confidence {
15202            assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
15203        }
15204        // top1_prob is a valid softmax probability.
15205        let logits = [1.0f32, 3.0, 0.5, 3.0];
15206        let p0 = top1_prob_t(&logits, 1, 1.0);
15207        let p1 = top1_prob_t(&logits, 3, 1.0);
15208        assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
15209        assert!(p0 > 0.0 && p0 < 1.0);
15210        // Calibration temperature > 1 softens an over-confident peak.
15211        let sharp = top1_prob_t(&logits, 1, 1.0);
15212        let soft = top1_prob_t(&logits, 1, 2.0);
15213        assert!(soft < sharp, "higher temperature lowers peak confidence");
15214    }
15215
15216    #[test]
15217    fn trace_is_opt_in_and_parallels_the_output() {
15218        // Off by default: the runtime is silent unless observation asked.
15219        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15220        p.sampler_config.temperature = 0.0;
15221        p.sampler_config.repetition_penalty = 1.0;
15222        let r = p.generate("abcd", 10, None, None).unwrap();
15223        assert!(r.traces.is_empty(), "trace must be empty unless enabled");
15224
15225        // On: exactly one row per emitted token, aligned with the output.
15226        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15227        p.sampler_config.temperature = 0.0;
15228        p.sampler_config.repetition_penalty = 1.0;
15229        p.set_trace(true);
15230        let r = p.generate("abcd", 10, None, None).unwrap();
15231        assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
15232        for (i, tr) in r.traces.iter().enumerate() {
15233            assert_eq!(tr.t, i, "trace index is sequential");
15234            assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
15235            assert_eq!(
15236                tr.confidence, r.token_confidence[i],
15237                "trace confidence matches the confidence channel"
15238            );
15239            // No dynamic router in this pipeline → no skill, no coherence.
15240            assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
15241        }
15242    }
15243
15244    #[test]
15245    fn explain_prefill_logits_match_greedy_first_token() {
15246        // `cortiq explain` shows the next-token distribution from
15247        // prefill_next_logits; its argmax must equal what greedy generate
15248        // actually emits first — otherwise explain would lie.
15249        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15250        p.sampler_config.temperature = 0.0;
15251        p.sampler_config.repetition_penalty = 1.0;
15252        let ids = p.tokenizer.encode("abcd");
15253        let logits = p.prefill_next_logits(&ids, None);
15254        let argmax = logits
15255            .iter()
15256            .enumerate()
15257            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
15258            .unwrap()
15259            .0 as u32;
15260        let r = p.generate("abcd", 1, None, None).unwrap();
15261        assert_eq!(
15262            argmax, r.token_ids[0],
15263            "explain preview must match greedy emit"
15264        );
15265    }
15266
15267    #[test]
15268    fn laguna_shared_expert_is_unconditionally_added() {
15269        let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
15270        let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
15271        let zero_dense = || DenseFfn {
15272            gate_proj: matrix(vec![0.0; 4]),
15273            up_proj: matrix(vec![0.0; 4]),
15274            down_proj: matrix(vec![0.0; 4]),
15275            act: Act::Silu,
15276            down_t: None,
15277            segs: Vec::new(),
15278        };
15279        let shared = DenseFfn {
15280            gate_proj: identity(),
15281            up_proj: identity(),
15282            down_proj: identity(),
15283            act: Act::Silu,
15284            down_t: None,
15285            segs: Vec::new(),
15286        };
15287        let x = [1.0, 2.0];
15288        let expected = dense_ffn(&shared, &x, None);
15289        let moe = MoeFfn {
15290            router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
15291            experts: vec![zero_dense()],
15292            top_k: 1,
15293            norm_topk_prob: true,
15294            router_sigmoid: true,
15295            expert_bias: None,
15296            routed_scaling: 1.0,
15297            route_tau: None,
15298            shared: Some((shared, None)),
15299            stats: std::cell::RefCell::new(Vec::new()),
15300            act_sq: std::cell::RefCell::new(Vec::new()),
15301            act_rows: std::cell::RefCell::new(Vec::new()),
15302            mask: None,
15303            per_expert_scale: None,
15304            router_input_norm: false,
15305            resonance: None,
15306        };
15307        let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
15308        for (actual, expected) in actual.iter().zip(expected) {
15309            assert!((actual - expected).abs() < 1e-6);
15310        }
15311    }
15312
15313    #[test]
15314    fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
15315        const B: usize = 19;
15316        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15317        p.set_o1(Some(crate::nystrom::O1Cfg {
15318            layers: crate::nystrom::O1Layers::All,
15319            m: 4,
15320            w: 8,
15321            sink: 2,
15322            rect: crate::nystrom::O1Rect::Aggregate,
15323        }));
15324        p.o1_begin_with_prefix(Some(B));
15325        let ids: Vec<u32> = (0..B as u32).collect();
15326        let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
15327
15328        assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
15329        assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
15330        let next = p.embed_single(B as u32);
15331        let _ = p.forward_layers(&next, B, None);
15332        assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
15333    }
15334
15335    #[test]
15336    fn o1_pair_transition_commits_scratch_before_epoch_publication() {
15337        const B: usize = 19;
15338        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15339        // Keep a real recurrent layer ahead of the Full O(1) layer so the
15340        // pair test observes the GDN lane-2 scratch swap at the same
15341        // boundary, rather than only exercising an artificial scratch vec.
15342        let gdn_cfg = crate::linear_core::GdnCfg {
15343            num_v_heads: 2,
15344            num_k_heads: 1,
15345            key_head_dim: 2,
15346            value_head_dim: 4,
15347            conv_kernel: 3,
15348            hidden_size: 8,
15349            rms_eps: 1e-6,
15350            output_gate_sigmoid: false,
15351        };
15352        let synth = |n: usize, salt: usize| -> Vec<f32> {
15353            (0..n)
15354                .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
15355                .collect()
15356        };
15357        let qt = |rows: usize, cols: usize, salt: usize| {
15358            crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
15359        };
15360        let c_dim = gdn_cfg.conv_dim();
15361        let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
15362        p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
15363            in_proj_qkv: qt(c_dim, 8, 1),
15364            in_proj_z: qt(vd, 8, 2),
15365            in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
15366            in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
15367            conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
15368            a_log: vec![0.2, 0.5],
15369            dt_bias: synth(gdn_cfg.num_v_heads, 6),
15370            norm: vec![1.0; gdn_cfg.value_head_dim],
15371            out_proj: qt(8, vd, 7),
15372        });
15373        p.gdn_cfg = Some(gdn_cfg);
15374        p.set_o1(Some(crate::nystrom::O1Cfg {
15375            layers: crate::nystrom::O1Layers::All,
15376            m: 4,
15377            w: 8,
15378            sink: 2,
15379            rect: crate::nystrom::O1Rect::Aggregate,
15380        }));
15381        p.o1_begin_with_prefix(Some(B));
15382        for pos in 0..B - 2 {
15383            let emb = p.embed_single(pos as u32);
15384            let _ = p.forward_layers(&emb, pos, None);
15385        }
15386        let lane1_state = p.kv_cache.layers[0].linear_state.clone();
15387
15388        let e1 = p.embed_single((B - 2) as u32);
15389        let e2 = p.embed_single((B - 1) as u32);
15390        let _ = p.forward_pair(&e1, &e2, B - 2);
15391
15392        assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
15393        assert!(
15394            p.kv_cache
15395                .layers
15396                .iter()
15397                .enumerate()
15398                .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
15399        );
15400        assert!(!p.kv_cache.layers[0].linear_state.is_empty());
15401        assert_ne!(
15402            p.kv_cache.layers[0].linear_state, lane1_state,
15403            "real pair must commit GDN lane 2 before returning"
15404        );
15405        assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
15406        let next = p.embed_single(B as u32);
15407        let _ = p.forward_layers(&next, B, None);
15408        assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
15409    }
15410
15411    #[test]
15412    fn o1_error_observation_stays_terminal_until_reset() {
15413        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15414        p.set_o1(Some(crate::nystrom::O1Cfg {
15415            layers: crate::nystrom::O1Layers::All,
15416            m: 4,
15417            w: 8,
15418            sink: 2,
15419            rect: crate::nystrom::O1Rect::Aggregate,
15420        }));
15421        p.o1_begin();
15422        p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
15423
15424        assert!(p.o1_seal_checked().is_err());
15425        assert!(
15426            p.o1_seal_checked().is_err(),
15427            "retry must see the sticky error"
15428        );
15429        let k = vec![0.2f32; 4];
15430        let v = vec![0.3f32; 4];
15431        p.kv_cache.layers[0].append(&k, &v, &[]);
15432        assert_eq!(p.kv_cache.layers[0].seq_len, 0);
15433
15434        p.reset_session();
15435        p.o1_begin();
15436        p.kv_cache.layers[0].append(&k, &v, &[]);
15437        assert_eq!(p.kv_cache.layers[0].seq_len, 1);
15438    }
15439
15440    #[test]
15441    fn nll_graph_failure_is_terminal_and_request_is_reusable() {
15442        let ids = vec![1u32, 2, 3, 4, 5, 6];
15443        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15444        p.graph_logits = Some(vec![123.0]);
15445        p.graph_want_logits = true;
15446        p.graph_failed
15447            .store(true, std::sync::atomic::Ordering::Relaxed);
15448        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
15449        let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
15450        assert!(err.contains("before NLL"));
15451        assert!(p.graph_logits.is_none());
15452        assert!(!p.graph_want_logits);
15453        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
15454        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
15455
15456        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15457        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
15458        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
15459        assert_eq!(actual.1, expected.1);
15460        assert!((actual.0 - expected.0).abs() < 1e-9);
15461    }
15462
15463    #[test]
15464    fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
15465        let ids = vec![1u32, 2, 3, 4, 5, 6];
15466        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15467        p.nll_test_fail_at = Some(1);
15468        let err = p
15469            .nll_ids_from(&ids, 0)
15470            .expect_err("one-shot forward failure");
15471        assert!(err.contains("forward") || err.contains("score row"));
15472        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
15473        assert!(!p.graph_want_logits);
15474        assert!(p.graph_logits.is_none());
15475        assert!(p.kv_history.is_empty());
15476
15477        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15478        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
15479        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
15480        assert_eq!(actual.1, expected.1);
15481        assert!((actual.0 - expected.0).abs() < 1e-9);
15482    }
15483
15484    #[test]
15485    fn nll_serial_failure_before_first_row_is_reported() {
15486        let ids = vec![1u32, 2, 3, 4];
15487        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15488        p.nll_test_force_serial = true;
15489        p.nll_test_fail_at = Some(0);
15490        let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
15491        assert!(err.contains("serial forward"));
15492        assert!(p.kv_history.is_empty());
15493        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
15494        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
15495    }
15496
15497    #[test]
15498    fn ffn_probe_failure_discards_recorder_and_state() {
15499        let ids = vec![1u32, 2, 3, 4];
15500        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15501        p.nll_test_fail_at = Some(0);
15502        let err = p
15503            .probe_ffn_mass_batch(&ids)
15504            .expect_err("probe forward failure");
15505        assert!(err.contains("NLL"));
15506        assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
15507        assert!(p.kv_history.is_empty());
15508        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
15509    }
15510
15511    #[test]
15512    fn nll_test_controls_are_pipeline_scoped() {
15513        let ids = vec![1u32, 2, 3, 4];
15514        let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15515        let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15516        failing.nll_test_force_serial = true;
15517        failing.nll_test_fail_at = Some(0);
15518
15519        assert!(!failing.can_prefill_batched());
15520        assert!(unaffected.can_prefill_batched());
15521        let expected = unaffected
15522            .nll_ids_from(&ids, 0)
15523            .expect("unaffected pipeline remains usable");
15524        let err = failing
15525            .nll_ids_from(&ids, 0)
15526            .expect_err("failure injection belongs to failing pipeline");
15527        assert!(err.contains("serial forward"));
15528        assert!(failing.nll_test_fail_at.is_none());
15529        assert!(unaffected.can_prefill_batched());
15530        let actual = unaffected
15531            .nll_ids_from(&ids, 0)
15532            .expect("unaffected pipeline remains reusable");
15533        assert_eq!(actual.1, expected.1);
15534        assert!((actual.0 - expected.0).abs() < 1e-9);
15535    }
15536
15537    #[test]
15538    fn forward_ids_failure_channel_is_terminal_and_reusable() {
15539        let ids = vec![1u32, 2, 3, 4, 5, 6];
15540        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15541        p.graph_logits = Some(vec![123.0]);
15542        p.graph_want_logits = true;
15543        p.graph_failed
15544            .store(true, std::sync::atomic::Ordering::Relaxed);
15545        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
15546
15547        let err = p
15548            .forward_ids(&ids, None)
15549            .expect_err("a failed forward must not become a valid head result");
15550        assert!(err.contains("forward_ids setup"));
15551        assert!(p.graph_logits.is_none());
15552        assert!(!p.graph_want_logits);
15553        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
15554        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
15555        assert_eq!(p.kv_cache.seq_len(), 0);
15556
15557        let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
15558            .forward_ids(&ids, None)
15559            .expect("fresh forward_ids");
15560        let actual = p
15561            .forward_ids(&ids, None)
15562            .expect("pipeline remains reusable after a failed forward");
15563        assert_eq!(actual.len(), expected.len());
15564        assert!(
15565            actual
15566                .iter()
15567                .zip(expected)
15568                .all(|(a, b)| (a - b).abs() < 1e-9)
15569        );
15570        assert_eq!(p.kv_cache.seq_len(), ids.len());
15571    }
15572}