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;
11
12/// MiMo-V2 multi-token prediction (draft stack + speculative round). A
13/// child module so it runs on the pipeline's own helpers.
14#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16#[path = "clover.rs"]
17pub mod clover;
18/// d1's non-causal forward over a packed state and question set.
19#[path = "pipeline_masked.rs"]
20pub mod masked;
21#[path = "pipeline_speech.rs"]
22pub mod speech;
23use crate::kv_cache::KvCache;
24use crate::linear_core::{
25    GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
26    gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
27    vmf_phase_pair,
28};
29use crate::pool::Pool;
30use crate::qtensor::QTensor;
31use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
32use crate::tokenizer::Tokenizer;
33use cortiq_core::mask::TaskMask;
34use cortiq_core::types::NormStyle;
35
36pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
37    std::sync::atomic::AtomicBool::new(false);
38
39/// One MoE layer's expert bank as the resident token graph reads it: the
40/// `(gate, up, down)` tensor indices in routing order (the in-bank shared
41/// expert last), the layer's uniform layout flags and activation, and the
42/// file that holds them. A pure function of the layer's weights — walking
43/// 257 experts × 3 matrices per layer per token was ~2 ms of host time a
44/// token on a 40-layer 256-expert stack, so the walk runs once a layer.
45#[derive(Clone)]
46struct GraphMoeBank {
47    experts: Vec<(usize, usize, usize)>,
48    q4tp: Option<bool>,
49    gu_q2: Option<bool>,
50    q82: Option<bool>,
51    act: Option<crate::gpu::GraphAct>,
52    model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
53}
54
55/// Identity of the weights a `GraphMoeBank` was built from: the physical
56/// layer, the expert table's storage and length, the in-bank shared expert,
57/// and the first and last expert tensors' (file, index) — so a table
58/// rebuilt at a reused address (a skill, a grown expert) cannot alias an
59/// old entry.
60type GraphMoeKey = (usize, usize, usize, usize, usize, usize, usize);
61
62// MTP compaction statistics must describe the dense draft that will be
63// serialized, not an ambient serving experiment such as gate/oracle top-k.
64// A thread-local nesting counter keeps the guard local to calibration and
65// avoids mutating process-global environment variables mid-command.
66thread_local! {
67    static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
68}
69
70struct ExactMtpCalibrationGuard;
71
72impl ExactMtpCalibrationGuard {
73    fn enter() -> Self {
74        EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
75        Self
76    }
77}
78
79impl Drop for ExactMtpCalibrationGuard {
80    fn drop(&mut self) {
81        EXACT_MTP_CALIBRATION.with(|depth| {
82            debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
83            depth.set(depth.get().saturating_sub(1));
84        });
85    }
86}
87
88fn exact_mtp_calibration_active() -> bool {
89    EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
90}
91
92/// Extend an MTP draft-head shortlist to the model vocabulary.  Rows outside
93/// the shortlist were deliberately not evaluated, so they must be impossible
94/// draft candidates rather than neutral zero logits.
95fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
96    logits.resize(vocab_size, f32::NEG_INFINITY);
97}
98
99/// Reusable per-pipeline forward scratch: the four norm outputs the
100/// decode paths recompute every layer (single: n1/p1; pair: all four).
101/// Plain buffers, resized once — steady-state decode reuses them.
102struct ForwardScratch {
103    n1: Vec<f32>,
104    n2: Vec<f32>,
105    p1: Vec<f32>,
106    p2: Vec<f32>,
107}
108
109impl ForwardScratch {
110    fn new(hidden: usize) -> Self {
111        Self {
112            n1: vec![0.0; hidden],
113            n2: vec![0.0; hidden],
114            p1: vec![0.0; hidden],
115            p2: vec![0.0; hidden],
116        }
117    }
118}
119
120/// Complete inference pipeline state.
121pub struct Pipeline {
122    /// In-process layer split across local GPUs: (device, first layer,
123    /// last layer) per segment, in execution order. `None` = one device.
124    /// Arc so cloning the plan out of `&mut self` does not fight the
125    /// borrow checker on the hot path.
126    gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
127    /// Arc: the server shares one tokenizer handle across request
128    /// handlers without borrowing a pipeline slot.
129    pub tokenizer: std::sync::Arc<Tokenizer>,
130    pub kv_cache: KvCache,
131    pub sampler_config: SamplerConfig,
132    pub weights: PipelineWeights,
133    pub hidden_size: usize,
134    pub intermediate_size: usize,
135    pub num_heads: usize,
136    pub num_kv_heads: usize,
137    pub head_dim: usize,
138    /// Total virtual layers (num_layers × num_loops for looped models).
139    pub num_layers: usize,
140    /// Physical layers in weights.layers (≤ num_layers for looped models).
141    pub physical_layers: usize,
142    /// Looped Transformer: apply final norm after each loop iteration.
143    pub loop_final_norm: bool,
144    /// SenseNova-Loop: virtual → physical layer order (None: `v %
145    /// physical_layers`, the whole-stack loop of Nanbeige).
146    pub layer_map: Option<Vec<usize>>,
147    /// SenseNova-Loop: the mHC routers (`sensenova::Mhc`).
148    pub mhc: Option<Box<crate::sensenova::Mhc>>,
149    /// The decode token's mHC streams, `[streams · hidden]`.
150    mhc_r: Vec<f32>,
151    /// Interleaved-MRoPE sections and the prompt rows' (t, h, w) positions
152    /// for the prefill of an image prompt (SenseNova-Loop); cleared when
153    /// the prefill ends.
154    pub mrope_sections: Option<[usize; 3]>,
155    pub prompt_mrope: Option<Vec<[i64; 3]>>,
156    pub vocab_size: usize,
157    pub rms_eps: f64,
158    pub rope_base: f32,
159    pub norm_style: NormStyle,
160    /// RoPE dims actually rotated (≤ head_dim; Qwen3.5 uses head_dim/4).
161    pub rotary_dim: usize,
162    /// Optional Q-head count override for each attention layer (Laguna).
163    pub attention_heads_per_layer: Option<Vec<usize>>,
164    /// Optional KV-head count of each PHYSICAL attention layer (MiMo-V2:
165    /// 4 on full-attention layers, 8 on sliding ones). Set only through
166    /// [`Pipeline::set_attn_geometry`], which also reshapes the layer
167    /// caches. None = every layer has `num_kv_heads`.
168    pub kv_heads_per_layer: Option<Vec<usize>>,
169    /// Width of each V head when it is narrower than `head_dim` (MiMo-V2:
170    /// 128 against 192). V is zero-padded to `head_dim` inside the cache
171    /// and the attention output is compacted back to nh·v_head_dim before
172    /// o_proj (see `QwenAttnCfg::v_head_dim`). None = `head_dim`.
173    pub v_head_dim: Option<usize>,
174    /// `CMF_LAYER_DUMP=<dir>` (read once at construction; tests set it
175    /// directly): the hidden state after every layer, for every position,
176    /// as raw little-endian f32 files `p{pos:06}_l{li:02}.f32` of
177    /// `hidden_size` floats each. Written by the CPU layer walks — the
178    /// batched prefill (`prefill_batch_span`) and the single-token forward
179    /// (`forward_layers_span`) — so both prompt ingest and decode can be
180    /// diffed layer by layer against an external oracle (tools/mimo_ref).
181    /// Layers a device graph runs (wgpu token/batch graph, Metal chunk or
182    /// block graphs) and the own-stack families (DeepSeek-V4/V4.1,
183    /// Qwen3.8-Flash-Next, Gemma-3n) are not dumped: run with CMF_GPU=0
184    /// for a complete set. `li` is the virtual layer index. Final logits:
185    /// `CMF_LOGIT_DUMP=<file>` (hidden + logits of the first decode step).
186    pub layer_dump: Option<std::path::PathBuf>,
187    /// GPU-graph declines already logged for this pipeline, as (graph
188    /// site, reason) — one line each, see `graph_attn_decline_reason`.
189    graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
190    /// MiMo-V2 expert placement (prefix / dynamic bank / hybrid), decided
191    /// on the first forward — see `crate::mimo_moe`.
192    pub(crate) mimo_moe: crate::mimo_moe::Slot,
193    /// Opt-in MiniCPM q4tp expert cache for a graph-produced CPU tail.
194    /// It is deliberately separate from MiMo's placement policy because it
195    /// serves only the stock MiniCPM top-8 geometry and otherwise falls back
196    /// to the ordinary exact host MoE path.
197    pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
198    /// Linear-core geometry (present when the model has linear layers).
199    pub vmf_cfg: Option<VmfPhaseCfg>,
200    /// GatedDeltaNet geometry (faithful vendor operator).
201    pub gdn_cfg: Option<GdnCfg>,
202    /// MiniCPM-class logit scale (tied lm_head → cannot fold into weights).
203    pub logit_multiplier: Option<f32>,
204    /// Cooperative cancel: set from any thread (FFI `cortiq_cancel`,
205    /// a dropped server connection); the generate loop checks it at
206    /// every prefill chunk and decode step and finishes with
207    /// `finish_reason: "cancelled"`. Auto-cleared when honoured.
208    pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
209    /// A GPU graph failure is distinct from a user/request cancellation.
210    /// Graph code sets this before raising the cooperative cancel flag so the
211    /// generation API can return an error instead of reporting a successful
212    /// `finish_reason: cancelled` result.
213    graph_failed: std::sync::atomic::AtomicBool,
214    /// Resident-graph MoE expert banks, built once per layer
215    /// (`GraphMoeBank`). `CMF_GRAPH_MOE_BANK=0` rebuilds them every token.
216    graph_moe_banks: std::sync::Mutex<std::collections::HashMap<GraphMoeKey, GraphMoeBank>>,
217    /// Token ids currently materialized in the KV cache (the forwarded
218    /// prompt + all generated tokens except the last, which is sampled
219    /// but not yet forwarded). Lets the next generate call prefill only
220    /// the suffix when a chat app resends the whole history.
221    pub kv_history: Vec<u32>,
222    /// Owner tag of `kv_history`: the resident device graph held that
223    /// sequence (true) or the host did. Reuse continues only on the owner.
224    pub kv_history_device: bool,
225    /// KDA geometry (Kimi Linear / Kimi-K3) — shared by every Kda layer.
226    pub kda_cfg: Option<crate::linear_core::KdaCfg>,
227    /// Gemma-3n stack (AltUp/LAuReL/PLE/KV-sharing): its own forward —
228    /// weights.layers stays empty, the KV caches are the shared ones.
229    pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
230    /// Gemma-4 E-series stack (dual-geometry gemma4 + PLE + shared KV):
231    /// its own per-token forward, same contract as `g3n`.
232    pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
233    /// DeepSeek-V4 runs its own stack too: its hidden state is `hc_mult`
234    /// copies of a vector, so no loop written for a single residual
235    /// stream can carry it.
236    pub dsv4: Option<
237        Box<(
238            crate::dsv4::Dsv4Globals,
239            Vec<crate::dsv4::Dsv4Layer>,
240            crate::dsv4::Dsv4Cfg,
241            crate::dsv4::Dsv4State,
242        )>,
243    >,
244    /// DeepSeek-V4.1 owns the shared CED/CSA2 attention state, raw Engram
245    /// lookup and four-stream mHC handoff. It cannot use the V4 cache
246    /// layout, so it has a dedicated executor and state tuple.
247    pub dsv41: Option<
248        Box<(
249            crate::dsv41::Dsv41Globals,
250            Vec<crate::dsv41::Dsv41Layer>,
251            crate::dsv41::Dsv41Cfg,
252            crate::dsv41::Dsv41State,
253        )>,
254    >,
255    /// Optional V4.1 vision tower. Text-only files leave this unset.
256    pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
257    /// Prepared image rows consumed by the next V4.1 prefill.
258    dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
259    /// Qwen3.8-Flash-Next owns four residual streams plus QSA/PLE state;
260    /// the generic single-residual layer loop cannot represent it.
261    /// Block-diffusion generation (Clover): preview weights and the
262    /// decoding controls; `None` on every causal model.
263    pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
264    clover_stats: clover::CloverStats,
265    pub qwen4_exp: Option<
266        Box<(
267            crate::qwen4_exp::Globals,
268            Vec<crate::qwen4_exp::Layer>,
269            crate::qwen4_exp::Cfg,
270            crate::qwen4_exp::State,
271        )>,
272    >,
273    /// DeepSeek-V4's own speculation stack: three draft modules, each a full
274    /// layer, plus a confidence head on the last. Empty when the file has
275    /// none, which is the only signal the decode path needs.
276    pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
277    /// The draft's per-sequence state (KV rings, captured trunk hidden).
278    pub dspark: Option<crate::dsv4::DsparkState>,
279    /// Drafts awaiting their verdict: (position, proposals, still matching,
280    /// accepted so far).
281    pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
282    /// Accepted prefix length of every graded draft.
283    pub dspark_hist: Vec<usize>,
284    /// The real tokens the drafts were graded against — a degenerate,
285    /// repeating output would make any acceptance number meaningless, and
286    /// the cheapest guard against believing one is to count them.
287    pub dspark_real: Vec<u32>,
288    /// The trunk's expert picks for the last few tokens, per layer. The
289    /// union over a window of them is what a batched verify would have to
290    /// read, and the ratio to the pick count is all it could save.
291    pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
292    /// (unique, total) expert picks per draft, trunk side and draft side.
293    pub dspark_exp: Vec<(usize, usize, usize, usize)>,
294    /// Wall time spent in the deliberately out-of-core draft. Kept separate
295    /// from trunk decode so block batching can be judged without conflating
296    /// it with GPU chain variance.
297    pub dspark_draft_ns: u128,
298    /// Per-layer winners of the last pooled route — the admission predictor.
299    m3_winners: Vec<Vec<u32>>,
300    /// Route frequency per (layer, expert) — the bank's prewarm profile.
301    m3_counts: Vec<u32>,
302    m3_n_exp: usize,
303    m3_tokens: u64,
304    m3_prewarmed: bool,
305    /// LFM2 short-convolution geometry (present when the model has
306    /// `ShortConv` mixer layers).
307    pub short_conv_cfg: Option<ShortConvCfg>,
308    /// Multi-token-prediction head (None = absent).
309    pub mtp: Option<MtpModule>,
310    /// MiMo-V2's draft stack (three chained MTP layers from the
311    /// `<stem>.mtp.cmf` sidecar); None = absent. Speculative greedy decode
312    /// uses it unless `CMF_MTP=0` / `CMF_MIMO_MTP=0`.
313    pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
314    /// Set while the MiMo speculative verify runs `prefill_batch`: its MoE
315    /// layers take `moe_ffn_rows_exact` (each row bit-identical to decode).
316    verify_exact_moe: bool,
317    /// Speculative decode via MTP (greedy only; `CMF_MTP=0` disables).
318    pub speculative: bool,
319    /// Keep generating past end-of-sequence ids (the llama-bench contract
320    /// for a timed run). A loop flag, deliberately NOT a sampler
321    /// suppression: suppressed ids count as a penalty and switch the
322    /// speculative round and the greedy burst off, so a benchmark that
323    /// suppressed EOS never measured either.
324    pub ignore_eos: bool,
325    /// Draft-head shortlist guard: tokens left during which the draft
326    /// uses the FULL head because a recently committed id lay past the
327    /// `CMF_DRAFT_VOCAB` cut (Cyrillic and CJK ids sit above 131072 in
328    /// Qwen's table, so a prefix shortlist would draft nothing usable
329    /// there — measured on Russian prose: 2.9 → 1.6 accepted a round).
330    pub draft_full_streak: u32,
331    /// Adaptive draft depth for the speculative round (None until the
332    /// first round): grows while nearly every draft is accepted, shrinks
333    /// when fewer than half are. The verify's cost climbs with the rows on
334    /// a discrete card (RTX PRO 4000: 52 ms at 2 rows, 74 at 5, 80 at 6),
335    /// so prose wants k≈3 and code or the repetitive bench k≈5 — measured
336    /// 33.6 vs 27.6 tok/s on an essay at k=3 vs 5, 45.6 vs 38 on code.
337    /// `CMF_GRAPH_SPEC_K` pins it.
338    pub spec_k_adapt: Option<usize>,
339    /// EWMA of the accepted fraction that drives `spec_k_adapt`.
340    pub spec_acc_ewma: f32,
341    rng: SplitMix64,
342    sampler_scratch: SamplerScratch,
343    /// Speculative SAMPLING state (graph_spec_step, temperature > 0): the
344    /// correction token a rejected draft produced — committed by the loop
345    /// top in place of a fresh draw — and the per-round draft
346    /// distributions / target scratch, reused so a round allocates
347    /// nothing at the vocab size.
348    spec_forced: Option<u32>,
349    spec_q: Vec<Vec<f32>>,
350    spec_p: Vec<f32>,
351    spec_res: Vec<f32>,
352    /// The same three for the sparse chain (top-k configs).
353    spec_qs: Vec<sampler::Sparse>,
354    spec_ps: sampler::Sparse,
355    spec_ress: sampler::Sparse,
356    /// Which arm the MTP draft block runs on this generation: Some(true)
357    /// = the whole-token graph (device attention, one submit a step),
358    /// Some(false) = the per-op path; None = not decided yet. Decided
359    /// on the first draft and held, because the two arms keep the MTP
360    /// KV in different places (device mirror vs the CPU cache) and a
361    /// mid-run switch would read the wrong one.
362    mtp_graph_mode: Option<bool>,
363    /// The Metal verify graph of the round in flight, between its sync
364    /// (logits read) and the commit that replays the accepted prefix.
365    #[cfg(target_os = "macos")]
366    metal_verify: Option<MetalVerifyPending>,
367    /// Precomputed RoPE inverse frequencies [head_dim/2]. Arc: the
368    /// forward path clones a handle to escape the &mut self borrow —
369    /// cloning the table itself was a per-forward allocation.
370    pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
371    /// Reusable norm buffers for the decode hot path (roadmap §3 P0:
372    /// steady-state forward should not heap-allocate). Disjoint field
373    /// from `weights`/`kv_cache`, so split borrows keep working.
374    ws: ForwardScratch,
375    /// Persistent worker pool (None = serial; see CMF_THREADS).
376    pool: Option<std::sync::Arc<Pool>>,
377    // ── Dynamic per-token skill routing (spec §9, claim 14/16) ──
378    /// Source model, retained so a skill switch can re-resolve the
379    /// touched layers' FFN tensors (Mapped = mmap pointers, cheap).
380    pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
381    /// Masks present → weights are dequantized f32 (rebuild path).
382    pub(crate) dyn_force_f32: bool,
383    /// Per-skill FFN layers actually replaced (derived from tensors, not
384    /// the meta `layers` field — ru2 replaces down_proj in 0..23 while
385    /// its meta says [20..23]). None = skill touches non-FFN tensors →
386    /// ineligible for cheap dynamic switching (honest refusal).
387    pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
388    /// Currently overlaid skill (index into model.header.skills); None =
389    /// backbone. Set at load time to the statically-overlaid skill so
390    /// `set_active_skill(None)` correctly reverts it (else a static
391    /// skill would silently persist — the union-diff assumes dyn_active
392    /// always mirrors the live overlay). Switched by `set_active_skill`.
393    pub(crate) dyn_active: Option<usize>,
394    /// Pipeline was loaded with a soft blend (materialized working
395    /// tensors, not a single skill index) → dynamic routing refuses:
396    /// there is no single index to revert the blend from.
397    pub(crate) dyn_blend_loaded: bool,
398    /// Layer whose post-residual hidden feeds the router φ (shared by
399    /// swarm skills). None = φ capture off.
400    pub(crate) dyn_phi_layer: Option<usize>,
401    /// EMA of φ at `dyn_phi_layer` over the decode window (on-policy).
402    dyn_phi_ema: Vec<f32>,
403    dyn_phi_seen: usize,
404    /// Hysteresis router driving per-token skill switches during decode
405    /// (None = static/no dynamic routing). Taken out during generation.
406    pub dyn_router: Option<crate::swarm::DynRouter>,
407    /// O(1) Nyström attention setting (CLI/env/header-hint resolved by
408    /// the caller; None = plain cache attention everywhere).
409    o1_cfg: Option<crate::nystrom::O1Cfg>,
410    /// Bumped once per collecting→sealed transition — the GPU state mirror
411    /// re-uploads when it sees a new epoch (each fresh sealed state).
412    o1_epoch: u64,
413    /// Per-layer o1 flags derived from `o1_cfg` (Full layers only).
414    o1_flags: Vec<bool>,
415    /// Emit a structured per-token trace (B4 telemetry channel). Off by
416    /// default — the runtime is silent unless observation is requested.
417    trace: bool,
418    /// Confidence-calibration temperature (B1): reported probability is
419    /// softmax(logits / calib_temp). 1.0 = raw. Set from header.calibration.
420    calib_temp: f32,
421    /// Process-unique id keying this pipeline's device KV mirrors.
422    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
423    graph_kv_id: u64,
424    /// Decode asks the token graph to also run final-norm + lm_head on
425    /// the device (drops the separate per-op lm_head round trip).
426    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
427    graph_want_logits: bool,
428    /// NLL quality gates require the graph's fused head rather than silently
429    /// accepting a CPU head fallback. Generation keeps the historical
430    /// best-effort `graph_want_logits` behavior.
431    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
432    graph_head_required: bool,
433    /// Logits the graph produced for the token just forwarded (taken by
434    /// the decode loop; None = compute on the CPU path).
435    graph_logits: Option<Vec<f32>>,
436    /// Packed Embryo graph model, built lazily on the first eligible
437    /// resident call so other architectures pay no packing cost.
438    embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
439    /// This pipeline's token graph refused for a STRUCTURAL reason (its
440    /// own weights / layer kinds): not retried. Per pipeline, never
441    /// process-wide: a skill lane whose pack does not build must not flip
442    /// the backbone slot mid-sequence onto the host path (and a new lane
443    /// must not clear the backbone's verdict) — R4/NF-2.
444    graph_refused: std::sync::atomic::AtomicBool,
445    /// Token embeddings are multiplied by this at input (Gemma: √hidden).
446    pub embed_multiplier: f32,
447    /// Attention score scale (1/√head_dim unless the arch overrides —
448    /// Gemma's query_pre_attn_scalar).
449    pub attn_scale: f32,
450    /// Sliding-window attention: (window, every-Nth-layer-is-global
451    /// pattern) — Gemma-3.
452    pub swa: Option<(usize, usize)>,
453    /// Sliding-window tail trimming, `(slack, align)` for
454    /// `LayerKvCache::trim_window`: every layer with a window keeps only
455    /// the rows its window can still read (Spark-X2.5: 576..639 rows per
456    /// 512-window layer instead of the whole context — 27 of the 4B's 36
457    /// layers). Set at load for Spark-X2.5 only (`CMF_SWA_TRIM=0` turns
458    /// it off); None keeps every row, as every other model always has.
459    pub swa_trim: Option<(usize, usize)>,
460    /// Prompt end of the current turn: the tail trim keeps every row a
461    /// rollback to it still reads (`LayerKvCache::trim_window_anchored`),
462    /// so the next chat turn can drop this turn's answer.
463    swa_trim_anchor: usize,
464    /// Fewest prompt rows the Metal chunk prefill graph takes; a shorter
465    /// chunk walks the layers on the host. 32 since the graph landed
466    /// (measured on Qwen2.5-0.5B); Spark-X2.5/X2.6 take 8 (`loader`).
467    pub chunk_min_rows: usize,
468    /// Explicit local/global schedule for architectures that cannot be
469    /// represented by Gemma's every-Nth-global convention.
470    pub sliding_layers: Option<Vec<bool>>,
471    /// Per-layer flag: this layer runs WITHOUT RoPE. Recursive Flint applies
472    /// position encoding only on its sliding-window layers; its 14
473    /// full-attention layers have none. The rotary dim of such a layer is 0,
474    /// which is what the attention kernel reads.
475    pub no_rope_layers: Option<Vec<bool>>,
476    /// Natively bounded anchor record (`arch.anchor_core`): the file's
477    /// operator, installed once at load. `Some` = the model is
478    /// bounded-native — `--o1`/`CMF_O1*` are refused, prefix reuse and
479    /// the penalty window are bounded, and no anchor layer stores
480    /// anything per position.
481    pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
482    /// The `[W][rd/2]` relative-rotation table every bounded layer shares
483    /// (built from `inv_freq` once the RoPE setup is final).
484    bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
485    /// Bounded prefix-reuse key (length + rolling hash + tail) of a
486    /// bounded-native model; `kv_history` stays empty there so nothing
487    /// in the pipeline grows with the dialogue.
488    pub kv_prefix: KvPrefix,
489    /// Prompt positions the last `generate*` call actually forwarded
490    /// (prefix reuse subtracts what the cache already held).
491    pub last_prefill_tokens: usize,
492    /// RoPE table of the sliding (local) layers, when they use their
493    /// own base frequency (Gemma-3: 10k local vs 1M global).
494    pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
495    pub rotary_dim_local: Option<usize>,
496    pub rope_scale: f32,
497    pub rope_scale_local: f32,
498    /// Gemma-4: global layers run their own geometry — (head_dim,
499    /// num_kv_heads); sliding layers keep the base fields.
500    pub global_attn: Option<(usize, usize)>,
501    /// Gemma-4: the global layers' proportional RoPE table (len
502    /// global_head_dim/2, zero-padded tail = identity rotation).
503    pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
504    /// Scale-less RMS normalization of V heads before caching (Gemma-4).
505    pub attn_v_norm: bool,
506    /// HunYuan dense: per-head q/k norm runs after RoPE (see the arch flag).
507    pub qk_norm_after_rope: bool,
508    /// The per-head `self_attn.g_proj` output gate uses sigmoid
509    /// (Spark-X2.5) instead of softplus (Laguna).
510    pub proj_gate_sigmoid: bool,
511    /// Final-logit soft-capping C: logits = C·tanh(logits/C) (Gemma-4).
512    pub final_softcap: Option<f32>,
513    /// Cortiq Embryo hierarchical head: cluster matrix [C, hidden]. The
514    /// flat logits h·Eᵀ are turned into the two-level log-probabilities
515    /// log softmax_c(h·Cᵀ)[c(v)] + log softmax_{s∈c(v)}(h·E_c(v)ᵀ)[v].
516    pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
517    /// Gemma-2 attention-logit soft-capping (0.0 = off).
518    pub attn_softcap: f32,
519    /// Compute per-token confidence (a full-vocab softmax each
520    /// token). On by default; `bench --core` turns it off to match
521    /// llama-bench's core timing.
522    confidence_on: bool,
523    /// Test-only one-shot forward failure, scoped to this pipeline so
524    /// parallel scoring tests cannot consume one another's injection.
525    #[cfg(test)]
526    nll_test_fail_at: Option<usize>,
527    /// Test-only route override; avoids mutating the process-wide
528    /// `CMF_PREFILL` environment variable while forcing the serial path.
529    #[cfg(test)]
530    nll_test_force_serial: bool,
531}
532
533#[cfg(target_os = "macos")]
534impl Drop for Pipeline {
535    fn drop(&mut self) {
536        // the async replay writes into `kv_cache` Vecs about to be freed
537        let _ = crate::gpu_metal::wait_replay();
538        crate::gpu::kv_mirror_drop(self.graph_kv_id);
539    }
540}
541
542#[cfg(not(target_os = "macos"))]
543impl Drop for Pipeline {
544    fn drop(&mut self) {
545        // The wgpu resident Embryo graph owns recurrent/KV buffers keyed by
546        // this pipeline's sequence id.  Release that sequence image when a
547        // pooled pipeline is dropped; model weights stay cached for reuse.
548        crate::gpu::graph_kv_reset(self.graph_kv_id);
549    }
550}
551
552/// Model weights. Matrices are `QTensor` (owned f32 for small models
553/// and tests — bit-identical to the historical paths — or quantized
554/// bytes zero-copy from the CMF mmap for big models). 1-D norms are
555/// always small and stay f32.
556pub struct PipelineWeights {
557    /// Embedding table: [vocab_size, hidden_size]
558    pub embed_tokens: QTensor,
559    /// Per-layer weights
560    pub layers: Vec<LayerWeights>,
561    /// LM head: [vocab_size, hidden_size]
562    pub lm_head: QTensor,
563    /// Final norm: [hidden_size]
564    pub final_norm: Vec<f32>,
565}
566
567/// One transformer layer: shared norms + MLP, attention by kind.
568pub struct LayerWeights {
569    pub input_norm: Vec<f32>,
570    /// The pre-FFN norm (`post_attention_layernorm` classically;
571    /// `pre_feedforward_layernorm` on Gemma-2/3 sandwich layers).
572    pub post_norm: Vec<f32>,
573    /// Gemma-2/3 sandwich: norm applied to the ATTENTION OUTPUT before
574    /// its residual add (`post_attention_layernorm` there).
575    pub attn_out_norm: Option<Vec<f32>>,
576    /// Gemma-4: the whole layer output is multiplied by this scalar.
577    pub layer_scale: Option<f32>,
578    /// Gemma-2/3 sandwich: norm applied to the FFN OUTPUT before its
579    /// residual add (`post_feedforward_layernorm`).
580    pub ffn_out_norm: Option<Vec<f32>>,
581    pub ffn: FfnKind,
582    pub attn: AttnKind,
583}
584
585/// FFN gate activation: SiLU (SwiGLU family), tanh-GELU (Gemma's GeGLU),
586/// or a source-specific gated contract. A property of the model, carried on
587/// every FFN triple.
588#[derive(Clone, Copy, PartialEq, Debug, Default)]
589pub enum Act {
590    #[default]
591    Silu,
592    GeluTanh,
593    /// Exact erf GELU (HF `hidden_act = "gelu"`: Spark-X2.5).
594    Gelu,
595    /// Kimi-K3 SituAndMul: BOTH halves transform —
596    /// a = β·tanh(g/β)·σ(g), up' = linβ·tanh(u/linβ) (linβ>0), out = a·up'.
597    Situ {
598        beta: f32,
599        linear_beta: f32,
600    },
601    /// MiniMax M3's SwiGLU-OAI: upper-clamped gate, symmetrically clamped
602    /// up branch, `sigmoid(alpha * gate)`, and a `+1` up-branch offset.
603    /// It deliberately has no generic GPU graph arm until a matching kernel
604    /// exists; CPU uses the exact scalar reference in `inference`.
605    SwiGluOai {
606        alpha: f32,
607        limit: f32,
608    },
609}
610
611impl Act {
612    pub fn from_arch(name: &str) -> Self {
613        if name == "gelu_tanh" {
614            Self::GeluTanh
615        } else if name == "gelu" {
616            Self::Gelu
617        } else {
618            Self::Silu
619        }
620    }
621
622    /// Arch-driven constructor (activation name + situ betas).
623    pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
624        match arch.hidden_act.as_str() {
625            "situ" => Self::Situ {
626                beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
627                linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
628            },
629            // `swigluoai` alone is deliberately not a generic activation
630            // alias.  Its alpha/limit are part of MiniMax M3's checked
631            // source contract, and accepting that string on an unrelated
632            // architecture would silently impose M3 math on its FFN.
633            "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
634                alpha: 1.702,
635                limit: 7.0,
636            },
637            other => Self::from_arch(other),
638        }
639    }
640
641    /// Construct the MiniMax M3 SwiGLU-OAI activation from a checked model
642    /// contract. This is intentionally explicit rather than recognizing an
643    /// arbitrary `hidden_act` string: callers must validate `alpha` and
644    /// `limit` against their source architecture before enabling it.
645    #[inline]
646    pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
647        Self::SwiGluOai { alpha, limit }
648    }
649
650    #[inline]
651    pub fn apply(self, x: f32) -> f32 {
652        match self {
653            Self::Silu => inference::silu(x),
654            Self::GeluTanh => inference::gelu_tanh(x),
655            Self::Gelu => inference::gelu_erf(x),
656            Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
657            Self::SwiGluOai { alpha, limit } => {
658                let gate = x.min(limit);
659                gate * (1.0 / (1.0 + (-alpha * gate).exp()))
660            }
661        }
662    }
663
664    /// Gated combine — the FFN contract. Situ transforms the UP half
665    /// too, so callers must use this instead of apply(g)·u.
666    #[inline]
667    pub fn combine(self, g: f32, u: f32) -> f32 {
668        match self {
669            Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
670                self.apply(g) * (linear_beta * (u / linear_beta).tanh())
671            }
672            Self::SwiGluOai { alpha, limit } => {
673                inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
674            }
675            _ => self.apply(g) * u,
676        }
677    }
678
679    /// The wgpu graphs' arm for this activation; None = no device kernel
680    /// computes it, and the graph builder must refuse the layer (the dense
681    /// graph FFN once computed SiLU for whatever the model asked for).
682    pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
683        match self {
684            Self::Silu => Some(crate::gpu::GraphAct::Silu),
685            Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
686            Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
687            Self::Situ { .. } | Self::SwiGluOai { .. } => None,
688        }
689    }
690
691    /// The WGPU graph arm.  Keep this deliberately separate from
692    /// `graph_act`: OAI needs its dedicated code rather than a generic SiLU
693    /// fallback.  Only the frozen MiniMax-M3 source constants get a device
694    /// code; a future OAI-like architecture with different constants must add
695    /// and test its own kernel contract.
696    pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
697        self.moe_job_graph_contract().map(|(act, _)| act)
698    }
699
700    /// The exact device contract for a gate/up/down MoE job.  OAI is not a
701    /// generic "clamped SiLU": code 3 commits a backend to MiniMax M3's
702    /// alpha, asymmetric up transform, and frozen clamp.  Keep the clamp
703    /// alongside the marker so a backend cannot accidentally run code 3 with
704    /// a nearby model's limit.
705    pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
706        match self {
707            Self::SwiGluOai { alpha, limit }
708                if alpha.to_bits() == 1.702f32.to_bits()
709                    && limit.to_bits() == 7.0f32.to_bits() =>
710            {
711                Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
712            }
713            _ => self.graph_act().map(|act| (act, 0.0)),
714        }
715    }
716}
717
718/// Dense gated triple — the FFN of a dense layer or of one expert.
719pub struct DenseFfn {
720    pub gate_proj: QTensor,
721    pub up_proj: QTensor,
722    pub down_proj: QTensor,
723    /// Gate activation (SiLU default; Gemma: tanh-GELU).
724    pub act: Act,
725    /// `down_proj` stored transposed (`[inter, hidden]`), when the file
726    /// carries it. Only the per-token sparse path reads it: a neuron's
727    /// down weights are a contiguous ROW there, so the token's chosen
728    /// neurons are the only bytes touched. `None` = the ordinary layout,
729    /// and the sparse path stays off.
730    pub down_t: Option<QTensor>,
731    /// Task tubes (spec: defragged task-conditional width). The three
732    /// matrices above are the CORE — the neurons every task computes;
733    /// each tube is an independently quantized slice of the SAME layer
734    /// holding the neurons only some tasks need. A tube is a normal
735    /// tensor triple, so every kernel runs it unchanged, and the bytes
736    /// of an inactive tube are never read. Empty = ordinary dense FFN.
737    pub segs: Vec<FfnSeg>,
738}
739
740/// One task tube: a contiguous slice of a layer's FFN neurons, stored
741/// as its own `[w, hidden]` / `[hidden, w]` triple. `start` is the
742/// neuron's index in the layer's FULL space (core first, then tubes in
743/// order) — the bit a task mask sets to switch this tube on.
744pub struct FfnSeg {
745    pub gate: QTensor,
746    pub up: QTensor,
747    pub down: QTensor,
748    pub start: usize,
749    pub width: usize,
750}
751
752/// FFN operator of a layer, decided by tensor presence at load time
753/// (router `mlp.gate.weight` in the directory = MoE layer).
754pub enum FfnKind {
755    Dense(DenseFfn),
756    /// Mixture-of-Experts (Qwen2-MoE / Qwen3-MoE): softmax over ALL
757    /// expert logits → top-k, optional renorm; experts stay quantized
758    /// in mmap — only the selected ones are touched per token.
759    Moe(MoeFfn),
760    /// Gemma-4 MoE: a dense MLP branch AND a routed-expert branch in
761    /// the SAME layer, each with its own norm sandwich. The dense
762    /// branch reads the pre-FFN-normed input; the expert branch (and
763    /// the router) read the RAW residual through `pre_norm_2`:
764    ///   d = post_norm_1(dense(x̂));  m = post_norm_2(Σwₑ·FFNₑ(pre_norm_2(h)))
765    ///   ffn_out = d + m   (the caller's ffn_out_norm + residual follow)
766    DenseMoe(Box<DenseMoeFfn>),
767}
768
769/// Gemma-4 dual-branch FFN (see `FfnKind::DenseMoe`).
770pub struct DenseMoeFfn {
771    pub dense: DenseFfn,
772    pub moe: MoeFfn,
773    /// post_feedforward_layernorm_1 — dense-branch output norm.
774    pub post_norm_1: Vec<f32>,
775    /// pre_feedforward_layernorm_2 — expert-branch input norm (applied
776    /// to the RAW residual, not the pre-FFN-normed activation).
777    pub pre_norm_2: Vec<f32>,
778    /// post_feedforward_layernorm_2 — expert-branch output norm.
779    pub post_norm_2: Vec<f32>,
780}
781
782pub struct MoeFfn {
783    /// Router `mlp.gate.weight` [num_experts, hidden].
784    pub router: QTensor,
785    pub experts: Vec<DenseFfn>,
786    pub top_k: usize,
787    pub norm_topk_prob: bool,
788    /// Router scores per-expert with a sigmoid (LFM2-MoE / DeepSeek-V3
789    /// `noaux_tc`) instead of a softmax over all experts (Qwen).
790    pub router_sigmoid: bool,
791    /// Addend in the selected-score denominator of a sigmoid router.
792    /// Most supported checkpoints use the historical `1e-6` floor; Flint's
793    /// reference routing uses `1e-20`; MiniMax M3.1 preserves the same FP32
794    /// `sum + 1e-20` law. This is per-MoE rather than a global family switch
795    /// because the same sigmoid routing law has several contracts.
796    pub sigmoid_denom_eps: f32,
797    /// Per-expert selection bias `mlp.expert_bias` [num_experts]
798    /// (LFM2-MoE): added to the sigmoid scores for the top-k CHOICE only;
799    /// the gathered weights use the unbiased scores. None = no bias.
800    pub expert_bias: Option<Vec<f32>>,
801    /// Top-k weights are multiplied by this after the optional renorm
802    /// (LFM2-MoE `routed_scaling_factor`; 1.0 = off).
803    pub routed_scaling: f32,
804    /// Adaptive routing (CMF_MOE_TAU, opt-in): keep the smallest
805    /// prefix of the top-k whose renormalized mass reaches τ —
806    /// confident tokens touch 1–2 experts, flat ones keep all k.
807    /// MoE decode is memory-bound, so skipped experts are skipped
808    /// weight traffic. None = classic fixed top-k (bit-identical).
809    pub route_tau: Option<f32>,
810    /// Always-on shared expert. Qwen2-MoE carries an additional sigmoid
811    /// gate; Laguna adds the shared expert unconditionally (`None`).
812    pub shared: Option<(DenseFfn, Option<QTensor>)>,
813    /// Expert-selection counters (truncated Fisher B-field of claim 12:
814    /// routing frequency during calibration). Filled by every forward,
815    /// read by the CLI via CMF_MOE_STATS. RefCell: decode is single-threaded.
816    pub stats: std::cell::RefCell<Vec<u64>>,
817    /// Per-CHANNEL sum of squares of this FFN's input, accumulated over a
818    /// calibration run (`CMF_RMS_TRACE`). These are the RMS activation
819    /// traces AWNP needs: raw weight magnitude says every channel matters
820    /// equally, and the question AWNP asks is whether the ACTIVATIONS
821    /// disagree. Off unless the env var is set — an f64 add per channel
822    /// per token is cheap, but not free.
823    pub act_sq: std::cell::RefCell<Vec<f64>>,
824    /// Raw FFN-input rows captured for the layers named by `CMF_ACT_DUMP`
825    /// (`"9,19"`). AWNP is nullspace PROJECTION: after dropping channels the
826    /// survivors are refitted to absorb what was removed, and how much they
827    /// can absorb depends on the activation COVARIANCE, not on per-channel
828    /// RMS. Per-channel numbers can only bound the cost from above.
829    pub act_rows: std::cell::RefCell<Vec<f32>>,
830    /// Task mask over routed experts (DTG-MA over MoE, claim-12 B-field
831    /// applied): `false` experts are excluded from selection, the
832    /// softmax renormalizes over the allowed set. Built by the loader
833    /// from CMF_MOE_MASK=<stats.json> + CMF_MOE_MASK_COVER. None = all.
834    pub mask: Option<Vec<bool>>,
835    /// Gemma-4: per-expert weight scale applied AFTER the top-k renorm
836    /// (`router.per_expert_scale`). None = 1.0 everywhere.
837    pub per_expert_scale: Option<Vec<f32>>,
838    /// Gemma-4: the router reads a SCALE-LESS rms-norm of its input
839    /// (the constant gain router.scale·√hidden is folded into the
840    /// router weights at convert time).
841    pub router_input_norm: bool,
842    /// Cortiq Embryo: resonance routing (P1) — the "logits" are
843    /// bias_e − ‖(x−μ_e) − U_eᵀU_e(x−μ_e)‖², argmax = the expert whose
844    /// descriptor reconstructs the input best. `router` is a placeholder.
845    pub resonance: Option<Resonance>,
846    /// Growth records (`kind = "expert_append"`, spec §9.5.1) mounted
847    /// behind the trunk experts, one entry per grown expert IN THE ORDER
848    /// they sit in `experts` (the tail `experts[experts.len() - grown.len()..]`).
849    /// The trunk keeps `experts.len() - grown.len()` experts. Empty on a
850    /// gated MoE, on a file without records and under `CMF_GROWTH=off`.
851    pub grown: Vec<GrownExpert>,
852}
853
854/// One grown expert of an `expert_append` record as the loader mounted it.
855#[derive(Debug, Clone, PartialEq, Eq)]
856pub struct GrownExpert {
857    /// The record's skill id.
858    pub record: String,
859    /// Position of the record in `header.skills`.
860    pub record_index: usize,
861    pub layer: usize,
862    /// The index the tensor name declares (`experts.{e}` — the chain rule
863    /// of the format); the executed position may be smaller when an
864    /// earlier record of the layer is not mounted.
865    pub expert: usize,
866}
867
868/// Per-expert resonance descriptors of one MoE layer (`mlp.desc.*`).
869pub struct Resonance {
870    /// [E, hidden]
871    pub mu: Vec<f32>,
872    /// [E, k, hidden] orthonormal directions (k may be 0)
873    pub u: Vec<f32>,
874    pub k: usize,
875    /// [E] selection bias (loss-free balancing, trained online)
876    pub bias: Vec<f32>,
877    /// [E] reconstruction-error shell: an expert whose error
878    /// `‖(x−μ)⊥U‖² = d² − proj` exceeds its shell scores `−∞` (it never
879    /// wins). `+inf` = no shell — every trunk expert; a grown expert
880    /// (`expert_append`) carries the finite `desc.shell` its record stores.
881    /// Empty = no shell anywhere (legacy constructors).
882    pub shell: Vec<f32>,
883}
884
885/// `CMF_GROWTH_SHELL` state: 0 = not yet read from the environment, 1 = on,
886/// 2 = off. Process-wide, like the environment it mirrors.
887static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
888
889/// Is the growth shell applied (`Resonance::scores` −∞ rule, the resident
890/// graph's packed shell)? `CMF_GROWTH_SHELL=off` disables it for
891/// measurement; [`set_growth_shell`] overrides the environment in-process
892/// (`growth-eval --shell`). Default: on.
893pub fn growth_shell_enabled() -> bool {
894    use std::sync::atomic::Ordering;
895    match GROWTH_SHELL.load(Ordering::Relaxed) {
896        1 => true,
897        2 => false,
898        _ => {
899            let off = std::env::var("CMF_GROWTH_SHELL")
900                .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
901                .unwrap_or(false);
902            GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
903            !off
904        }
905    }
906}
907
908/// Switch the growth shell on/off for this process (`None` = re-read
909/// `CMF_GROWTH_SHELL` on the next query). A pipeline packed into the
910/// resident graph BEFORE the switch keeps the shell it was packed with —
911/// build a new pipeline after switching.
912pub fn set_growth_shell(on: Option<bool>) {
913    GROWTH_SHELL.store(
914        match on {
915            Some(true) => 1,
916            Some(false) => 2,
917            None => 0,
918        },
919        std::sync::atomic::Ordering::Relaxed,
920    );
921}
922
923impl Resonance {
924    /// Does any expert carry a finite shell (a mounted growth record)?
925    pub fn has_shell(&self) -> bool {
926        self.shell.iter().any(|s| s.is_finite())
927    }
928
929    /// The shell the runtime applies right now: the stored one, or all
930    /// `+inf` when the shell is switched off (`CMF_GROWTH_SHELL=off`).
931    pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
932        let mut out = vec![f32::INFINITY; ne];
933        if growth_shell_enabled() {
934            for (o, s) in out.iter_mut().zip(&self.shell) {
935                *o = *s;
936            }
937        }
938        out
939    }
940
941    /// Routing scores for one input row (higher = better). A grown
942    /// expert whose reconstruction error lies outside its shell gets
943    /// `−∞` (unless the shell is switched off); trunk rows are the exact
944    /// bit pattern they were before growth.
945    pub fn scores(&self, x: &[f32], out: &mut [f32]) {
946        let h = x.len();
947        let ne = out.len();
948        let shell_on = growth_shell_enabled() && !self.shell.is_empty();
949        for e in 0..ne {
950            let mu = &self.mu[e * h..(e + 1) * h];
951            let mut d2 = 0.0f32;
952            for j in 0..h {
953                let d = x[j] - mu[j];
954                d2 += d * d;
955            }
956            let mut proj = 0.0f32;
957            for i in 0..self.k {
958                let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
959                let mut p = 0.0f32;
960                for j in 0..h {
961                    p += (x[j] - mu[j]) * u[j];
962                }
963                proj += p * p;
964            }
965            let err = d2 - proj;
966            out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
967            if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
968                out[e] = f32::NEG_INFINITY;
969            }
970        }
971    }
972}
973
974/// Attention operator of a layer. Extension point: new operators are
975/// new variants here + a forward in their own module.
976pub enum AttnKind {
977    /// GQA softmax attention (+ optional Qwen3.5 qk-norm / output gate).
978    Full {
979        wq: QTensor,
980        wk: QTensor,
981        wv: QTensor,
982        wo: QTensor,
983        q_norm: Option<Vec<f32>>,
984        k_norm: Option<Vec<f32>>,
985        output_gate: bool,
986        /// Laguna: a separate softplus projection applied to the attention
987        /// output before O. The bool means one scalar per head (broadcast
988        /// across head_dim); false means one scalar per element.
989        softplus_gate: Option<(QTensor, bool)>,
990        /// Qwen2-family projection biases (q, k, v).
991        bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
992    },
993    /// Canonical linear core (VMF phase attention).
994    Linear(VmfPhaseWeights),
995    /// Faithful vendor linear operator (Qwen3.5 GatedDeltaNet).
996    LinearGdn(GdnWeights),
997    /// LFM2 gated short-convolution mixer (no KV cache; conv ring state
998    /// lives in the layer's `linear_state`).
999    ShortConv(ShortConvWeights),
1000    /// DeepSeek-V2 Multi-head Latent Attention. v1 executes it as
1001    /// expand-to-MHA: the latent is projected per token, K/V expand to
1002    /// every head and live in the ordinary cache (K head layout
1003    /// [rope | nope] so the standard partial rotary covers the shared
1004    /// rope key; V rows are zero-padded to the K head_dim and the pad
1005    /// is sliced off before O). Latent-resident cache is a later
1006    /// optimization, not a semantic change.
1007    Mla(Box<MlaWeights>),
1008    /// Kimi Delta Attention (Kimi Linear / Kimi-K3): per-channel decayed
1009    /// delta rule, separate q/k/v short convs, sigmoid-gated output norm.
1010    /// State lives in the layer's `linear_state` (no KV cache).
1011    Kda(Box<crate::linear_core::KdaWeights>),
1012    /// Natively bounded softmax anchor `swa_sink_v1` (Embryo-O1): ring of
1013    /// the last W raw keys with relative RoPE + trained NoPE sinks, one
1014    /// softmax. State is the fixed-size ring in `LayerKvCache::bounded`;
1015    /// nothing is stored per position (see `crate::bounded`).
1016    Bounded(Box<crate::bounded::BoundedWeights>),
1017}
1018
1019/// DeepSeek-V2 MLA projections (see `AttnKind::Mla`).
1020pub struct MlaWeights {
1021    /// `[nh·(rope+nope), hidden]` (or `[…, q_lora]` when compressed) —
1022    /// the converter permutes each head rope-first so rotary_dim =
1023    /// qk_rope works unchanged.
1024    pub q_proj: QTensor,
1025    /// Compressed q (K3/V3 class): x → q_a `[q_lora, hidden]` →
1026    /// rms(q_a_norm) → q_proj (= q_b). None = direct q (V2-Lite).
1027    pub q_a: Option<QTensor>,
1028    pub q_a_norm: Option<Vec<f32>>,
1029    /// `kv_a_proj_with_mqa` `[lora + rope, hidden]` (latent first).
1030    pub kv_a: QTensor,
1031    /// RMS-norm weights over the latent (`kv_a_layernorm`, [lora]).
1032    pub kv_a_norm: Vec<f32>,
1033    /// `[nh·(nope+v), lora]` — per head [k_nope | v].
1034    pub kv_b: QTensor,
1035    /// `[hidden, nh·v]`.
1036    pub o_proj: QTensor,
1037    pub nh: usize,
1038    pub qk_rope: usize,
1039    pub qk_nope: usize,
1040    pub v_dim: usize,
1041    pub lora: usize,
1042    /// Softmax scale (1/√(rope+nope), YaRN-mscale-corrected at load).
1043    pub scale: f32,
1044    /// Kimi Linear NoPE: skip the rotary entirely (layout unchanged).
1045    pub nope: bool,
1046}
1047
1048/// Multi-token-prediction head (DeepSeek/Qwen style, spec §2.1):
1049/// `x = eh_proj·[enorm(embed(next)); hnorm(hidden)]` → one transformer
1050/// block over its own KV → shared lm_head. Drafts the token after next;
1051/// the main model verifies, so output is exact — MTP only buys speed.
1052pub struct MtpModule {
1053    pub enorm: Vec<f32>,
1054    pub hnorm: Vec<f32>,
1055    /// [hidden, 2·hidden]
1056    pub eh_proj: QTensor,
1057    pub layer: LayerWeights,
1058    pub final_norm: Vec<f32>,
1059    pub kv: crate::kv_cache::LayerKvCache,
1060}
1061
1062/// Teacher-forced activation statistics for one MTP draft FFN.
1063///
1064/// `mass[i]` is `Σ_rows |activation_i|`; `squared_mass[i]` is
1065/// `Σ_rows activation_i²`, where `activation = act(gate) · up` immediately
1066/// before the draft layer's down projection. `observed_positions` is the
1067/// number of teacher-forced rows that contributed to both arrays.
1068///
1069/// Rows of FFN input the canonical draft probe measures in one batch.
1070const MTP_PROBE_FFN_ROWS: usize = 128;
1071
1072/// [`Pipeline::probe_mtp_ffn`] returns one of these for each actual draft
1073/// layer.  It intentionally has a separate type/API from
1074/// [`Pipeline::probe_ffn_mass`] so a compactor cannot accidentally substitute
1075/// trunk statistics for an MTP sidecar whose FFN has a different width.
1076#[derive(Clone, Debug, PartialEq)]
1077pub struct MtpFfnProbe {
1078    pub mass: Vec<f64>,
1079    pub squared_mass: Vec<f64>,
1080    pub observed_positions: usize,
1081}
1082
1083/// A Metal verify graph after its sync: what the commit needs — the
1084/// graph (per-layer replay scratch), the GDN layers in encode order (their
1085/// CPU states receive the replay), and the attention layers with the CPU
1086/// row count they were encoded against (the accepted rows are pulled from
1087/// the mirror from there).
1088/// One item of the Metal rows-graph plan.
1089#[cfg(target_os = "macos")]
1090enum MetalRowsItem<'a> {
1091    Gdn {
1092        run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1093        first: usize,
1094    },
1095    Attn {
1096        l: crate::gpu_metal::AttnGpuLayer<'a>,
1097        li: usize,
1098        q_norm: Option<&'a [f32]>,
1099        k_norm: Option<&'a [f32]>,
1100        output_gate: bool,
1101    },
1102}
1103
1104#[cfg(target_os = "macos")]
1105struct MetalVerifyPending {
1106    graph: crate::gpu_metal::VerifyGraph,
1107    gdn_layers: Vec<usize>,
1108    attn_layers: Vec<(usize, usize)>,
1109}
1110
1111/// A round's batched MTP warm-up, submitted but not yet waited
1112/// (`mtp_warm_batch_submit` → `mtp_warm_batch_finish`): the trunk commit's
1113/// GDN replay is queued between the two.
1114#[cfg(target_os = "macos")]
1115struct MetalWarmPending {
1116    graph: crate::gpu_metal::VerifyGraph,
1117    cpu_stored: usize,
1118    b: usize,
1119}
1120
1121#[cfg(target_os = "macos")]
1122enum MetalRowsRun {
1123    /// Capability/preflight refusal before a command buffer was committed.
1124    Declined,
1125    /// A graph was admitted and then failed; callers must clear the sequence
1126    /// rather than replaying it through CPU/serial state.
1127    Failed,
1128    Completed(MetalVerifyPending),
1129}
1130
1131#[cfg(target_os = "macos")]
1132enum MetalPrefillOutcome {
1133    Declined,
1134    Failed,
1135    Completed(Vec<f32>),
1136}
1137
1138#[cfg(target_os = "macos")]
1139enum MetalBatchNllOutcome {
1140    Declined,
1141    Failed(String),
1142    Completed(f64, usize),
1143}
1144
1145/// The speculation trial's phases (see the decode loop): up to five timed
1146/// speculative rounds, eight timed plain tokens, then the faster arm until a
1147/// re-check. Two consecutive rounds with no accepted draft skip directly to
1148/// the plain measurement: another speculative round cannot pay when it emits
1149/// exactly the one token the plain loop would have produced.
1150#[derive(Clone, Copy)]
1151enum SpecTrial {
1152    Spec {
1153        t0: std::time::Instant,
1154        gen0: usize,
1155        rounds: usize,
1156    },
1157    Plain {
1158        t0: std::time::Instant,
1159        gen0: usize,
1160    },
1161    Decided {
1162        spec: bool,
1163        recheck_at: usize,
1164    },
1165}
1166
1167/// `CMF_GRAPH_SPEC_TIME`: 0 = off, 1 = one line per speculative round
1168/// plus the host stamps of any OUTLIER round (wall > 1.4× the running
1169/// median), 2 = the host stamps of every round.
1170pub(crate) fn spec_time_level() -> u8 {
1171    static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1172    *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1173        Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1174        Err(_) => 0,
1175    })
1176}
1177
1178/// `GraphMoeBank` reuse across tokens (`CMF_GRAPH_MOE_BANK=0` rebuilds
1179/// every layer's bank on every token, the route before the cache).
1180fn graph_moe_bank_on() -> bool {
1181    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1182    *ON.get_or_init(|| std::env::var("CMF_GRAPH_MOE_BANK").as_deref() != Ok("0"))
1183}
1184
1185/// Decide whether the batched speculative verifier is admitted by the
1186/// user/backend policy, before capability checks.  Native Metal's row-batch
1187/// verifier is deliberately fail-closed by default: it has produced a
1188/// different *emitted* greedy token than the serial path on a real model.
1189/// It remains available only through the documented explicit experiment
1190/// spelling, `CMF_GRAPH_SPEC=1`; other backends retain their historical
1191/// nonzero override behaviour.
1192fn graph_spec_wanted_by_policy(
1193    spec_env: Option<&str>,
1194    spec_default_ok: bool,
1195    penalized: bool,
1196    metal_wgpu: bool,
1197    native_metal: bool,
1198) -> bool {
1199    match spec_env {
1200        Some("0") => false,
1201        // Keep the native path genuinely explicit.  In particular, a
1202        // typo/nonzero value must not silently re-enable an unqualified
1203        // verifier just because historical non-Metal parsing treated it as
1204        // force-on.
1205        Some("1") => true,
1206        Some(_) => !native_metal,
1207        // The discrete/wgpu default stays as it was; only native Metal has
1208        // a quality gate here.
1209        None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1210    }
1211}
1212
1213/// The round's host stamps: `spec_stamp(name)` records the time since
1214/// the previous stamp (the section that just ended) — from anywhere on
1215/// the round's call chain (the Metal verify, the draft step, the commit),
1216/// no plumbing. Off (a single atomic load) unless `CMF_GRAPH_SPEC_TIME`
1217/// is set; one decode thread at a time is assumed (diagnostics).
1218struct SpecStampLog {
1219    t_last: std::time::Instant,
1220    items: Vec<(&'static str, f32)>,
1221}
1222
1223static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1224
1225pub(crate) fn spec_stamp(name: &'static str) {
1226    if spec_time_level() == 0 {
1227        return;
1228    }
1229    if let Ok(mut g) = SPEC_STAMPS.lock() {
1230        if let Some(log) = g.as_mut() {
1231            let now = std::time::Instant::now();
1232            log.items
1233                .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1234            log.t_last = now;
1235        }
1236    }
1237}
1238
1239fn spec_stamps_begin() {
1240    if spec_time_level() == 0 {
1241        return;
1242    }
1243    if let Ok(mut g) = SPEC_STAMPS.lock() {
1244        *g = Some(SpecStampLog {
1245            t_last: std::time::Instant::now(),
1246            items: Vec::with_capacity(64),
1247        });
1248    }
1249}
1250
1251fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1252    SPEC_STAMPS
1253        .lock()
1254        .ok()
1255        .and_then(|mut g| g.take())
1256        .map(|l| l.items)
1257        .unwrap_or_default()
1258}
1259
1260/// One line: every stamp name in first-seen order with its total over the
1261/// round and, when it fired more than once (the draft steps), the count.
1262fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1263    let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1264    for &(n, ms) in items {
1265        match agg.iter_mut().find(|e| e.0 == n) {
1266            Some(e) => {
1267                e.1 += ms;
1268                e.2 += 1;
1269            }
1270            None => agg.push((n, ms, 1)),
1271        }
1272    }
1273    let mut s = String::with_capacity(agg.len() * 16);
1274    for (n, ms, k) in agg {
1275        if k > 1 {
1276            s.push_str(&format!("{n} {ms:.1}/{k} "));
1277        } else {
1278            s.push_str(&format!("{n} {ms:.1} "));
1279        }
1280    }
1281    s
1282}
1283
1284/// The speculation monitor: exponential averages of a round's wall time
1285/// and of the tokens it produced, and the plain token's wall time — the
1286/// three numbers the keep/stop rule needs. A round pays when
1287/// `tokens_per_round · plain_ms > round_ms · 1.03`. The one-shot trial
1288/// (four rounds against eight tokens) mis-called prose: the first rounds
1289/// after a prompt are formulaic and accept well, the body does not (an
1290/// essay measured 39 against a plain 44.8 with the trial saying
1291/// "speculate"), so the rule now runs on EVERY round and stops after four
1292/// consecutive losing rounds; a stopped speculation is retried 128 tokens
1293/// later.
1294///
1295/// Native Metal (`metal: true`) does not pay the eight plain tokens up
1296/// front: on the 27B a plain token is ~150 ms, so the trial alone cost
1297/// ~1.2 s of every answer. There the plain phase is (a) skipped while the
1298/// rounds land at least `SPEC_PROXY_TOKENS` tokens each — a k=7 round on
1299/// Metal costs ~1.9 plain tokens (286 against 148 ms measured on the M4),
1300/// so 3.5 tokens/round cannot lose on any Metal round/plain ratio seen —
1301/// and (b) otherwise bounded to the fewest tokens that time it: two, or
1302/// as many as fit in `SPEC_PLAIN_MIN_MS` (a 150-ms token measures itself;
1303/// a 10-ms one needs the eight). The keep/stop rule itself is unchanged:
1304/// the moment a plain rate exists, it decides.
1305#[derive(Default, Clone, Copy)]
1306struct SpecMon {
1307    round_ms: f64,
1308    tokens: f64,
1309    plain_ms: f64,
1310    n: u32,
1311    fails: u32,
1312    /// Consecutive rounds that produced only their verifier-tip token (zero
1313    /// accepted MTP drafts). This is deliberately counted before the warm-up
1314    /// round is excluded from timing: two real zero-acceptance rounds are
1315    /// enough to stop charging an obviously losing speculative arm.
1316    zero_accept_rounds: u8,
1317    metal: bool,
1318}
1319
1320/// Tokens per round at or above which a Metal round pays without a plain
1321/// measurement (see `SpecMon`).
1322const SPEC_PROXY_TOKENS: f64 = 3.5;
1323/// The Metal plain phase: at least two tokens, and more until this much
1324/// wall time has been timed (up to the eight the other backends time).
1325const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1326
1327impl SpecMon {
1328    fn round(&mut self, dt_ms: f64, produced: usize) {
1329        if produced <= 1 {
1330            self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1331        } else {
1332            self.zero_accept_rounds = 0;
1333        }
1334        self.n += 1;
1335        if self.n == 1 {
1336            return; // round 1 pays the batch scratch and the draft mirror
1337        }
1338        let a = if self.n == 2 { 1.0 } else { 0.3 };
1339        self.round_ms += a * (dt_ms - self.round_ms);
1340        self.tokens += a * (produced as f64 - self.tokens);
1341    }
1342    fn pays(&self) -> bool {
1343        if self.plain_ms > 0.0 {
1344            self.tokens * self.plain_ms > self.round_ms * 1.03
1345        } else {
1346            self.metal && self.tokens >= SPEC_PROXY_TOKENS
1347        }
1348    }
1349    fn repeated_zero_acceptance(&self) -> bool {
1350        self.zero_accept_rounds >= 2
1351    }
1352    /// Has the plain phase timed enough tokens to decide?
1353    fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1354        let n = generated.saturating_sub(gen0);
1355        if n >= 8 {
1356            return true;
1357        }
1358        self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1359    }
1360}
1361
1362/// Ids of the consumed prefix the bounded reuse key remembers literally
1363/// (the rest is covered by the rolling hash).
1364pub const KV_PREFIX_TAIL: usize = 128;
1365
1366/// Bounded prefix-reuse key: how many ids the cache holds, a rolling
1367/// hash of ALL of them and the last [`KV_PREFIX_TAIL`] ids literally.
1368/// Answers "does this prompt strictly extend what is cached" exactly
1369/// (hash over the whole consumed prefix + literal tail) without keeping
1370/// the dialogue — the record is a fixed size whatever the session length.
1371#[derive(Debug, Clone, Default)]
1372pub struct KvPrefix {
1373    len: usize,
1374    hash: u64,
1375    tail: Vec<u32>,
1376    /// Owner of the state this key describes: the resident device graph
1377    /// (true) or the host. A turn continues the prefix only on its owner
1378    /// (R4: a host continuation of a device sequence reads an empty host
1379    /// state; a device continuation of a host sequence has no image).
1380    device: bool,
1381}
1382
1383impl KvPrefix {
1384    #[inline]
1385    fn fold(mut h: u64, ids: &[u32]) -> u64 {
1386        for &id in ids {
1387            h ^= id as u64;
1388            h = h.wrapping_mul(0x100000001b3);
1389            h ^= h >> 29;
1390        }
1391        h
1392    }
1393
1394    pub fn clear(&mut self) {
1395        self.len = 0;
1396        self.hash = 0xcbf29ce484222325;
1397        self.tail.clear();
1398        self.device = false;
1399    }
1400
1401    /// Was the prefix built on the resident device graph?
1402    pub fn on_device(&self) -> bool {
1403        self.device
1404    }
1405
1406    /// Tag the owner of the recorded prefix.
1407    pub fn set_on_device(&mut self, device: bool) {
1408        self.device = device;
1409    }
1410
1411    /// Ids the cache holds (the forwarded prefix).
1412    pub fn len(&self) -> usize {
1413        self.len
1414    }
1415
1416    pub fn is_empty(&self) -> bool {
1417        self.len == 0
1418    }
1419
1420    /// Literal tail currently kept (≤ `KV_PREFIX_TAIL`).
1421    pub fn tail_len(&self) -> usize {
1422        self.tail.len()
1423    }
1424
1425    /// Replace the key with `ids` (a fresh sequence).
1426    pub fn set(&mut self, ids: &[u32]) {
1427        self.clear();
1428        self.extend(ids);
1429    }
1430
1431    /// Append `more` to the consumed prefix (an extension-only turn).
1432    pub fn extend(&mut self, more: &[u32]) {
1433        if self.len == 0 && self.hash == 0 {
1434            self.hash = 0xcbf29ce484222325;
1435        }
1436        self.hash = Self::fold(self.hash, more);
1437        self.len += more.len();
1438        if more.len() >= KV_PREFIX_TAIL {
1439            self.tail.clear();
1440            self.tail
1441                .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1442        } else {
1443            let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1444            self.tail.drain(..drop);
1445            self.tail.extend_from_slice(more);
1446        }
1447    }
1448
1449    /// Cached positions when `ids` strictly extends the consumed prefix,
1450    /// 0 otherwise. The tail is compared literally first (cheap), then
1451    /// the hash over the whole prefix must agree.
1452    pub fn extension(&self, ids: &[u32]) -> usize {
1453        if self.len == 0 || ids.len() <= self.len {
1454            return 0;
1455        }
1456        let t = self.tail.len();
1457        if ids[self.len - t..self.len] != self.tail[..] {
1458            return 0;
1459        }
1460        if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1461            return 0;
1462        }
1463        self.len
1464    }
1465}
1466
1467/// Result of a generation call.
1468pub struct GenerateResult {
1469    pub text: String,
1470    pub token_ids: Vec<u32>,
1471    pub prompt_tokens: usize,
1472    pub tokens_generated: usize,
1473    pub finish_reason: String,
1474    /// Speculative-decode stats (0/0 when MTP is absent or inactive).
1475    pub mtp_drafted: usize,
1476    pub mtp_accepted: usize,
1477    /// Per-generated-token confidence = softmax probability of the token
1478    /// that was actually emitted (softmax probability on the chosen state). High =
1479    /// the model was sure; low = it was guessing. Same length as the
1480    /// generated slice of `token_ids`.
1481    pub token_confidence: Vec<f32>,
1482    /// Structured per-token telemetry (B4 channel). Empty unless
1483    /// `set_trace(true)`; otherwise same length as the generated slice.
1484    pub traces: Vec<TokenTrace>,
1485}
1486
1487/// One row of the structured telemetry trace (B4): the model's internal
1488/// routing state at the moment a token was emitted. Every field is a
1489/// quantity the runtime already computes — nothing is inferred or
1490/// estimated (anti-principle: only measured bytes).
1491#[derive(Clone, Debug)]
1492pub struct TokenTrace {
1493    /// 0-based index within the generated slice.
1494    pub t: usize,
1495    /// The emitted token id.
1496    pub token_id: u32,
1497    /// Softmax probability on the emitted token — how sure the model was.
1498    pub confidence: f32,
1499    /// Skill in force while this token was generated (None = backbone).
1500    pub active_skill: Option<String>,
1501    /// Recon error E = ‖r−BBᵀr‖²/‖φ‖² at the last routing eval — coherence
1502    /// with the active skill's subspace (low = coherent). None = no router
1503    /// or not yet evaluated.
1504    pub recon: Option<f32>,
1505    /// The router changed the active skill right after this token (a
1506    /// domain boundary crossed under the hysteresis barrier).
1507    pub switched: bool,
1508}
1509
1510/// Calibrated softmax probability of `id` under `logits` (the confidence on
1511/// the emitted token) — the confidence signal, cheap from logits already
1512/// computed for sampling. `temp` is the calibration temperature (B1):
1513/// softmax(logits / temp); 1.0 = raw.
1514#[cfg_attr(not(test), allow(dead_code))]
1515fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1516    let t = if temp > 1e-3 { temp } else { 1.0 };
1517    let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1518    let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1519    if sum > 0.0 {
1520        (((logits[id as usize] - max) / t).exp()) / sum
1521    } else {
1522        0.0
1523    }
1524}
1525
1526/// prefill-GEMM enabled? (CMF_PREFILL=seq — emergency fallback to the
1527/// sequential path.)
1528fn prefill_batched() -> bool {
1529    std::env::var("CMF_PREFILL")
1530        .map(|v| v != "seq")
1531        .unwrap_or(true)
1532}
1533
1534/// Decide the graph NLL route without conflating graph quality with the
1535/// optional native-Metal fused head. A hidden-state graph remains a valid
1536/// quality route on Vulkan/Wgpu; only native Metal requires graph logits.
1537#[inline]
1538/// ‖a − b‖ / ‖b‖ (infinite when the lengths differ).
1539fn rel_l2(a: &[f32], b: &[f32]) -> f64 {
1540    if a.len() != b.len() {
1541        return f64::INFINITY;
1542    }
1543    let (mut d, mut r) = (0f64, 0f64);
1544    for (&x, &y) in a.iter().zip(b) {
1545        let e = (x - y) as f64;
1546        d += e * e;
1547        r += y as f64 * y as f64;
1548    }
1549    (d / r.max(1e-30)).sqrt()
1550}
1551
1552fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1553    let graph_quality = unmasked && prefer_graph;
1554    let fused_head_quality = graph_quality && native_metal;
1555    (graph_quality, fused_head_quality)
1556}
1557
1558/// Input to the layer-major batched span walk: token ids (embeds itself,
1559/// full-stack and coordinator prefill) or ready boundary hiddens (the
1560/// network worker's side of a split).
1561#[derive(Clone, Copy)]
1562enum PrefillIn<'a> {
1563    Ids(&'a [u32]),
1564    Hidden(&'a [f32]),
1565}
1566
1567/// The batched prefill walks `weights.layers`. Architectures that load
1568/// their own stack (gemma-3n's AltUp replicas, DeepSeek-V4's hyper-
1569/// connections) leave that empty and must go position by position — asking
1570/// otherwise indexes an empty vector, which is a panic rather than a
1571/// fallback. Every call site goes through here so the next such
1572/// architecture is one line, not four.
1573impl Pipeline {
1574    pub(crate) fn can_prefill_batched(&self) -> bool {
1575        #[cfg(test)]
1576        let force_serial = self.nll_test_force_serial;
1577        #[cfg(not(test))]
1578        let force_serial = false;
1579        prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1580    }
1581
1582    /// The backend's automatic capacity split for a mapped transformer.
1583    /// Kept as a method so prefill and decode use the exact same boundary.
1584    fn automatic_gpu_prefix(&self) -> Option<usize> {
1585        let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1586        crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1587    }
1588
1589    /// Positions per batched pass of the layer-stack prefill for THIS
1590    /// model on THIS backend (see [`prefill_chunk_rule`]). Pub: the network
1591    /// split must chunk exactly like the local path to reproduce it.
1592    pub fn prefill_chunk(&self) -> usize {
1593        let env = env_prefill_chunk();
1594        if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1595            return prefill_chunk_rule(env, ChunkHost::here(), false);
1596        }
1597        prefill_chunk_rule(
1598            None,
1599            ChunkHost::Other,
1600            self.chunk_stack_facts().dense_on_discrete(),
1601        )
1602    }
1603
1604    /// Architectures that run a dedicated per-token stack (gemma-3n's
1605    /// AltUp flow, the gemma-4 E-series PLE/shared-KV body): batched
1606    /// prefill and the graphs stay off for them.
1607    fn per_token_stack(&self) -> bool {
1608        self.g3n.is_some() || self.g4e.is_some()
1609    }
1610
1611    fn chunk_stack_facts(&self) -> ChunkStackFacts {
1612        let plain_dense = !self.weights.layers.is_empty()
1613            && !self.per_token_stack()
1614            && self.dsv4.is_none()
1615            && self.dsv41.is_none()
1616            && self.qwen4_exp.is_none()
1617            && self.weights.layers.iter().all(|lw| {
1618                // LFM2/LFM3's short-conv mixer is two projection GEMMs and
1619                // an elementwise conv: the host-driven chunk walk starves
1620                // the card the same way (LFM3-1.7B q8_2f on an A40, 2048
1621                // positions: 137 tok/s at 48, 460-640 at 512).
1622                matches!(lw.attn, AttnKind::Full { .. } | AttnKind::ShortConv(_))
1623                    && matches!(lw.ffn, FfnKind::Dense(_))
1624            });
1625        let gpu_on = crate::gpu::enabled();
1626        ChunkStackFacts {
1627            plain_dense,
1628            discrete: gpu_on && crate::gpu::discrete(),
1629            gpu_on,
1630            // Only asked when the rest already qualifies: it opens the
1631            // backend's capacity plan.
1632            capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1633                || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1634            multi_gpu: self.gpu_plan.is_some(),
1635            o1: self.o1_active(),
1636        }
1637    }
1638}
1639
1640/// Prefill chunk (positions per batched pass), model-agnostic form. On
1641/// macOS the AMX GEMM path wants tall panels — M=48 starves the matrix
1642/// units (ggml uses ubatch 512); elsewhere the historical 48 stays.
1643/// CMF_PREFILL_CHUNK overrides. The architectures with their own stacks
1644/// (DeepSeek-V4/V4.1) chunk with this; the layer-stack prefill asks
1645/// [`Pipeline::prefill_chunk`], which also knows the model and the card.
1646/// A different chunk is a different (equally valid) generation: panel
1647/// width reorders float accumulation.
1648pub fn prefill_chunk() -> usize {
1649    prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1650}
1651
1652fn env_prefill_chunk() -> Option<usize> {
1653    std::env::var("CMF_PREFILL_CHUNK")
1654        .ok()
1655        .and_then(|v| v.parse::<usize>().ok())
1656}
1657
1658/// The host classes the chunk width distinguishes.
1659#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1660enum ChunkHost {
1661    Macos,
1662    /// Linux/Android aarch64 (phones, SBCs).
1663    Aarch64,
1664    /// Everything else: x86-64 Linux/Windows, CPU or Vulkan/DX12.
1665    Other,
1666}
1667
1668impl ChunkHost {
1669    fn here() -> Self {
1670        if cfg!(target_os = "macos") {
1671            ChunkHost::Macos
1672        } else if cfg!(target_arch = "aarch64") {
1673            ChunkHost::Aarch64
1674        } else {
1675            ChunkHost::Other
1676        }
1677    }
1678}
1679
1680/// Chunk for a plain dense stack whose every layer lives on a discrete
1681/// card. On x86 the layer-stack prefill is host-driven: each GEMM and the
1682/// chunk attention (which re-uploads the whole KV prefix per layer) is a
1683/// separate submit + readback, so 48 positions a pass left the card idle
1684/// between them. Measured in-process on an RTX 3090 (Vulkan), 2048-token
1685/// prompt — see CHANGELOG 0.7.6 for the table.
1686const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1687
1688/// The chunk-width rule. `dense_on_discrete` is true only for a plain
1689/// dense transformer (full attention, dense FFN, no special stack) that
1690/// is entirely resident on one discrete card — the one case measured
1691/// here. GDN hybrids, MoE, DeepSeek stacks, capacity-split and CPU-only
1692/// runs keep the width they were tuned with.
1693fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1694    if let Some(n) = env {
1695        return n.max(1);
1696    }
1697    match host {
1698        ChunkHost::Macos => 512,
1699        // Mobile: big enough to feed the batched attend (gate b ≥ 32)
1700        // and the blocked SDOT GEMM without the memory of 512.
1701        ChunkHost::Aarch64 => 256,
1702        ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1703        ChunkHost::Other => 48,
1704    }
1705}
1706
1707/// What the chunk rule needs to know about a loaded stack.
1708#[derive(Clone, Copy, Debug, Default)]
1709struct ChunkStackFacts {
1710    /// Every layer is `AttnKind::Full` or `AttnKind::ShortConv` +
1711    /// `FfnKind::Dense`, and no architecture-owned stack (g3n,
1712    /// DeepSeek-V4/V4.1, qwen4-exp) is set.
1713    plain_dense: bool,
1714    /// The active GPU backend is a discrete card.
1715    discrete: bool,
1716    /// The backend is up and not paused.
1717    gpu_on: bool,
1718    /// A capacity-derived device prefix: some layers run on the host.
1719    capacity_split: bool,
1720    /// An in-process multi-GPU plan is set.
1721    multi_gpu: bool,
1722    /// O(1) layers (their Q trace is recorded by the prefill).
1723    o1: bool,
1724}
1725
1726impl ChunkStackFacts {
1727    fn dense_on_discrete(self) -> bool {
1728        self.plain_dense
1729            && self.discrete
1730            && self.gpu_on
1731            && !self.capacity_split
1732            && !self.multi_gpu
1733            && !self.o1
1734    }
1735}
1736
1737/// Number of prompt rows that have a real teacher-forced next-token pair in a
1738/// prefill span.  The final prompt row has no successor token, so it must not
1739/// be handed to the MTP warm-up.  Keeping this arithmetic in one helper makes
1740/// the full-chunk and tail-chunk boundaries explicit for both the graph and
1741/// CPU implementations.
1742#[inline]
1743fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1744    if end <= start || start >= input_len {
1745        return 0;
1746    }
1747    let rows = (end.min(input_len) - start).min(input_len - start);
1748    if end < input_len {
1749        rows
1750    } else {
1751        rows.saturating_sub(1)
1752    }
1753}
1754
1755/// Callback for streaming tokens. Return `false` to cancel.
1756pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1757
1758/// One layer's cache ownership at a cross-turn KV reuse boundary.
1759#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1760pub(crate) struct ReuseLayer {
1761    /// Exact-attention layer (rows in `LayerKvCache`); otherwise a
1762    /// recurrent / latent mixer whose state cannot be rewound.
1763    pub full: bool,
1764    /// Positions the host owner cache reaches (`pos_len`: a trimmed
1765    /// sliding tail stores only the newest of them).
1766    pub host_rows: usize,
1767    /// Rows the wgpu token graph's device mirror holds (None: no mirror).
1768    pub device_rows: Option<usize>,
1769    /// A recurrent state lives on the device (advanced past the host copy).
1770    pub device_state: bool,
1771}
1772
1773/// What a reused turn must do before its tail prefill runs on the HOST.
1774#[derive(Debug, Clone, PartialEq, Eq)]
1775pub(crate) enum ReusePlan {
1776    /// Host caches already hold exactly the reused prefix.
1777    Ready,
1778    /// Copy device mirror rows `[from..to)` into the host cache of each
1779    /// listed layer (the rows decode wrote on the device only).
1780    Pull(Vec<(usize, usize, usize)>),
1781    /// The prefix cannot be continued on the host exactly: start fresh.
1782    Fresh,
1783}
1784
1785/// The wgpu whole-token graph decodes into a DEVICE K/V mirror and never
1786/// writes those rows back to the host cache, while the chunked prefill of a
1787/// pure-attention model reads (and appends to) the host cache. A reused turn
1788/// therefore found its host cache ending at the previous PROMPT, not at the
1789/// previous answer: the tail prefill attended without the model's own
1790/// answer and appended its rows at the wrong index (MiniCPM5 on Vulkan
1791/// repeated its tool call instead of reading the tool result). Every layer
1792/// must hold exactly `reuse_from` host rows before the host continues; rows
1793/// that exist only on the device are pulled back, anything else is fresh.
1794pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1795    let mut pulls = Vec::new();
1796    for (li, l) in layers.iter().enumerate() {
1797        if !l.full {
1798            if l.device_state {
1799                return ReusePlan::Fresh;
1800            }
1801            continue;
1802        }
1803        if l.host_rows == reuse_from {
1804            continue;
1805        }
1806        if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1807            pulls.push((li, l.host_rows, reuse_from));
1808            continue;
1809        }
1810        return ReusePlan::Fresh;
1811    }
1812    if pulls.is_empty() {
1813        ReusePlan::Ready
1814    } else {
1815        ReusePlan::Pull(pulls)
1816    }
1817}
1818
1819impl Pipeline {
1820    /// Reset state whose meaning is local to a single MTP draft sequence.
1821    /// Unlike ordinary KV reuse, MTP is always rebuilt from the prompt.
1822    fn reset_fresh_mtp_draft_state(&mut self) {
1823        self.draft_full_streak = 0;
1824    }
1825
1826    /// Clear all per-sequence state, including backend device mirrors.
1827    ///
1828    /// The host KV/history buffers are only half of the request lifecycle on
1829    /// wgpu: GDN/O(1) state and cached graph bind groups are keyed by the
1830    /// pipeline id and otherwise survive a pooled request.  Keep every fresh
1831    /// sequence entry point on this one reset path so a new request cannot
1832    /// inherit the prior request's device state.
1833    fn clear_sequence_state(&mut self) {
1834        // a replay still writing the GDN owners must land before they are
1835        // cleared or reallocated (the device holds raw pointers to them)
1836        #[cfg(target_os = "macos")]
1837        let _ = crate::gpu_metal::wait_replay();
1838        self.kv_cache.clear();
1839        // Both reuse keys (the legacy `kv_history` and the bounded
1840        // `kv_prefix`) describe the state being dropped here.
1841        self.clear_history();
1842        self.graph_logits = None;
1843        if let Some(b) = &mut self.dsv41 {
1844            b.3.clear();
1845        }
1846        crate::gpu::graph_kv_reset(self.graph_kv_id);
1847        // MTP is detached from `self` for the duration of generation, so its
1848        // device mirror is not covered by the trunk reset above.  Reset the
1849        // derived id as well: a failed/aborted warm-up must never leave a
1850        // mirror that a later request can mistake for a current MTP cache.
1851        crate::gpu::graph_kv_reset(self.mtp_kv_id());
1852    }
1853
1854    /// Make the host caches own exactly the reused prefix `[0..reuse_from)`
1855    /// before a reused turn's tail prefill runs on the host (see
1856    /// [`kv_reuse_plan`]). Returns false when the prefix cannot be continued
1857    /// exactly — the caller then starts a fresh sequence. A model whose
1858    /// prefill runs through the token graph keeps its device state as the
1859    /// authority and is left untouched.
1860    fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1861        if self.graph_prefill_preferred() {
1862            return true;
1863        }
1864        let kv_id = self.graph_kv_id;
1865        let layers: Vec<ReuseLayer> = (0..self.num_layers)
1866            .map(|li| {
1867                let full = matches!(
1868                    self.weights.layers[self.phys_layer(li)].attn,
1869                    AttnKind::Full { .. }
1870                );
1871                ReuseLayer {
1872                    full,
1873                    // Absolute depth: a trimmed sliding tail stores fewer
1874                    // rows than the positions it has seen.
1875                    host_rows: self.kv_cache.layers[li].pos_len(),
1876                    device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1877                    device_state: crate::gpu::graph_state_resident(kv_id, li),
1878                }
1879            })
1880            .collect();
1881        // No wgpu device state at all (CPU, Metal — whose graph appends every
1882        // decoded row to the owner cache itself): the host is the owner and
1883        // the extension check already proved the prefix.
1884        if layers
1885            .iter()
1886            .all(|l| l.device_rows.is_none() && !l.device_state)
1887        {
1888            return true;
1889        }
1890        let plan = kv_reuse_plan(reuse_from, &layers);
1891        let (what, rows, n) = match &plan {
1892            ReusePlan::Ready => ("host ready", 0, 0),
1893            ReusePlan::Fresh => ("fresh", 0, 0),
1894            ReusePlan::Pull(p) => (
1895                "pull",
1896                p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1897                p.len(),
1898            ),
1899        };
1900        let t0 = std::time::Instant::now();
1901        let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1902        if std::env::var("CMF_PREFILL_PROF").is_ok() {
1903            eprintln!(
1904                "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1905                if ok { "" } else { " (failed → fresh)" },
1906                t0.elapsed().as_secs_f64() * 1e3
1907            );
1908        }
1909        ok
1910    }
1911
1912    fn apply_kv_reuse_plan(
1913        &mut self,
1914        reuse_from: usize,
1915        plan: ReusePlan,
1916        layers: &[ReuseLayer],
1917    ) -> bool {
1918        let kv_id = self.graph_kv_id;
1919        match plan {
1920            ReusePlan::Fresh => return false,
1921            ReusePlan::Ready => {}
1922            ReusePlan::Pull(pulls) => {
1923                // Mirrors of one uniform geometry: one batched read serves
1924                // every layer. Per-layer geometry (MiMo-V2: 4/8 KV heads,
1925                // narrow V, sliding rings) is read layer by layer in the
1926                // host layout instead.
1927                let (nkv, hd) = {
1928                    let c = &self.kv_cache.layers[pulls[0].0];
1929                    (c.num_kv_heads, c.head_dim)
1930                };
1931                let uniform = pulls.iter().all(|&(li, _, _)| {
1932                    let c = &self.kv_cache.layers[li];
1933                    (c.num_kv_heads, c.head_dim) == (nkv, hd)
1934                });
1935                let batched = if uniform {
1936                    crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1937                } else {
1938                    None
1939                };
1940                let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1941                    Some(rows) => rows,
1942                    None => {
1943                        let mut rows = Vec::with_capacity(pulls.len());
1944                        for &(li, from, to) in &pulls {
1945                            let (lnkv, lhd) = {
1946                                let c = &self.kv_cache.layers[li];
1947                                (c.num_kv_heads, c.head_dim)
1948                            };
1949                            let Some((k, v, first_valid)) =
1950                                crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1951                            else {
1952                                return false;
1953                            };
1954                            // The host continues at `to`: a sliding layer
1955                            // reads back only its last window, a full one
1956                            // every row it lacks.
1957                            let need_from = match self.layer_window(li) {
1958                                Some(w) => from.max((to + 1).saturating_sub(w)),
1959                                None => from,
1960                            };
1961                            if first_valid > need_from {
1962                                return false;
1963                            }
1964                            rows.push((k, v));
1965                        }
1966                        rows
1967                    }
1968                };
1969                for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1970                    let cache = &mut self.kv_cache.layers[li];
1971                    let row = cache.num_kv_heads * cache.head_dim;
1972                    for p in 0..to - from {
1973                        cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1974                    }
1975                    if cache.pos_len() != to {
1976                        return false;
1977                    }
1978                }
1979            }
1980        }
1981        // A mirror past the prefix (a greedy burst that ran beyond the stop)
1982        // holds rows of the OLD continuation: rewind it so the next graph
1983        // token re-syncs those positions from the host.
1984        for (li, l) in layers.iter().enumerate() {
1985            if l.full
1986                && l.device_rows.is_some_and(|d| d > reuse_from)
1987                && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1988            {
1989                return false;
1990            }
1991        }
1992        true
1993    }
1994
1995    /// Finish a generation lifecycle after the MTP/router owners were
1996    /// detached.  Every terminal path must put those owners back before the
1997    /// pooled pipeline can serve another request.  Graph side channels and
1998    /// device mirrors are cleared on errors and cancellations; a successful
1999    /// generation keeps its decode-ready host cache for KV reuse.
2000    fn finish_generation(
2001        &mut self,
2002        mtp: &mut Option<MtpModule>,
2003        router: &mut Option<crate::swarm::DynRouter>,
2004        clear_sequence: bool,
2005    ) {
2006        // A dynamic route may have switched the overlay before the terminal
2007        // path. Restore the backbone while the detached router is still
2008        // available, because set_active_skill also owns the overlay reset.
2009        if router.is_some() {
2010            let _ = self.set_active_skill(None);
2011        }
2012        // The last speculative round's replay may still be in flight on
2013        // the second queue: whoever reads the host cache after generate()
2014        // returns (session export, the network split's KV wire, a KV
2015        // reuse) must see the final states.
2016        // A replay that failed leaves the GDN owners half-written: fail
2017        // closed and drop the sequence instead of handing the cache on.
2018        #[cfg(target_os = "macos")]
2019        let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
2020        if clear_sequence {
2021            self.clear_sequence_state();
2022            if let Some(m) = mtp.as_mut() {
2023                // The MTP owner is detached while generation runs, so the
2024                // trunk reset above cannot clear its host cache.  Drop its
2025                // partial rows before reattaching it to the pooled pipeline;
2026                // the next request must start from the same empty anchor on
2027                // CPU and on the device mirror.
2028                m.kv.clear();
2029            }
2030            if let Some(m) = self.mtp.as_mut() {
2031                // A non-speculative request leaves the configured MTP owner
2032                // attached.  Clear that dormant cache too when a shared
2033                // generation failure/cancellation resets the sequence.
2034                m.kv.clear();
2035            }
2036        }
2037        self.graph_want_logits = false;
2038        self.graph_head_required = false;
2039        self.graph_logits = None;
2040        self.graph_failed
2041            .store(false, std::sync::atomic::Ordering::Relaxed);
2042        self.cancel
2043            .store(false, std::sync::atomic::Ordering::Relaxed);
2044        self.dyn_router = router.take().or(self.dyn_router.take());
2045        self.mtp = mtp.take().or(self.mtp.take());
2046        self.mtp_graph_mode = None;
2047        self.spec_forced = None;
2048    }
2049
2050    /// Consume a graph failure reported by a forward that returns only a
2051    /// hidden vector.  `forward_ids` is a public Result API, so it must not
2052    /// turn the graph's zero hidden sentinel into a valid lm_head result.
2053    fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2054        if self
2055            .graph_failed
2056            .swap(false, std::sync::atomic::Ordering::Relaxed)
2057        {
2058            self.cancel
2059                .store(false, std::sync::atomic::Ordering::Relaxed);
2060            self.clear_sequence_state();
2061            self.graph_logits = None;
2062            self.graph_want_logits = false;
2063            self.graph_head_required = false;
2064            return Err(format!("GPU graph failed during {phase} at position {pos}"));
2065        }
2066        Ok(())
2067    }
2068
2069    #[cfg(target_os = "macos")]
2070    fn fail_metal_graph(&mut self, reason: &str) {
2071        crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2072        self.clear_sequence_state();
2073        self.graph_logits = None;
2074        self.graph_failed
2075            .store(true, std::sync::atomic::Ordering::Relaxed);
2076        self.cancel
2077            .store(true, std::sync::atomic::Ordering::Relaxed);
2078        tracing::error!("native Metal TokenGraph failed closed: {reason}");
2079    }
2080
2081    /// Start an NLL/PPL request with all graph side channels in a known
2082    /// state.  A graph failure also raises the cooperative cancel bit; it is
2083    /// consumed here and that graph-induced bit is cleared so an independent
2084    /// request can be reused.  A caller-owned cancellation remains intact.
2085    fn nll_begin(&mut self) -> Result<(), String> {
2086        if self
2087            .graph_failed
2088            .swap(false, std::sync::atomic::Ordering::Relaxed)
2089        {
2090            self.cancel
2091                .store(false, std::sync::atomic::Ordering::Relaxed);
2092            self.clear_sequence_state();
2093            self.graph_logits = None;
2094            self.graph_want_logits = false;
2095            self.graph_head_required = false;
2096            return Err("GPU graph failed before NLL scoring".to_string());
2097        }
2098        self.clear_sequence_state();
2099        self.graph_logits = None;
2100        self.graph_want_logits = false;
2101        self.graph_head_required = false;
2102        Ok(())
2103    }
2104
2105    /// End an NLL/PPL request, including the side channels that are not part
2106    /// of the host KV cache.  This is intentionally explicit instead of
2107    /// relying on a tuple/sentinel return: callers must see every failure.
2108    fn nll_end(&mut self) {
2109        self.clear_sequence_state();
2110        self.graph_logits = None;
2111        self.graph_want_logits = false;
2112        self.graph_head_required = false;
2113        self.graph_failed
2114            .store(false, std::sync::atomic::Ordering::Relaxed);
2115    }
2116
2117    /// Check the graph failure channel at a scoring boundary and leave the
2118    /// pipeline reusable when the device path failed.
2119    fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2120        #[cfg(test)]
2121        if self.nll_test_fail_at == Some(pos) {
2122            self.nll_test_fail_at = None;
2123            self.graph_failed
2124                .store(true, std::sync::atomic::Ordering::Relaxed);
2125            self.cancel
2126                .store(true, std::sync::atomic::Ordering::Relaxed);
2127        }
2128        if self
2129            .graph_failed
2130            .swap(false, std::sync::atomic::Ordering::Relaxed)
2131        {
2132            self.cancel
2133                .store(false, std::sync::atomic::Ordering::Relaxed);
2134            self.clear_sequence_state();
2135            self.graph_logits = None;
2136            self.graph_want_logits = false;
2137            return Err(format!(
2138                "GPU graph failed during NLL {phase} at position {pos}"
2139            ));
2140        }
2141        Ok(())
2142    }
2143
2144    /// Map a virtual layer index to its physical weight index.
2145    /// Looped Transformer (Nanbeige 4.2): 22 physical layers × 2 loops = 44 virtual;
2146    /// virtual layer 23 maps back to physical layer 1 (23 % 22 = 1).
2147    #[inline]
2148    pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2149        match &self.layer_map {
2150            Some(m) => m[virtual_idx],
2151            None => virtual_idx % self.physical_layers,
2152        }
2153    }
2154
2155    /// Mage-VL (`llava_onevision2`) prompt ingest through the batched wgpu
2156    /// graph (`CMF_GRAPH_PREFILL=0` keeps the chunked host prefill).
2157    fn onevision2_prefill(&self) -> bool {
2158        self.model.as_ref().is_some_and(|m| m.header.arch.arch_name == "llava_onevision2")
2159            && std::env::var("CMF_GRAPH_PREFILL").as_deref() != Ok("0")
2160    }
2161
2162    /// SenseNova-Loop: whether virtual layer `li` runs inside the mHC
2163    /// streams (the device graphs decline such layers).
2164    #[inline]
2165    pub fn mhc_layer(&self, li: usize) -> bool {
2166        self.mhc.as_ref().is_some_and(|m| m.on(self.phys_layer(li)))
2167    }
2168
2169    /// mHC before a sub-layer of the decode token: route its streams into
2170    /// `h` (entering the range copies `h` into every stream first).
2171    fn mhc_pre(&mut self, li: usize, ffn: bool, h: &mut [f32]) -> crate::sensenova::MhcCoef {
2172        let p = self.phys_layer(li);
2173        let m = self.mhc.as_deref().expect("mhc layer");
2174        let n = m.s.streams * m.dim;
2175        if self.mhc_r.len() != n {
2176            self.mhc_r.resize(n, 0.0);
2177        }
2178        if !ffn && m.enters(p) {
2179            m.enter(&mut self.mhc_r, h);
2180        }
2181        m.route(p, ffn, &self.mhc_r, h)
2182    }
2183
2184    /// mHC after a sub-layer: fold its output `y` into the streams; after
2185    /// the range's last FFN, `h` becomes the streams' mean.
2186    fn mhc_post(&mut self, li: usize, ffn: bool, y: &[f32], c: &crate::sensenova::MhcCoef, h: &mut [f32]) {
2187        let p = self.phys_layer(li);
2188        let m = self.mhc.as_deref().expect("mhc layer");
2189        m.merge(&mut self.mhc_r, y, c);
2190        if ffn && m.leaves(p) {
2191            m.leave(&self.mhc_r, h);
2192        }
2193    }
2194
2195    /// True when `virtual_idx` is the last layer of a loop iteration
2196    /// (used for loop_final_norm insertion).
2197    #[inline]
2198    pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2199        self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2200    }
2201
2202    /// Build a pipeline from parts (used by the loader and tests).
2203    #[allow(clippy::too_many_arguments)]
2204
2205    /// Whole-block q1 token graph on the GPU (macOS/Metal): the run of
2206    /// consecutive q1 layers — GDN *and* full attention — starting at
2207    /// `start` executes as few command buffers as the CPU truly needs.
2208    /// Hidden stays device-resident across every layer; the only syncs
2209    /// are before each CPU attend (it needs q/k/v and owns the KV
2210    /// cache) and the final hidden readback. Recurrent states
2211    /// round-trip through shared memory (the CPU stays their owner, so
2212    /// every other path remains coherent). Returns the first layer
2213    /// index NOT covered (== `start` → refused, caller falls through
2214    /// to the per-layer CPU path).
2215    /// Should prefill run position-by-position through the GPU token
2216    /// graph instead of the batched CPU chunk-GEMM? True for q1 GDN
2217    /// hybrids on native Metal: their chunk prefill is walled by the
2218    /// sequential scalar recurrence, so the graph's decode rate wins.
2219    /// NOT for Looped Transformers, despite the per-chunk loop_final_norm
2220    /// sync: the chunk-GEMM amortizes each weight over the whole chunk,
2221    /// which the per-position graph cannot (Nanbeige 4.2 on M4, 512-token
2222    /// prompt: 85 tok/s chunked vs 14 through the graph).
2223    #[cfg(target_os = "macos")]
2224    fn graph_prefill_preferred(&self) -> bool {
2225        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2226        if !crate::gpu::enabled_here()
2227            || !graph_force
2228            || std::env::var("CMF_GPU_BLOCK")
2229                .map(|v| v == "0")
2230                .unwrap_or(false)
2231            // CMF_PREFILL_GRAPH=0: the chunked prefill (GEMM projections,
2232            // CPU recurrence) instead of the per-position token graph.
2233            || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2234        {
2235            return false;
2236        }
2237        self.weights
2238            .layers
2239            .iter()
2240            .any(|lw| {
2241                matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2242            })
2243    }
2244
2245    /// Prompt ingest through the batched wgpu graph in device-prefix mode:
2246    /// a MoE stack that does not fit the card runs each chunk's leading
2247    /// layers on the device (experts resident) and the rest on the host's
2248    /// batched walk. On by default for models with per-layer attention
2249    /// geometry (MiMo-V2 — its measured default); `CMF_BATCH_PREFIX=1`
2250    /// opts any other MoE model in, `=0` keeps the chunked host prefill.
2251    #[cfg(not(target_os = "macos"))]
2252    fn batch_prefix_prefill(&self) -> bool {
2253        let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2254            Ok("0") => return false,
2255            Ok("1") => true,
2256            _ => false,
2257        };
2258        crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2259            && crate::gpu::enabled_here()
2260            && !self.graph_refused()
2261            && (forced || self.graph_attn_decline_reason().is_some())
2262            && self.wgpu_graph_attn_decline().is_none()
2263            && self.attn_softcap == 0.0
2264            && self
2265                .weights
2266                .layers
2267                .iter()
2268                .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2269            && self.automatic_gpu_prefix().is_some()
2270    }
2271
2272    /// Gemma-4's prompt through the batched wgpu graph (`gemma_batch`): a
2273    /// dense stack wholly on a discrete card, whose token graph already
2274    /// decodes it. Not `graph_prefill_preferred` — the host stays the KV
2275    /// owner for reuse and scoring; only generation's ingest changes.
2276    /// `CMF_G4_BATCH=0` keeps the chunked host prefill.
2277    fn g4_batch_prefill(&self) -> bool {
2278        #[cfg(target_os = "macos")]
2279        {
2280            false
2281        }
2282        #[cfg(not(target_os = "macos"))]
2283        {
2284            // An EMPTY layer stack (`all()` is vacuously true) plus a
2285            // gemma4 geometry flag is exactly the gemma-4 E-series
2286            // per-token stack — it has no batch graph rows to ingest.
2287            !self.weights.layers.is_empty()
2288                && !self.per_token_stack()
2289                && (self.global_attn.is_some() || self.attn_v_norm)
2290                && crate::gpu::g4_batch_on()
2291                && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2292                && crate::gpu::enabled_here()
2293                && crate::gpu::discrete()
2294                && !self.graph_refused()
2295                && !self.o1_active()
2296                && self.attn_softcap == 0.0
2297                && self.wgpu_graph_attn_decline().is_none()
2298                && self.weights.layers.iter().all(|lw| {
2299                    matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2300                })
2301                && self.automatic_gpu_prefix().is_none()
2302        }
2303    }
2304
2305    /// Gemma-4's batched prefill of `ids` at positions `pos0..`
2306    /// (`g4_batch_prefill`): chunks of `generation_batch_k` rows through the
2307    /// batch graph. Ok((n, rows)): `n` positions ingested; `rows` = the last
2308    /// one's hidden, or every row's (`keep_all`). A refusal stops early
2309    /// (n < ids.len()) after pulling the device rows the host walk needs.
2310    /// Err: a failure after admission — the device state is undefined.
2311    #[cfg(not(target_os = "macos"))]
2312    fn g4_batch_ingest(
2313        &mut self,
2314        ids: &[u32],
2315        pos0: usize,
2316        keep_all: bool,
2317    ) -> Result<(usize, Vec<f32>), String> {
2318        let hs = self.hidden_size;
2319        let chunk = self.generation_batch_k().max(1);
2320        let end_all = pos0 + ids.len();
2321        let mut pos = pos0;
2322        let mut out = Vec::new();
2323        while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2324            let end = (pos + chunk).min(end_all);
2325            let bk = end - pos;
2326            let mut hiddens = vec![0f32; bk * hs];
2327            for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2328                hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2329            }
2330            let positions: Vec<usize> = (pos..end).collect();
2331            let t_chunk = std::time::Instant::now();
2332            let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2333            if std::env::var("CMF_GRAPH_PROF").is_ok() {
2334                let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2335                eprintln!(
2336                    "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2337                    bk as f64 / (ms / 1000.0)
2338                );
2339            }
2340            match outcome {
2341                crate::gpu::BatchGraphOutcome::Completed => {
2342                    if keep_all {
2343                        out.extend_from_slice(&hiddens);
2344                    } else {
2345                        out = hiddens[(bk - 1) * hs..].to_vec();
2346                    }
2347                    pos = end;
2348                }
2349                crate::gpu::BatchGraphOutcome::Failed => {
2350                    return Err("Gemma-4 batched prefill failed after admission".into());
2351                }
2352                crate::gpu::BatchGraphOutcome::Declined => {
2353                    #[cfg(feature = "gpu")]
2354                    if pos > pos0 {
2355                        self.pull_lagging_host_kv(0, self.num_layers, pos);
2356                    }
2357                    break;
2358                }
2359            }
2360        }
2361        Ok((pos - pos0, out))
2362    }
2363
2364    /// Hidden rows of every position of `ids` through Gemma-4's batched
2365    /// prefill from a fresh sequence (the prefill's teacher-forced logits
2366    /// for parity gates: `logits_from_hidden` per row). None when the model
2367    /// or the card does not take that route, or it refused part way.
2368    pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2369        #[cfg(not(target_os = "macos"))]
2370        {
2371            if ids.is_empty() || !self.g4_batch_prefill() {
2372                return None;
2373            }
2374            self.clear_sequence_state();
2375            match self.g4_batch_ingest(ids, 0, true) {
2376                Ok((n, rows)) if n == ids.len() => Some(rows),
2377                _ => {
2378                    self.clear_sequence_state();
2379                    None
2380                }
2381            }
2382        }
2383        #[cfg(target_os = "macos")]
2384        {
2385            let _ = ids;
2386            None
2387        }
2388    }
2389
2390    #[cfg(not(target_os = "macos"))]
2391    fn graph_prefill_preferred(&self) -> bool {
2392        // Discrete-GPU wgpu whole-token graph: GDN layers carry recurrent state
2393        // (conv ring + delta-rule S) resident on the GPU. A batched CPU prefill
2394        // builds that state on the CPU only, leaving the GPU buffers zeroed at
2395        // decode → garbage. Route GDN-hybrid prefill through the graph one
2396        // position at a time so the resident state is seeded exactly as decode
2397        // will read it. Pure-attention models keep the batched CPU prefill (its
2398        // KV mirror re-syncs from the CPU cache, so no seeding gap).
2399        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2400        if !graph_on || !crate::gpu::enabled_here() {
2401            return false;
2402        }
2403        // Embryo's phase state is device-owned by the resident graph during
2404        // prefill; the batched CPU path would leave decode seeing a zeroed
2405        // device recurrence.  Route the prompt position-by-position too.
2406        if self.embryo_resident_eligible() {
2407            // The resident graph owns the phase state and anchor KV on the
2408            // device.  A prefill-only graph would leave decode on the host
2409            // with no way to import that state, so keep the whole sequence
2410            // on one owner (or use the ordinary CPU prefill/decode pair).
2411            return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2412        }
2413        // The descriptor-aware Prism graph now carries both the FWHT/affine
2414        // transforms and resident GDN state, so it is also the exact prefill
2415        // path for this model.  Keeping it here (rather than falling through
2416        // to the CPU chunk walk) is required for a long prompt to seed the
2417        // same device state that decode consumes.
2418        // O(1) needs the CPU prefill: the q-trace that seals the Nyström
2419        // skeleton is recorded there and nowhere else. The GDN half of
2420        // the hybrid loses nothing — the graph's first decode creates
2421        // its (ring, S) entries seeded from `cpu_state`, the same
2422        // handoff every graph run relies on when the entry is fresh.
2423        // Without this line the two designs collide on hybrids and o1
2424        // never becomes graph-portable: prefill through the graph
2425        // records no trace, so views stay None forever.
2426        if self.o1_active() {
2427            return false;
2428        }
2429        // A model the wgpu graphs decline outright would walk its prompt
2430        // one position at a time through a graph that never runs: the
2431        // batched CPU chunk prefill is the right ingest for it.
2432        if self.wgpu_graph_attn_decline().is_some() {
2433            return false;
2434        }
2435        if self
2436            .weights
2437            .layers
2438            .iter()
2439            .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2440            // Mage-VL: its screenshot prompts (~2k rows) go through the
2441            // batched graph, 4× the chunked host ingest on an A40 (2048
2442            // tokens: 806 tok/s at 128 rows, 893 at 256, against 196).
2443            || self.onevision2_prefill()
2444        {
2445            return true;
2446        }
2447        // MoE models too: the chunked CPU prefill runs every expert on the
2448        // host (Hy-MT2-30B-A3B on a Xeon: 8 tok/s of ingest against 53 of
2449        // graph decode), while the token graph — and the batched graph under
2450        // CMF_BATCH_K — keep the experts resident. Full attention in the
2451        // graph writes the KV mirror that decode reads, exactly as it does
2452        // for the hybrids' attention layers. Only when the whole stack is
2453        // resident: with a device prefix the per-position walk finishes
2454        // every token on the host, and the chunked prefill (GEMMs on the
2455        // card, the expert loop batched on the host) is the faster ingest
2456        // (the 8 GB ladder point: 7 tok/s chunked against ~1 walked).
2457        self.weights
2458            .layers
2459            .iter()
2460            .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2461            && self.automatic_gpu_prefix().is_none()
2462    }
2463
2464    #[cfg(target_os = "macos")]
2465    fn q1_graph_gpu(
2466        &mut self,
2467        start: usize,
2468        upto: Option<usize>,
2469        position: usize,
2470        h: &mut [f32],
2471    ) -> usize {
2472        let _mt0 = std::time::Instant::now(); // CMF_METAL_HOSTPROF
2473        use crate::gpu::{
2474            AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, SconvGpuLayer, TokenGraph,
2475        };
2476        const MOE_WORKING_SET_REASON: &str =
2477            "model weights exceed Metal working set for native MoE graph";
2478        // A prior admission check rejected this pipeline. Avoid rebuilding the
2479        // entire 40-layer graph plan at every individual host layer after the
2480        // fallback begins; the per-operation path below stays eligible.
2481        if self
2482            .graph_declines
2483            .borrow()
2484            .iter()
2485            .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2486        {
2487            return start;
2488        }
2489        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2490        if self.attn_softcap > 0.0 // capped scores: no graph kernel — CPU path
2491            || !self.metal_rope_scale_ok()
2492            || !crate::gpu::enabled_here()
2493            || !graph_force
2494            || std::env::var("CMF_GPU_BLOCK")
2495                .map(|v| v == "0")
2496                .unwrap_or(false)
2497        {
2498            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2499                eprintln!(
2500                    "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2501                    self.attn_softcap > 0.0,
2502                    crate::gpu::enabled_here(),
2503                    graph_force,
2504                );
2505            }
2506            if self.graph_head_required {
2507                self.fail_metal_graph("native graph front gate refused");
2508            }
2509            return start;
2510        }
2511        // The graph encodes SiLU or exact-GELU FFNs and attention with an
2512        // explicit model scale. Sliding-window layers with their own RoPE
2513        // table / rotary width ride it too (`metal_graph_swa`): both the
2514        // device attend and the sandwich's CPU attend read each layer's
2515        // window and table. Sandwich norms and other activations fall back
2516        // to the CPU path.
2517        // Gemma-4 (`metal_gemma4`): its sliding layers ride the same door,
2518        // its global layers, V norm, sandwich norms, layer scalar and
2519        // tanh-GELU the `gpu_metal::gemma4` kernels.
2520        let gemma4 = self.metal_gemma4();
2521        let swa_graph = self.metal_graph_swa() || gemma4;
2522        // The normal native GQA kernel carries a learned, value-less
2523        // softmax column.  It also takes the exact per-layer sliding
2524        // window, but its split-K twin has no sink column.  Thus a
2525        // sink-bearing model may use this door only when the ordinary
2526        // sliding geometry has passed `metal_graph_swa` (or when it has no
2527        // sliding layers at all).
2528        let graph_attn_reason = self.graph_attn_decline_reason();
2529        let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2530            && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2531        // Plain-weight (x̂·w) sandwich norms (LFM3) take the generic norm
2532        // kernels, as on the wgpu graph; Gemma-2/3's (1+w) norms and the
2533        // layer scalar stay Gemma-4's.
2534        let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
2535        if (self.swa.is_some() && !swa_graph)
2536            || (self.global_attn.is_some() && !gemma4)
2537            || self.attention_heads_per_layer.is_some()
2538            || (self.attn_v_norm && !gemma4)
2539            // Per-layer KV heads / narrow V stay outside this graph.  A
2540            // learned sink is admitted only through native_sink_attend.
2541            || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2542            || self.weights.layers.iter().any(|lw| {
2543                ((lw.layer_scale.is_some()
2544                    || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some())))
2545                    && !gemma4)
2546                    || matches!(&lw.ffn, FfnKind::Dense(d)
2547                        if !(matches!(d.act, Act::Silu | Act::Gelu)
2548                            || (gemma4 && d.act == Act::GeluTanh)))
2549            })
2550        {
2551            // The Metal graphs' device attend has no per-layer attention
2552            // geometry (the wgpu graphs do): say so once, by name.
2553            if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2554                self.note_graph_decline("metal block graph", reason);
2555            }
2556            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2557                eprintln!(
2558                    "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2559                    self.swa.is_some(),
2560                    self.global_attn.is_some(),
2561                    self.attention_heads_per_layer.is_some(),
2562                    self.attn_v_norm,
2563                    (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2564                );
2565            }
2566            if self.graph_head_required {
2567                self.fail_metal_graph("native graph architecture gate refused");
2568            }
2569            return start;
2570        }
2571        // Looped Transformer: the graph covers ALL loop iterations;
2572        // encode_loop_norm is inserted on-device at each boundary.
2573        let limit = upto
2574            .map(|u| u + 1)
2575            .unwrap_or(self.num_layers)
2576            .min(self.num_layers);
2577
2578        enum Item<'a> {
2579            Gdn {
2580                run: Vec<GdnGpuLayer<'a>>,
2581                first: usize,
2582            },
2583            /// A run of LFM2/LFM3 short-conv layers (conv ring on device).
2584            Sconv {
2585                run: Vec<SconvGpuLayer<'a>>,
2586                first: usize,
2587            },
2588            Attn {
2589                l: AttnGpuLayer<'a>,
2590                li: usize,
2591                q_norm: Option<&'a [f32]>,
2592                k_norm: Option<&'a [f32]>,
2593                output_gate: bool,
2594                /// `self_attn.g_proj` output gate (per-head flag): the
2595                /// sandwich projects the device-normed input on the host
2596                /// and gates the attend's output before O.
2597                proj_gate: Option<(&'a QTensor, bool)>,
2598                /// The same gate's f32 rows `[nh × hidden]` when the device
2599                /// attend can apply it (per-head sigmoid, f32 in RAM).
2600                head_gate_w: Option<&'a [f32]>,
2601                bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2602                /// Attend on the device too (no sync): F32 KV, no
2603                /// o1/bias, dims inside the kernels' contract.
2604                full_gpu: bool,
2605            },
2606        }
2607
2608        // Device-attend KERNEL contract, shared by every Full layer. The
2609        // hd>128 default-off POLICY is applied after the scan: it was
2610        // measured on dense models, and a MoE plan inverts it — with the
2611        // experts on device each CPU-attend sandwich costs a
2612        // commit+wait, ~30 submits/token (W2 on M4: 14.7 tok/s
2613        // sandwiched vs 27.1 device-attend vs 18.8 pure CPU).
2614        let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2615        let attend_contract = attend_mode != "0"
2616            && attend_mode != "off"
2617            && self.head_dim % 4 == 0
2618            && self.head_dim <= 256
2619            // Flint has explicit no-RoPE layers.  The device RoPE kernel
2620            // accepts rd=0 (zero rotation lanes) and binds a sentinel table;
2621            // retain the historical lane-pair requirement for nonzero rd.
2622            && (self.rotary_dim == 0
2623                || (self.rotary_dim >= 2
2624                    && self.rotary_dim <= self.head_dim
2625                    && (self.rotary_dim / 2) % 32 == 0))
2626            && self.num_kv_heads > 0
2627            && self.num_heads % self.num_kv_heads == 0;
2628
2629        let mut plan: Vec<Item> = Vec::new();
2630        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2631        // Break-reason diagnostics ride the same env as the plan summary.
2632        let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2633        let mut scan = start;
2634        while scan < limit {
2635            // SenseNova-Loop: MRoPE prompt rows rotate on the host; an mHC
2636            // layer rides the graph when its kernels compiled (two streams,
2637            // dense FFN, device attend — checked below).
2638            if self.prompt_mrope.is_some()
2639                || (self.mhc_layer(scan)
2640                    && (!crate::gpu_metal::mhc::available()
2641                        || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
2642                        || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
2643                        || !matches!(self.weights.layers[self.phys_layer(scan)].ffn, FfnKind::Dense(_))))
2644            {
2645                break;
2646            }
2647            let lw = &self.weights.layers[self.phys_layer(scan)];
2648            let ffn = match &lw.ffn {
2649                FfnKind::Dense(d) if d.segs.is_empty() => {
2650                    let (Some(g), Some(u), Some(dn)) = (
2651                        d.gate_proj.metal_graph_parts(),
2652                        d.up_proj.metal_graph_parts(),
2653                        d.down_proj.metal_graph_parts(),
2654                    ) else {
2655                        if block_diag {
2656                            eprintln!(
2657                                "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2658                            );
2659                        }
2660                        break;
2661                    };
2662                    MetalFfn::Dense {
2663                        gate: g,
2664                        up: u,
2665                        down: dn,
2666                        // The front gate admitted SiLU, exact GELU and
2667                        // (Gemma-4) tanh-GELU only.
2668                        act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2669                    }
2670                }
2671                FfnKind::Moe(m) => {
2672                    // Flint's detached Q4 shared expert is intentionally
2673                    // tried before the generic uniform-bank graph builder.
2674                    // The latter correctly refuses its ragged Q2/Q4 widths;
2675                    // this narrow descriptor keeps the router and selected
2676                    // routed experts device-resident inside a bounded page.
2677                    let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2678                    {
2679                        MetalFfn::FlintMoe(flint)
2680                    } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2681                        MetalFfn::Moe(moe)
2682                    } else {
2683                        if block_diag {
2684                            eprintln!(
2685                                "block-graph: L{scan} MoE outside the graph contract — run ends"
2686                            );
2687                        }
2688                        break;
2689                    };
2690                    if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2691                        model_ref.get_or_insert_with(|| model.clone());
2692                    }
2693                    ffn
2694                }
2695                _ => {
2696                    if block_diag {
2697                        eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2698                    }
2699                    break;
2700                }
2701            };
2702            match &lw.attn {
2703                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2704                    let parts = (
2705                        w.in_proj_qkv.metal_graph_parts(),
2706                        w.in_proj_z.metal_graph_parts(),
2707                        w.in_proj_a.f32_parts(),
2708                        w.in_proj_b.f32_parts(),
2709                        w.out_proj.metal_graph_parts(),
2710                    );
2711                    let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2712                        if block_diag {
2713                            eprintln!(
2714                                "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2715                                w.in_proj_qkv.metal_graph_parts().is_some(),
2716                                w.in_proj_z.metal_graph_parts().is_some(),
2717                                w.in_proj_a.f32_parts().is_some(),
2718                                w.in_proj_b.f32_parts().is_some(),
2719                                w.out_proj.metal_graph_parts().is_some(),
2720                            );
2721                        }
2722                        break;
2723                    };
2724                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2725                        model_ref.get_or_insert_with(|| model.clone());
2726                    }
2727                    let gl = GdnGpuLayer {
2728                        mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
2729                        attn_norm: &lw.input_norm,
2730                        post_norm: &lw.post_norm,
2731                        qkv,
2732                        z,
2733                        a,
2734                        b,
2735                        out,
2736                        ffn,
2737                        conv1d: &w.conv1d,
2738                        a_log: &w.a_log,
2739                        dt_bias: &w.dt_bias,
2740                        gnorm: &w.norm,
2741                    };
2742                    match plan.last_mut() {
2743                        Some(Item::Gdn { run, .. }) => run.push(gl),
2744                        _ => plan.push(Item::Gdn {
2745                            run: vec![gl],
2746                            first: scan,
2747                        }),
2748                    }
2749                }
2750                AttnKind::ShortConv(w) if self.short_conv_cfg.is_some() => {
2751                    let (Some(inp), Some(out)) =
2752                        (w.in_proj.metal_graph_parts(), w.out_proj.metal_graph_parts())
2753                    else {
2754                        if block_diag {
2755                            eprintln!("block-graph: L{scan} short-conv parts refused");
2756                        }
2757                        break;
2758                    };
2759                    if let QTensor::Mapped { model, .. } = &w.in_proj {
2760                        model_ref.get_or_insert_with(|| model.clone());
2761                    }
2762                    let sl = SconvGpuLayer {
2763                        attn_norm: &lw.input_norm,
2764                        post_norm: &lw.post_norm,
2765                        in_proj: inp,
2766                        taps: &w.conv,
2767                        out,
2768                        attn_out_norm: lw.attn_out_norm.as_deref(),
2769                        ffn_out_norm: lw.ffn_out_norm.as_deref(),
2770                        ffn,
2771                    };
2772                    match plan.last_mut() {
2773                        Some(Item::Sconv { run, .. }) => run.push(sl),
2774                        _ => plan.push(Item::Sconv {
2775                            run: vec![sl],
2776                            first: scan,
2777                        }),
2778                    }
2779                }
2780                AttnKind::Full {
2781                    wq,
2782                    wk,
2783                    wv,
2784                    wo,
2785                    q_norm,
2786                    k_norm,
2787                    output_gate,
2788                    softplus_gate,
2789                    bias,
2790                } if (!self.kv_cache.layers[scan].o1_sealed()
2791                    // Sealed o1 stays plannable when the Metal o1 port
2792                    // is on: full_gpu attends through the device state,
2793                    // and any refusal falls to the sandwich, whose CPU
2794                    // core routes sealed layers through the nystrom step.
2795                    || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2796                    // A projected output gate: Spark-X2.5's per-head
2797                    // sigmoid form only (Laguna's softplus gate is
2798                    // unmeasured on these paths and keeps the CPU walk).
2799                    && softplus_gate
2800                        .as_ref()
2801                        .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2802                {
2803                    let parts = (
2804                        wq.metal_graph_parts(),
2805                        wk.metal_graph_parts(),
2806                        wv.metal_graph_parts(),
2807                        wo.metal_graph_parts(),
2808                    );
2809                    let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2810                        break;
2811                    };
2812                    if let QTensor::Mapped { model, .. } = wq {
2813                        model_ref.get_or_insert_with(|| model.clone());
2814                    }
2815                    let cache = &self.kv_cache.layers[scan];
2816                    // O(1) layer on Metal: the device attends through the
2817                    // sealed Nystrom state (opt-in while the port proves
2818                    // itself). Unsealed -> sandwich path = the CPU o1 step.
2819                    let o1_metal = cache.o1.is_some()
2820                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2821                        && cache.o1_views().is_some();
2822                    // The device attend takes the layer's window and RoPE
2823                    // table, and the per-head gate from f32 rows; a gate
2824                    // held otherwise sandwiches.
2825                    let head_gate_w = softplus_gate
2826                        .as_ref()
2827                        .and_then(|(g, _)| g.f32_parts())
2828                        .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2829                        .map(|(d, _, _)| d);
2830                    // Gemma-4's global layers are 512 wide over one KV head
2831                    // (the `gemma4` 16-slot kernels, `CMF_METAL_GEMMA4_W16`).
2832                    let (nkv_g, hd_g) = if gemma4 {
2833                        let (a, b, _) = self.layer_geom(scan);
2834                        (a, b)
2835                    } else {
2836                        (self.num_kv_heads, self.head_dim)
2837                    };
2838                    let full_gpu = attend_contract
2839                        && softplus_gate.is_none() == head_gate_w.is_none()
2840                        && cache.mode == crate::kv_cache::KvMode::F32
2841                        && (cache.o1.is_none() || o1_metal)
2842                        && bias.is_none()
2843                        && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2844                        && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2845                        && pk.1 == nkv_g * hd_g
2846                        && pv.1 == nkv_g * hd_g
2847                        && po.2 == self.num_heads * hd_g;
2848                    // An mHC layer needs the device attend (the CPU-attend
2849                    // sandwich has no stream hand-off).
2850                    if !full_gpu && self.mhc_layer(scan) {
2851                        break;
2852                    }
2853                    plan.push(Item::Attn {
2854                        l: AttnGpuLayer {
2855                            mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
2856                            attn_norm: &lw.input_norm,
2857                            post_norm: &lw.post_norm,
2858                            wq: pq,
2859                            wk: pk,
2860                            wv: pv,
2861                            wo: po,
2862                            ffn,
2863                            attn_out_norm: lw.attn_out_norm.as_deref(),
2864                            ffn_out_norm: lw.ffn_out_norm.as_deref(),
2865                            layer_scale: lw.layer_scale,
2866                        },
2867                        li: scan,
2868                        q_norm: q_norm.as_deref(),
2869                        k_norm: k_norm.as_deref(),
2870                        output_gate: *output_gate,
2871                        proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2872                        head_gate_w,
2873                        bias: bias
2874                            .as_ref()
2875                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2876                        full_gpu,
2877                    });
2878                }
2879                _ => break,
2880            }
2881            scan += 1;
2882        }
2883        // SenseNova-Loop: the mHC streams live on the device only inside one
2884        // graph, so a graph may neither enter a range midway nor stop
2885        // inside one (the host walks such a stretch with its own streams).
2886        if let Some(m) = self.mhc.as_deref() {
2887            let inside = |li: usize| m.on(self.phys_layer(li));
2888            let starts_mid = inside(start) && !m.enters(self.phys_layer(start));
2889            let stops_mid = scan > start && inside(scan - 1) && !m.leaves(self.phys_layer(scan - 1));
2890            if starts_mid || stops_mid {
2891                if std::env::var("CMF_GRAPH_DBG").is_ok() {
2892                    eprintln!("q1-graph: [{start}, {scan}) would split an mHC range — host path");
2893                }
2894                return start;
2895            }
2896        }
2897        let Some(model) = model_ref else {
2898            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2899                eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2900            }
2901            if self.graph_head_required {
2902                self.fail_metal_graph("native graph has no mapped model reference");
2903            }
2904            return start;
2905        };
2906        if plan.is_empty() {
2907            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2908                eprintln!("q1-graph: empty plan at layer {start}");
2909            }
2910            if self.graph_head_required {
2911                self.fail_metal_graph("native graph plan is empty");
2912            }
2913            return start;
2914        }
2915        let has_moe = plan.iter().any(|it| match it {
2916            Item::Gdn { run, .. } => run
2917                .iter()
2918                .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2919            Item::Sconv { run, .. } => run
2920                .iter()
2921                .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2922            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2923        });
2924        let has_flint_moe = plan.iter().any(|it| match it {
2925            Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2926            Item::Sconv { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2927            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2928        });
2929        let metal_working_set = crate::gpu_metal::working_set_bytes();
2930        let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2931        let model_bytes = model.primary_bytes().len() as u64;
2932        // Flint q2 can sit below recommendedMaxWorkingSetSize on a 24 GB
2933        // M4 yet still exceed maxBufferLength. In that case a normal graph
2934        // would manufacture overlapping whole-file WeightArena windows;
2935        // treat it as the same bounded-page requirement rather than letting
2936        // the generic graph reintroduce broad residency.
2937        let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2938            || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2939        // An oversized MoE mmap normally has to decline: the jobs kernels
2940        // bind all WeightArena windows.  The opt-in pager takes a different,
2941        // bounded route: a *complete sequence of logical layers* is put in
2942        // one transient no-copy page, graph-finished, dropped, then the outer
2943        // layer loop calls us again for the next page.  That preserves exact
2944        // hidden/GDN/KV handoff while replacing ~40 host-routed expert waits
2945        // with a few page-boundary syncs.  Never use it for a fused-head NLL
2946        // request: that contract explicitly requires a single whole graph.
2947        let pager_cap = if !has_moe {
2948            None
2949        } else if has_flint_moe {
2950            // Do not let generic `CMF_METAL_MOE_PAGER` turn the experimental
2951            // full-bank Flint graph on. Its direct selected-view pager stays
2952            // independently automatic; this path needs its own opt-in.
2953            crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2954        } else {
2955            crate::gpu_metal::moe_pager_cap_bytes()
2956        };
2957        let pager_eligible =
2958            pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2959        let mut pager_span: Option<(usize, usize)> = None;
2960        if pager_eligible {
2961            let cap = pager_cap.expect("checked above");
2962            // When this page reaches the true model end, include the fused
2963            // lm_head too.  Otherwise `lm_head_ok` would correctly refuse a
2964            // binding outside the page after the layers had already run.
2965            let tail_span = (self.graph_want_logits
2966                && upto.is_none()
2967                && scan == self.num_layers
2968                && std::env::var("CMF_GPU_LMHEAD")
2969                    .map(|v| v != "0")
2970                    .unwrap_or(true))
2971            .then(|| self.weights.lm_head.metal_graph_parts())
2972            .flatten()
2973            .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2974            let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2975                let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2976                Some(match span {
2977                    Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2978                    None => (lo, hi),
2979                })
2980            };
2981            let page_span = |span: (usize, usize), include_tail: bool| {
2982                let (mut lo, mut hi) = span;
2983                if include_tail {
2984                    if let Some((tail_lo, tail_hi)) = tail_span {
2985                        lo = lo.min(tail_lo);
2986                        hi = hi.max(tail_hi);
2987                    }
2988                }
2989                crate::gpu_metal::page_round_span(lo, hi)
2990            };
2991            let mut selected = 0usize;
2992            let mut selected_span: Option<(usize, usize)> = None;
2993            // `Item::Gdn` is built as a maximal run so the normal resident
2994            // graph can batch its recurrent state work.  A physical CMF can
2995            // nevertheless place that whole run across a page cap while each
2996            // logical layer fits.  Split *only the final admitted run* at a
2997            // layer boundary: the next outer-loop call rebuilds the remaining
2998            // suffix, preserving hidden/state handoff and keeping the normal
2999            // long-run batching within every page.
3000            let mut gdn_prefix: Option<(usize, usize)> = None;
3001            'items: for (i, item) in plan.iter().enumerate() {
3002                match item {
3003                    // Short-conv runs are not paged (no oversized
3004                    // short-conv MoE has been measured): the page ends.
3005                    Item::Sconv { .. } => break,
3006                    Item::Attn { li, .. } => {
3007                        let Some(span) = add_layer_span(selected_span, *li) else {
3008                            break;
3009                        };
3010                        let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
3011                            break;
3012                        };
3013                        if page_hi - page_lo > cap {
3014                            break;
3015                        }
3016                        selected = i + 1;
3017                        selected_span = Some((page_lo, page_hi));
3018                    }
3019                    Item::Gdn { first, run } => {
3020                        // Preserve a complete recurrent run whenever it can
3021                        // occupy a page on its own.  If the preceding items
3022                        // merely used the remaining cap, close that page and
3023                        // let the next q1 invocation retain the full run.
3024                        // Prefix-splitting is only for a run that genuinely
3025                        // cannot fit an empty page.
3026                        let mut whole = None;
3027                        for take in 1..=run.len() {
3028                            whole = add_layer_span(whole, *first + take - 1);
3029                            if whole.is_none() {
3030                                break;
3031                            }
3032                        }
3033                        let whole_fits_empty_page = whole
3034                            .and_then(|span| page_span(span, false))
3035                            .is_some_and(|(lo, hi)| hi - lo <= cap);
3036                        if whole_fits_empty_page {
3037                            let mut span = selected_span;
3038                            for take in 1..=run.len() {
3039                                let Some(next) = add_layer_span(span, *first + take - 1) else {
3040                                    break 'items;
3041                                };
3042                                span = Some(next);
3043                            }
3044                            let Some(span) = span else {
3045                                break;
3046                            };
3047                            let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
3048                            else {
3049                                break;
3050                            };
3051                            if page_hi - page_lo > cap {
3052                                break;
3053                            }
3054                            selected = i + 1;
3055                            selected_span = Some((page_lo, page_hi));
3056                            continue;
3057                        }
3058                        if selected != 0 {
3059                            break;
3060                        }
3061                        let mut span = None;
3062                        let mut best: Option<(usize, (usize, usize))> = None;
3063                        for take in 1..=run.len() {
3064                            let Some(next) = add_layer_span(span, *first + take - 1) else {
3065                                break;
3066                            };
3067                            // A partial final run cannot encode the lm_head;
3068                            // only its complete form above may include it.
3069                            let Some((page_lo, page_hi)) = page_span(next, false) else {
3070                                break;
3071                            };
3072                            if page_hi - page_lo > cap {
3073                                break;
3074                            }
3075                            span = Some((page_lo, page_hi));
3076                            best = Some((take, (page_lo, page_hi)));
3077                        }
3078                        let Some((take, page)) = best else {
3079                            break;
3080                        };
3081                        selected = i + 1;
3082                        selected_span = Some(page);
3083                        if take != run.len() {
3084                            gdn_prefix = Some((i, take));
3085                            break 'items;
3086                        }
3087                    }
3088                }
3089            }
3090            if selected != 0 {
3091                if let Some((item, take)) = gdn_prefix {
3092                    match &mut plan[item] {
3093                        Item::Gdn { run, .. } => run.truncate(take),
3094                        Item::Sconv { .. } | Item::Attn { .. } => {
3095                            unreachable!("only a GDN run is splittable")
3096                        }
3097                    }
3098                }
3099                let (page_lo, page_hi) = selected_span.expect("selected page span");
3100                tracing::info!(
3101                    page_start = page_lo,
3102                    page_end = page_hi,
3103                    page_mb = (page_hi - page_lo) / (1024 * 1024),
3104                    cap_mb = cap / (1024 * 1024),
3105                    graph_items = selected,
3106                    "native Metal MoE graph uses transient bounded weight page"
3107                );
3108                plan.truncate(selected);
3109                pager_span = Some((page_lo, page_hi));
3110            } else {
3111                tracing::warn!(
3112                    cap_mb = cap / (1024 * 1024),
3113                    "native Metal MoE pager declined: one graph item does not fit its safe page cap"
3114                );
3115            }
3116        }
3117        // A native MoE graph binds every WeightArena window to its jobs
3118        // kernels. On UMA, an mmap that already exceeds the driver's
3119        // recommended working set will therefore fail after admission, when
3120        // some recurrent/KV scratch has already been touched. Decline before
3121        // allocating/encoding the graph instead; the ordinary per-operation
3122        // route remains available and only materializes the needed window.
3123        // Do not apply this to dense graphs: they bind one projection at a
3124        // time and do not have the MoE jobs' all-window lifetime.
3125        if has_moe && needs_bounded_pager && pager_span.is_none() {
3126            tracing::info!(
3127                model_bytes,
3128                metal_working_set,
3129                "native Metal MoE graph declined before allocation"
3130            );
3131            self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
3132            if self.graph_head_required {
3133                self.fail_metal_graph(MOE_WORKING_SET_REASON);
3134            }
3135            return start;
3136        }
3137        let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
3138        let dev_attend = attend_contract
3139            && (self.head_dim <= 128
3140                || has_moe
3141                // A GDN hybrid attends on a quarter of its layers: the
3142                // hd>128 caution was measured on pure-dense models where
3143                // gqa_attend dominates, and on Qwen3.8-27B (hd 256, 48
3144                // GDN + 16 attn) the sandwich costs 2x the whole decode
3145                // (1.2 vs 2.21 tok/s measured before the arena fix).
3146                || (self.head_dim <= 256 && has_gdn)
3147                // Sliding-window layers bound most attends by the window:
3148                // Spark-X2.5-1.7B q4tp on the M4 decodes 67 tok/s
3149                // device-attended against 23 sandwiched (28 syncs/token).
3150                || (self.head_dim <= 256 && swa_graph)
3151                || attend_mode == "force"
3152                || attend_mode == "256");
3153        if !dev_attend {
3154            for it in &mut plan {
3155                if let Item::Attn { li, full_gpu, .. } = it {
3156                    // The hd>128 policy is about gqa_attend; an o1 layer
3157                    // attends through its own kernel set.
3158                    let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
3159                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
3160                    if !keep_o1 {
3161                        *full_gpu = false;
3162                    }
3163                }
3164            }
3165        }
3166        if std::env::var("CMF_GRAPH_DBG").is_ok() {
3167            use std::sync::atomic::{AtomicBool, Ordering};
3168            static SAID: AtomicBool = AtomicBool::new(false);
3169            if !SAID.swap(true, Ordering::Relaxed) {
3170                let fg = plan
3171                    .iter()
3172                    .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
3173                    .count();
3174                let att = plan
3175                    .iter()
3176                    .filter(|it| matches!(it, Item::Attn { .. }))
3177                    .count();
3178                eprintln!(
3179                    "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
3180                    plan.len(),
3181                    self.head_dim,
3182                    self.rotary_dim,
3183                    self.num_kv_heads,
3184                    self.num_heads,
3185                );
3186            }
3187        }
3188        let dims = GraphDims {
3189            hidden: self.hidden_size,
3190            eps: self.rms_eps as f32,
3191            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3192        };
3193        // The outer `forward_layers_span` scope is one logical token.  This
3194        // guard starts at q1_graph_gpu entry and ends after all page readback,
3195        // so a multi-page token cannot be reported as several whole tokens.
3196        let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
3197            crate::gpu_metal::MoePagerPageScope::new(
3198                _mt0,
3199                position,
3200                start,
3201                page_lo,
3202                page_hi,
3203                // `pager_span` is selected only after a cap was admitted;
3204                // keep the profiler observational if that invariant is ever
3205                // loosened rather than introducing a new inference panic.
3206                pager_cap.unwrap_or(0),
3207            )
3208        });
3209        let graph_new = match pager_span {
3210            Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3211            None => TokenGraph::new(&model, dims, h),
3212        };
3213        let Some(mut graph) = graph_new else {
3214            if self.graph_head_required {
3215                self.fail_metal_graph("native TokenGraph allocation refused");
3216            }
3217            return start;
3218        };
3219        if swa_graph && self.head_dim > 128 {
3220            // Spark-X2.5 (hd 256, 4 Q heads per KV head): the GQA-shared
3221            // split-K attend reads each K/V row once for the group instead
3222            // of once per head plus the importance re-read — at depth 1000
3223            // on the M4, 59.5 tok/s against 50.0 with the per-head kernel
3224            // up to the default 512 (every sliding layer sits at <= 512).
3225            // From the second position on and in 32-position blocks (more
3226            // threadgroups in flight for a latency-bound walk): Spark-X2.6
3227            // q4tp decode 0.86x the GPU time short and 0.88x at depth 1000,
3228            // q8_2f 0.91x / 0.92x (in-process A/B, M4).
3229            graph.set_attend_blk(1, 32);
3230        }
3231        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3232            nv: cfg.num_v_heads,
3233            nk: cfg.num_k_heads,
3234            dk: cfg.key_head_dim,
3235            dv: cfg.value_head_dim,
3236            kk: cfg.conv_kernel,
3237            hidden: self.hidden_size,
3238            inter: self.intermediate_size,
3239            c_dim: cfg.conv_dim(),
3240            eps: cfg.rms_eps as f32,
3241            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3242        });
3243        // Validate the whole plan BEFORE encoding anything: after the
3244        // first sync a refused layer would leave the token
3245        // half-executed, so truncate to the provably encodable prefix.
3246        let mut valid = 0usize;
3247        let mut end = start;
3248        crate::gpu::stageprof(1, _mt0.elapsed()); // конец планирования
3249        if std::env::var("CMF_PLAN_DUMP").is_ok() {
3250            static ONCE: std::sync::Once = std::sync::Once::new();
3251            ONCE.call_once(|| {
3252                for it in &plan {
3253                    match it {
3254                        Item::Gdn { first, run } => {
3255                            eprintln!("plan: Gdn first={first} len={}", run.len())
3256                        }
3257                        Item::Sconv { first, run } => {
3258                            eprintln!("plan: Sconv first={first} len={}", run.len())
3259                        }
3260                        Item::Attn { li, full_gpu, .. } => {
3261                            eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3262                        }
3263                    }
3264                }
3265            });
3266        }
3267        for item in &plan {
3268            let ok = match item {
3269                Item::Gdn { run, .. } => gcfg
3270                    .as_ref()
3271                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3272                    .unwrap_or(false),
3273                Item::Sconv { run, .. } => self
3274                    .short_conv_cfg
3275                    .is_some_and(|sc| run.iter().all(|l| graph.sconv_ok(l, sc.kernel))),
3276                Item::Attn { l, .. } => graph.attn_ok(l),
3277            };
3278            if !ok {
3279                if block_diag {
3280                    eprintln!(
3281                        "block-graph: plan item {} ({}) failed graph preflight",
3282                        valid,
3283                        match item {
3284                            Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3285                            Item::Sconv { run, first } => {
3286                                format!("short-conv run L{first}+{}", run.len())
3287                            }
3288                            Item::Attn { li, .. } => format!("Attn L{li}"),
3289                        }
3290                    );
3291                }
3292                break;
3293            }
3294            valid += 1;
3295            end += match item {
3296                Item::Gdn { run, .. } => run.len(),
3297                Item::Sconv { run, .. } => run.len(),
3298                Item::Attn { .. } => 1,
3299            };
3300        }
3301        plan.truncate(valid);
3302        if plan.is_empty() {
3303            if self.graph_head_required {
3304                self.fail_metal_graph("native graph preflight produced no valid items");
3305            }
3306            return start;
3307        }
3308
3309        if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3310            self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3311            return start;
3312        }
3313
3314        // Plain dense decode (every item a device-attended full-attention
3315        // layer with a dense FFN, no O(1) state): the only plan shape the
3316        // masked-nibble q4tp matvec and the concurrent layer encoder were
3317        // measured on (MiniCPM5-2B, Qwen3-0.6B on the M4). Hybrids, MoE and
3318        // o1 layers keep the historical serial path bit for bit.
3319        // Every projection must be ONE dispatch (q1t adds an overlay pass,
3320        // Prism q2tp a transform pass — dependent pairs a concurrent
3321        // encoder would race).
3322        let one_pass = |t: (usize, usize, usize)| {
3323            use cortiq_core::TensorDtype as D;
3324            matches!(
3325                model.tensors[t.0].dtype,
3326                D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3327            )
3328        };
3329        let dense_fast = plan.iter().all(|it| match it {
3330            Item::Attn {
3331                l, li, full_gpu, ..
3332            } => {
3333                *full_gpu
3334                    && self.kv_cache.layers[*li].o1.is_none()
3335                    && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3336                    && match l.ffn {
3337                        MetalFfn::Dense { gate, up, down, .. } => {
3338                            one_pass(gate) && one_pass(up) && one_pass(down)
3339                        }
3340                        _ => false,
3341                    }
3342            }
3343            // A short-conv run encodes serially in its own encoder: its
3344            // one-pass projections take the same matvec levers.
3345            Item::Sconv { run, .. } => run.iter().all(|l| {
3346                one_pass(l.in_proj)
3347                    && one_pass(l.out)
3348                    && match l.ffn {
3349                        MetalFfn::Dense { gate, up, down, .. } => {
3350                            one_pass(gate) && one_pass(up) && one_pass(down)
3351                        }
3352                        _ => false,
3353                    }
3354            }),
3355            Item::Gdn { .. } => false,
3356        });
3357        // MoE decode with every layer device-attended (Mellum2.1): the
3358        // routed experts' jobs ladder and the q8_2f attention projections
3359        // take their measured levers (`MOE_*`, `DENSE_Q8R4`).
3360        let moe_plan = !dense_fast
3361            && plan.iter().all(|it| match it {
3362                Item::Attn {
3363                    l, li, full_gpu, ..
3364                } => {
3365                    *full_gpu
3366                        && self.kv_cache.layers[*li].o1.is_none()
3367                        && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3368                }
3369                Item::Gdn { .. } | Item::Sconv { .. } => false,
3370            });
3371        let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3372        if block_diag {
3373            use std::sync::atomic::{AtomicBool, Ordering};
3374            static SAID_AB: AtomicBool = AtomicBool::new(false);
3375            if !SAID_AB.swap(true, Ordering::Relaxed) {
3376                eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3377            }
3378        }
3379        let _mv_fast = match ab {
3380            Some((bits, _)) => {
3381                graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3382                crate::gpu_metal::MvFastGuard::set_raw(bits)
3383            }
3384            None if moe_plan => {
3385                graph.set_dense_concurrent(
3386                    crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3387                );
3388                crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3389            }
3390            None => {
3391                graph.set_dense_concurrent(dense_fast);
3392                // Spark-X2.5 q8_2f: the four-row q8_2f matvec decodes the
3393                // 1.7B at 40.5 tok/s on the M4 against 27.7 one-row.
3394                let q8r4 = if swa_graph {
3395                    crate::gpu_metal::DENSE_Q8R4
3396                } else {
3397                    0
3398                };
3399                crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3400                    crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3401                } else {
3402                    0
3403                })
3404            }
3405        };
3406
3407        // RoPE table and rotary width are per layer (`layer_inv_freq`,
3408        // `layer_geom`) inside the loop below.
3409        let pool = self.pool.clone();
3410        let (nh, nkv, hd, hs, eps) = (
3411            self.num_heads,
3412            self.num_kv_heads,
3413            self.head_dim,
3414            self.hidden_size,
3415            self.rms_eps,
3416        );
3417        let norm_style = self.norm_style;
3418        let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3419        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3420        let kv_id = self.graph_kv_id;
3421        // GDN runs whose states await readback after the next sync
3422        // (device-attended layers add no sync, so several may stack).
3423        let mut pending: Vec<(usize, usize)> = Vec::new();
3424        // Device-attended layers: their K/V/imp are pulled from the
3425        // mirror after the final sync.
3426        let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3427        for item in &plan {
3428            let _xt0 = std::time::Instant::now();
3429            let _xkind: u32 = match item {
3430                Item::Gdn { .. } | Item::Sconv { .. } => 2,
3431                Item::Attn { .. } => 3,
3432            };
3433            // Looped Transformer: insert on-device norm at loop boundaries.
3434            if self.loop_final_norm {
3435                let item_start = match item {
3436                    Item::Gdn { first, .. } | Item::Sconv { first, .. } => *first,
3437                    Item::Attn { li, .. } => *li,
3438                };
3439                if item_start > start && self.is_loop_end(item_start - 1) {
3440                    graph.encode_loop_norm(&self.weights.final_norm);
3441                }
3442            }
3443            match item {
3444                Item::Gdn { run, first } => {
3445                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3446                        if l.linear_state.len() != want {
3447                            l.linear_state = vec![0f32; want];
3448                        }
3449                    }
3450                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3451                        .iter()
3452                        .map(|l| l.linear_state.as_slice())
3453                        .collect();
3454                    let _ig = std::time::Instant::now();
3455                    if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3456                        // Unreachable: the plan was validated above.
3457                        tracing::error!("q1 graph: GDN run refused after validation");
3458                        return start;
3459                    }
3460                    // Early commit: the GPU starts the run while the
3461                    // CPU encodes the next layer (nothing to wait on).
3462                    graph.commit_kind = 2;
3463                    graph.commit();
3464                    crate::gpu::stageprof(0, _ig.elapsed());
3465                    pending.push((*first, run.len()));
3466                }
3467                Item::Sconv { run, first } => {
3468                    let sc = self.short_conv_cfg.expect("validated short-conv run");
3469                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3470                        if l.linear_state.len() != sc.state_len() {
3471                            l.linear_state = vec![0f32; sc.state_len()];
3472                        }
3473                    }
3474                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3475                        .iter()
3476                        .map(|l| l.linear_state.as_slice())
3477                        .collect();
3478                    if !graph.encode_sconv_run(run, &ro, sc.kernel) {
3479                        tracing::error!("q1 graph: short-conv run refused after validation");
3480                        return start;
3481                    }
3482                    graph.commit_kind = 2;
3483                    graph.commit();
3484                    pending.push((*first, run.len()));
3485                }
3486                Item::Attn {
3487                    l,
3488                    li,
3489                    q_norm,
3490                    k_norm,
3491                    output_gate,
3492                    proj_gate,
3493                    head_gate_w,
3494                    bias,
3495                    full_gpu,
3496                } => {
3497                    let _ia = std::time::Instant::now();
3498                    // This layer's attention geometry: its window, RoPE
3499                    // table and rotary width (Spark-X2.5 interleaves
3500                    // 512-window layers rotating all dims at θ 1e4 with
3501                    // full layers rotating a quarter at θ 5e6). Every model
3502                    // without sliding layers reads the global ones here.
3503                    let inv_freq_l = self.layer_inv_freq(*li);
3504                    let rd_l = self.layer_geom(*li).2;
3505                    let window_l = self.layer_window(*li);
3506                    let head_gate_w = *head_gate_w;
3507                    // This layer's KV heads and head width: Gemma-4's global
3508                    // layers (one 512-wide KV head) differ from the base.
3509                    let (nkv, hd) = if gemma4 {
3510                        let (a, b, _) = self.layer_geom(*li);
3511                        (a, b)
3512                    } else {
3513                        (nkv, hd)
3514                    };
3515                    // ── Fully device-resident attention: no sync at all.
3516                    if *full_gpu {
3517                        let cache = &self.kv_cache.layers[*li];
3518                        let o1p = if cache.o1.is_some() {
3519                            match cache.o1_views() {
3520                                Some(views) => Some(crate::gpu::O1AttnParams {
3521                                    views,
3522                                    epoch: self.o1_epoch,
3523                                }),
3524                                // Sealed state gone mid-run: sandwich.
3525                                None => None,
3526                            }
3527                        } else {
3528                            None
3529                        };
3530                        let o1_layer = cache.o1.is_some();
3531                        if o1_layer && o1p.is_none() {
3532                            // fall to the sandwich (CPU o1 step)
3533                        }
3534                        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3535                        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3536                        let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3537                        // A trimmed tail always holds the window the
3538                        // device attend reads (`first = rows + 1 − w`).
3539                        debug_assert!(
3540                            cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3541                        );
3542                        let p = crate::gpu::AttnDeviceParams {
3543                            kv_id,
3544                            layer: *li,
3545                            nh,
3546                            nkv,
3547                            hd,
3548                            rd: rd_l,
3549                            position,
3550                            scale: self.attn_scale,
3551                            eps: eps as f32,
3552                            gemma,
3553                            late_qk_norm: self.qk_norm_after_rope,
3554                            output_gate: *output_gate,
3555                            q_norm: *q_norm,
3556                            k_norm: *k_norm,
3557                            inv_freq: &inv_freq_l,
3558                            rope_scale: self.layer_rope_scale(*li),
3559                            cpu_k,
3560                            cpu_v,
3561                            cpu_stored,
3562                            cpu_gen: cache.generation(),
3563                            o1: o1p,
3564                            window: window_l,
3565                            softmax_sink: cache.sinks.as_deref(),
3566                            head_gate: head_gate_w,
3567                            v_norm: self.attn_v_norm,
3568                        };
3569                        let o1_bad = o1_layer && p.o1.is_none();
3570                        if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3571                        {
3572                            // o1 layers leave no mirror row to pull.
3573                            if p.o1.is_none() {
3574                                dev_attn.push((*li, nkv, hd));
3575                            }
3576                            graph.commit_kind = 3;
3577                            graph.commit();
3578                            // The footer below is skipped by `continue`:
3579                            // account the device-attn item here or its
3580                            // cost hides from the stage profile entirely.
3581                            crate::gpu::stageprof(_xkind, _xt0.elapsed());
3582                            continue;
3583                        }
3584                        // Mirror refused (nothing encoded) → sandwich.
3585                    }
3586                    graph.encode_attn_prefix(l);
3587                    if let Err(err) = graph.sync_checked() {
3588                        self.fail_metal_graph(&err);
3589                        return start;
3590                    }
3591                    if !pending.is_empty() {
3592                        let idxs: Vec<usize> =
3593                            pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3594                        let mut outs: Vec<&mut [f32]> = self
3595                            .kv_cache
3596                            .layers
3597                            .iter_mut()
3598                            .enumerate()
3599                            .filter(|(i, _)| idxs.binary_search(i).is_ok())
3600                            .map(|(_, s)| s.linear_state.as_mut_slice())
3601                            .collect();
3602                        graph.read_states(&mut outs);
3603                    }
3604                    let mut q_raw = attention::take_buf(l.wq.1);
3605                    let mut k = attention::take_buf(l.wk.1);
3606                    let mut v = attention::take_buf(l.wv.1);
3607                    graph.read_qkv(&mut q_raw, &mut k, &mut v);
3608                    // Projected output gate: g = G·norm(h) on the host, off
3609                    // the normed input the prefix left on the device.
3610                    let mut gate_raw = proj_gate.map(|(gp, _)| {
3611                        let mut normed = attention::take_buf(hs);
3612                        graph.read_normed(&mut normed);
3613                        let mut raw = attention::take_buf(gp.rows());
3614                        gp.matvec(&normed, &mut raw, pool.as_deref());
3615                        attention::recycle_buf(&mut normed);
3616                        raw
3617                    });
3618                    let cfg = QwenAttnCfg {
3619                        mrope: None,
3620                        num_heads: nh,
3621                        num_kv_heads: nkv,
3622                        head_dim: hd,
3623                        hidden_size: hs,
3624                        position,
3625                        inv_freq: &inv_freq_l,
3626                        rotary_dim: rd_l,
3627                        scale: self.attn_scale,
3628                        softcap: self.attn_softcap,
3629                        window: window_l,
3630                        // Gemma-4 only (the front gate refuses V norm
3631                        // otherwise).
3632                        v_norm: self.attn_v_norm,
3633                        qk_norm_after_rope: self.qk_norm_after_rope,
3634                        gate_sigmoid: self.proj_gate_sigmoid,
3635                        q_norm: *q_norm,
3636                        k_norm: *k_norm,
3637                        output_gate: *output_gate,
3638                        softplus_gate: None,
3639                        rope_scale: self.layer_rope_scale(*li),
3640                        bias: *bias,
3641                        rms_eps: eps,
3642                        norm_style,
3643                        pool: pool.as_deref(),
3644                        v_head_dim: hd,
3645                    };
3646                    // CMF_ATTN_ORACLE=1: diff the device attend against
3647                    // this CPU attend on identical inputs (bring-up).
3648                    let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3649                        || std::env::var("CMF_ATTN_DUMP").is_ok();
3650                    let _ = full_gpu;
3651                    let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3652                    let mut ao = attention::qwen_attention_core(
3653                        q_raw,
3654                        k,
3655                        v,
3656                        &mut self.kv_cache.layers[*li],
3657                        &cfg,
3658                    );
3659                    // CMF_ATTN_DUMP=<dir>: this token's rope'd Q and the layer's whole
3660                    // K/V cache as raw f32 (offline attention-statistics probes:
3661                    // block bounds, mass concentration). Needs CMF_GPU_ATTEND=0.
3662                    if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3663                        if let Some((qr0, k0, v0)) = oracle_in.clone() {
3664                            let (cq, _cg, _ck, _cv) =
3665                                attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3666                            let cache = &self.kv_cache.layers[*li];
3667                            let n = cache.head_keys(0).len() / hd;
3668                            let mut bytes: Vec<u8> = Vec::new();
3669                            for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3670                                bytes.extend_from_slice(&v.to_le_bytes());
3671                            }
3672                            for v in &cq {
3673                                bytes.extend_from_slice(&v.to_le_bytes());
3674                            }
3675                            for g in 0..nkv {
3676                                for v in cache.head_keys(g) {
3677                                    bytes.extend_from_slice(&v.to_le_bytes());
3678                                }
3679                            }
3680                            for g in 0..nkv {
3681                                for v in cache.head_values(g) {
3682                                    bytes.extend_from_slice(&v.to_le_bytes());
3683                                }
3684                            }
3685                            let _ =
3686                                std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3687                        }
3688                    }
3689                    if let Some((qr0, k0, v0)) =
3690                        oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3691                    {
3692                        let (cq, _cg, ck, cv) =
3693                            attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3694                        let mut h_now = vec![0f32; hs];
3695                        graph.read_h(&mut h_now);
3696                        let cache = &self.kv_cache.layers[*li];
3697                        let n_after = cache.head_keys(0).len() / hd;
3698                        // A sealed O(1) cache may have no dense current-row
3699                        // entry. The oracle is a debug probe, so let it see
3700                        // zero stored exact rows instead of underflowing.
3701                        let stored = n_after.saturating_sub(1);
3702                        let cpu_k: Vec<&[f32]> = (0..nkv)
3703                            .map(|g| &cache.head_keys(g)[..stored * hd])
3704                            .collect();
3705                        let cpu_v: Vec<&[f32]> = (0..nkv)
3706                            .map(|g| &cache.head_values(g)[..stored * hd])
3707                            .collect();
3708                        let p = crate::gpu::AttnDeviceParams {
3709                            kv_id,
3710                            layer: *li,
3711                            nh,
3712                            nkv,
3713                            hd,
3714                            // The layer's own geometry, as the CPU attend
3715                            // above used it (the projected gate is applied
3716                            // after this probe on both sides).
3717                            rd: rd_l,
3718                            position,
3719                            scale: self.attn_scale,
3720                            eps: eps as f32,
3721                            gemma,
3722                            late_qk_norm: self.qk_norm_after_rope,
3723                            output_gate: *output_gate,
3724                            q_norm: *q_norm,
3725                            k_norm: *k_norm,
3726                            inv_freq: &inv_freq_l,
3727                            rope_scale: self.layer_rope_scale(*li),
3728                            cpu_k,
3729                            cpu_v,
3730                            cpu_stored: stored,
3731                            cpu_gen: cache.generation(),
3732                            o1: None,
3733                            window: window_l,
3734                            softmax_sink: cache.sinks.as_deref(),
3735                            head_gate: None,
3736                            v_norm: self.attn_v_norm,
3737                        };
3738                        if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3739                            let md = |a: &[f32], b: &[f32]| {
3740                                a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3741                            };
3742                            let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3743                            eprintln!(
3744                                "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}",
3745                                nn(&cq),
3746                                md(&cq, &dq),
3747                                nn(&ck),
3748                                md(&ck, &dk),
3749                                nn(&cv),
3750                                md(&cv, &dv),
3751                                nn(&ao),
3752                                md(&ao, &dao)
3753                            );
3754                        } else {
3755                            eprintln!("attn-oracle L{li}: device probe declined");
3756                        }
3757                    }
3758                    if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3759                        // V is as wide as the head (the front gate), so ao
3760                        // is nh·hd here, as in `qwen_attention`.
3761                        attention::apply_projected_gate(
3762                            &mut ao,
3763                            raw,
3764                            per_head,
3765                            hd,
3766                            self.proj_gate_sigmoid,
3767                        );
3768                    }
3769                    if let Some(mut raw) = gate_raw.take() {
3770                        attention::recycle_buf(&mut raw);
3771                    }
3772                    graph.encode_attn_suffix(l, &ao);
3773                    // Early commit: the GPU starts O+FFN while the CPU
3774                    // encodes the following GDN run / attention prefix.
3775                    graph.commit();
3776                    attention::recycle_buf(&mut ao);
3777                }
3778            }
3779
3780            crate::gpu::stageprof(_xkind, _xt0.elapsed());
3781        }
3782        // Ride the final norm + lm_head in the same command buffer when
3783        // this run reaches the model's end and the caller wants logits:
3784        // the separate per-op lm_head submit (a full round trip) folds
3785        // into the sync that already happens here.
3786        let mut lm_rows = None;
3787        if self.graph_want_logits
3788            && upto.is_none()
3789            && end == self.num_layers
3790            && std::env::var("CMF_GPU_LMHEAD")
3791                .map(|v| v != "0")
3792                .unwrap_or(true)
3793        {
3794            if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3795                if graph.lm_head_ok(lm) {
3796                    graph.encode_lm_head(&self.weights.final_norm, lm);
3797                    lm_rows = Some(lm.1);
3798                }
3799            }
3800        }
3801        if self.graph_head_required && lm_rows.is_none() {
3802            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3803            self.fail_metal_graph("fused graph head was requested but not encodable");
3804            return start;
3805        }
3806        let _sy0 = std::time::Instant::now();
3807        if let Err(err) = graph.sync_checked() {
3808            if let Some(profile) = pager_profile_page.as_mut() {
3809                profile.finish(start, graph.pager_page_stats(), "sync-error");
3810            }
3811            self.fail_metal_graph(&err);
3812            return start;
3813        }
3814        let _rs0 = std::time::Instant::now();
3815        if !pending.is_empty() {
3816            let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3817            let mut outs: Vec<&mut [f32]> = self
3818                .kv_cache
3819                .layers
3820                .iter_mut()
3821                .enumerate()
3822                .filter(|(i, _)| idxs.binary_search(i).is_ok())
3823                .map(|(_, s)| s.linear_state.as_mut_slice())
3824                .collect();
3825            graph.read_states(&mut outs);
3826        }
3827        if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3828            use std::sync::atomic::{AtomicU64, Ordering};
3829            static SY: AtomicU64 = AtomicU64::new(0);
3830            static RS: AtomicU64 = AtomicU64::new(0);
3831            static N: AtomicU64 = AtomicU64::new(0);
3832            SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3833            RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3834            let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3835            if n % 100 == 0 {
3836                eprintln!(
3837                    "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3838                    SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3839                    RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3840                );
3841            }
3842        }
3843        if let Some(rows) = lm_rows {
3844            crate::gpu::hostprof_encode_done(_mt0);
3845            let mut lg = attention::take_buf(rows.min(self.vocab_size));
3846            graph.read_logits(&mut lg);
3847            crate::gpu::hostprof_total(_mt0);
3848            lg.resize(self.vocab_size, 0.0);
3849            if let Some(c) = self.final_softcap {
3850                softcap_in_place(&mut lg, c, self.pool.as_deref());
3851            }
3852            self.graph_logits = Some(lg);
3853        }
3854        graph.read_h(h);
3855        if self.graph_head_required && self.graph_logits.is_none() {
3856            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3857            self.fail_metal_graph("fused graph head completed without logits readback");
3858            return start;
3859        }
3860        METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3861        METAL_GRAPH_LAYERS.fetch_add(
3862            end.saturating_sub(start) as u64,
3863            std::sync::atomic::Ordering::Relaxed,
3864        );
3865        if self.graph_head_required {
3866            METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3867        }
3868        // Device-attended layers: replay the CPU bookkeeping — append
3869        // the mirror's new K/V row (rope'd on the GPU) into the owner
3870        // cache, then bank this token's attention-importance mass.
3871        for (li, nkv, hd) in dev_attn {
3872            let mut krow = attention::take_buf(nkv * hd);
3873            let mut vrow = attention::take_buf(nkv * hd);
3874            if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3875                let cache = &mut self.kv_cache.layers[li];
3876                cache.append(&krow, &vrow, &[]);
3877                let n = cache.seq_len;
3878                let mut imp = attention::take_buf(n);
3879                crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3880                cache.accumulate_imp(&imp);
3881                attention::recycle_buf(&mut imp);
3882            }
3883            attention::recycle_buf(&mut krow);
3884            attention::recycle_buf(&mut vrow);
3885        }
3886        if let Some(profile) = pager_profile_page.as_mut() {
3887            profile.finish(end, graph.pager_page_stats(), "page-complete");
3888        }
3889        if let Some((_, arm)) = ab {
3890            crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3891        }
3892        end
3893    }
3894
3895    pub fn new(
3896        tokenizer: Tokenizer,
3897        weights: PipelineWeights,
3898        hidden_size: usize,
3899        intermediate_size: usize,
3900        num_heads: usize,
3901        num_kv_heads: usize,
3902        head_dim: usize,
3903        num_layers: usize,
3904        physical_layers: usize,
3905        loop_final_norm: bool,
3906        vocab_size: usize,
3907        rms_eps: f64,
3908        rope_base: f32,
3909        norm_style: NormStyle,
3910        max_seq_len: usize,
3911        sampler_config: SamplerConfig,
3912    ) -> Self {
3913        let rng = match sampler_config.seed {
3914            Some(s) => SplitMix64::new(s),
3915            None => SplitMix64::from_entropy(),
3916        };
3917        let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3918        let pool = Pool::from_env();
3919        if let Some(p) = &pool {
3920            tracing::info!("worker pool: {} threads", p.n_workers());
3921            // Keep the workers on the socket that holds the weights.
3922            if let Some(model) = weights
3923                .lm_head
3924                .model_arc()
3925                .or_else(|| weights.embed_tokens.model_arc())
3926            {
3927                let regions: Vec<&[u8]> =
3928                    model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3929                p.bind_numa(&regions);
3930            }
3931        }
3932        Self {
3933            gpu_plan: None,
3934            tokenizer: std::sync::Arc::new(tokenizer),
3935            kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3936            sampler_config,
3937            weights,
3938            hidden_size,
3939            intermediate_size,
3940            num_heads,
3941            num_kv_heads,
3942            head_dim,
3943            num_layers,
3944            physical_layers,
3945            loop_final_norm,
3946            layer_map: None,
3947            mhc: None,
3948            mhc_r: Vec::new(),
3949            mrope_sections: None,
3950            prompt_mrope: None,
3951            vocab_size,
3952            rms_eps,
3953            rope_base,
3954            norm_style,
3955            rotary_dim: head_dim,
3956            attention_heads_per_layer: None,
3957            kv_heads_per_layer: None,
3958            v_head_dim: None,
3959            layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3960                .filter(|v| !v.is_empty())
3961                .map(std::path::PathBuf::from),
3962            graph_declines: std::cell::RefCell::new(Vec::new()),
3963            mimo_moe: Default::default(),
3964            minicpm_tail: Default::default(),
3965            vmf_cfg: None,
3966            gdn_cfg: None,
3967            kda_cfg: None,
3968            g3n: None,
3969            g4e: None,
3970            dsv4: None,
3971            dsv41: None,
3972            dsv41_vision: None,
3973            dsv41_prefill: None,
3974            qwen4_exp: None,
3975            clover: None,
3976            clover_stats: clover::CloverStats::default(),
3977            dsv4_mtp: Vec::new(),
3978            dspark: None,
3979            dspark_pending: Vec::new(),
3980            dspark_hist: Vec::new(),
3981            dspark_real: Vec::new(),
3982            dspark_trunk_picks: Vec::new(),
3983            dspark_exp: Vec::new(),
3984            dspark_draft_ns: 0,
3985            m3_winners: Vec::new(),
3986            m3_counts: Vec::new(),
3987            m3_n_exp: 0,
3988            m3_tokens: 0,
3989            m3_prewarmed: false,
3990            logit_multiplier: None,
3991            cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3992            graph_failed: std::sync::atomic::AtomicBool::new(false),
3993            graph_moe_banks: std::sync::Mutex::new(std::collections::HashMap::new()),
3994            kv_history: Vec::new(),
3995            kv_history_device: false,
3996            short_conv_cfg: None,
3997            mtp: None,
3998            mimo_mtp: None,
3999            verify_exact_moe: false,
4000            speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
4001            ignore_eos: false,
4002            draft_full_streak: 0,
4003            spec_k_adapt: None,
4004            spec_acc_ewma: 0.7,
4005            rng,
4006            sampler_scratch: SamplerScratch::default(),
4007            spec_forced: None,
4008            spec_q: Vec::new(),
4009            spec_p: Vec::new(),
4010            spec_res: Vec::new(),
4011            spec_qs: Vec::new(),
4012            spec_ps: Vec::new(),
4013            spec_ress: Vec::new(),
4014            mtp_graph_mode: None,
4015            #[cfg(target_os = "macos")]
4016            metal_verify: None,
4017            inv_freq,
4018            ws: ForwardScratch::new(hidden_size),
4019            pool,
4020            model: None,
4021            dyn_force_f32: false,
4022            dyn_skill_layers: Vec::new(),
4023            dyn_active: None,
4024            dyn_blend_loaded: false,
4025            dyn_phi_layer: None,
4026            dyn_phi_ema: Vec::new(),
4027            dyn_phi_seen: 0,
4028            dyn_router: None,
4029            o1_cfg: None,
4030            o1_epoch: 0,
4031            o1_flags: Vec::new(),
4032            trace: false,
4033            calib_temp: 1.0,
4034            confidence_on: true,
4035            embed_multiplier: 1.0,
4036            attn_scale: 1.0 / (head_dim as f32).sqrt(),
4037            swa: None,
4038            swa_trim: None,
4039            swa_trim_anchor: usize::MAX,
4040            chunk_min_rows: 32,
4041            sliding_layers: None,
4042            no_rope_layers: None,
4043            anchor_core: None,
4044            bounded_rope: None,
4045            kv_prefix: KvPrefix::default(),
4046            last_prefill_tokens: 0,
4047            inv_freq_local: None,
4048            rotary_dim_local: None,
4049            rope_scale: 1.0,
4050            rope_scale_local: 1.0,
4051            global_attn: None,
4052            inv_freq_global: None,
4053            attn_v_norm: false,
4054            qk_norm_after_rope: false,
4055            proj_gate_sigmoid: false,
4056            final_softcap: None,
4057            head_clusters: None,
4058            attn_softcap: 0.0,
4059            graph_want_logits: false,
4060            graph_head_required: false,
4061            graph_logits: None,
4062            embryo_graph: None,
4063            graph_refused: std::sync::atomic::AtomicBool::new(false),
4064            graph_kv_id: {
4065                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
4066                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
4067            },
4068            #[cfg(test)]
4069            nll_test_fail_at: None,
4070            #[cfg(test)]
4071            nll_test_force_serial: false,
4072        }
4073    }
4074
4075    /// Enable/disable per-layer O(1) Nyström attention. Only Full
4076    /// layers are eligible (a linear layer keeps its own operator).
4077    /// Applies to generation (`generate*`/`forward_ids`): the prompt
4078    /// pass stays exact, then the state seals after prefill or at the
4079    /// deferred skeleton-safe boundary for short prompts; decode runs on
4080    /// the O(1) state. Teacher-forced scoring (`ppl_ids`) intentionally
4081    /// stays exact.
4082    pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
4083        if let Err(e) = self.try_set_o1(cfg) {
4084            tracing::error!("{e}");
4085        }
4086    }
4087
4088    /// True when the file carries a native bounded anchor
4089    /// (`arch.anchor_core`): its state is a fixed record the header
4090    /// fixes, and the post-hoc O(1) override is meaningless on it.
4091    pub fn bounded_native(&self) -> bool {
4092        self.anchor_core.is_some()
4093    }
4094
4095    /// Bytes the resident device graph holds for this pipeline's sequence:
4096    /// `(recurrent state, anchor KV/ring)`; None on the host path.
4097    pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
4098        crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
4099    }
4100
4101    /// Why an O(1) override is refused on this pipeline, if it is.
4102    pub fn o1_refusal(&self) -> Option<String> {
4103        self.anchor_core.as_ref().map(|ac| {
4104            format!(
4105                "--o1 / CMF_O1 refused: the anchor is native bounded \
4106                 (anchor_core kind={} window={} sink={}); the file's operator \
4107                 is executed as-is and no post-hoc Nyström overlay applies",
4108                ac.kind, ac.window, ac.sink
4109            )
4110        })
4111    }
4112
4113    /// `set_o1` that reports the refusal instead of logging it.
4114    pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
4115        if let Some(c) = &cfg {
4116            if let Some(why) = self.o1_refusal() {
4117                self.o1_flags = Vec::new();
4118                self.o1_cfg = None;
4119                return Err(why);
4120            }
4121            if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
4122                self.o1_flags.clear();
4123                self.o1_cfg = None;
4124                return Err(format!(
4125                    "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
4126                    c.w, c.sink
4127                ));
4128            }
4129        }
4130        self.o1_flags = match &cfg {
4131            Some(c) => {
4132                let mut flags = c.layer_flags(self.num_layers);
4133                for (li, f) in flags.iter_mut().enumerate() {
4134                    // The Nyström state replaces a full-context plain
4135                    // softmax: a sliding window or a learned sink is not
4136                    // something it can represent, and a V narrower than
4137                    // the head is not what its streaming state stores.
4138                    // Those layers keep exact cache attention.
4139                    if *f
4140                        && (!matches!(
4141                            self.weights.layers[self.phys_layer(li)].attn,
4142                            AttnKind::Full { .. }
4143                        ) || self.layer_window(li).is_some()
4144                            || self.kv_cache.layers[li].sinks.is_some()
4145                            || self.layer_v_dim(li) != self.layer_geom(li).1)
4146                    {
4147                        *f = false;
4148                    }
4149                }
4150                flags
4151            }
4152            None => Vec::new(),
4153        };
4154        if let Some(c) = &cfg {
4155            let n = self.o1_flags.iter().filter(|&&f| f).count();
4156            tracing::info!(
4157                "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
4158                self.num_layers,
4159                c.m,
4160                c.w,
4161                c.sink,
4162                c.rect
4163            );
4164        }
4165        self.o1_cfg = cfg;
4166        Ok(())
4167    }
4168
4169    /// Install the file's bounded anchor: one fixed-size ring per
4170    /// `AttnKind::Bounded` layer (from the header, not per prompt) and
4171    /// the shared relative-rotation table. Must run after the RoPE setup
4172    /// (`set_rotary`, YaRN) so the table is built from the final
4173    /// `inv_freq`.
4174    pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
4175        if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
4176            return Err(format!(
4177                "anchor_core kind '{}' is not executable by this runtime",
4178                cfg.kind
4179            ));
4180        }
4181        if cfg.window == 0 {
4182            return Err("anchor_core.window must be >= 1".into());
4183        }
4184        let mut n = 0usize;
4185        for li in 0..self.num_layers {
4186            let pl = self.phys_layer(li);
4187            if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
4188                if w.window != cfg.window
4189                    || w.sink != cfg.sink
4190                    || w.group_windows != cfg.group_windows
4191                {
4192                    return Err(format!(
4193                        "layer {li}: bounded weights (window {} sink {} groups {:?}) disagree \
4194                         with anchor_core (window {} sink {} groups {:?})",
4195                        w.window, w.sink, w.group_windows, cfg.window, cfg.sink, cfg.group_windows
4196                    ));
4197                }
4198                self.kv_cache.layers[li].install_bounded(cfg.window);
4199                n += 1;
4200            }
4201        }
4202        if n == 0 {
4203            return Err("anchor_core is present but no layer executes it".into());
4204        }
4205        let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
4206        self.bounded_rope = Some(std::sync::Arc::new(rope));
4207        self.anchor_core = Some(cfg.clone());
4208        self.embryo_graph = None;
4209        tracing::info!(
4210            "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
4211            cfg.kind,
4212            cfg.window,
4213            cfg.sink,
4214            self.kv_cache
4215                .layers
4216                .iter()
4217                .map(|l| l.bounded_state_bytes())
4218                .max()
4219                .unwrap_or(0)
4220        );
4221        Ok(())
4222    }
4223
4224    /// Tag every layer's cache with its wire record kind and the model's
4225    /// operator identity (hash64 of `linear_core_identity` JSON) so the
4226    /// versioned state wire refuses a peer holding another operator.
4227    pub fn install_wire_identity(&mut self, identity: u64) {
4228        for li in 0..self.kv_cache.layers.len() {
4229            let pl = self.phys_layer(li);
4230            let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
4231                Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
4232                Some(AttnKind::Linear(_))
4233                | Some(AttnKind::LinearGdn(_))
4234                | Some(AttnKind::ShortConv(_))
4235                | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
4236                _ => crate::kv_cache::WireKind::Full,
4237            };
4238            let l = &mut self.kv_cache.layers[li];
4239            l.wire_kind = kind;
4240            l.wire_identity = identity;
4241            // A per-layer geometry (`set_attn_geometry`, Gemma's global
4242            // heads) rebuilds a layer cache with index 0: re-tag it.
4243            l.wire_layer = li as u32;
4244        }
4245    }
4246
4247    /// Forget the reuse keys (legacy `kv_history` and the bounded prefix).
4248    pub fn clear_history(&mut self) {
4249        self.kv_history.clear();
4250        self.kv_history_device = false;
4251        self.kv_prefix.clear();
4252    }
4253
4254    /// Did THIS pipeline's token graph refuse for a structural reason?
4255    /// (Per pipeline: another lane's refusal, or a new pipeline of the
4256    /// same model, never changes it.)
4257    pub fn graph_refused(&self) -> bool {
4258        self.graph_refused
4259            .load(std::sync::atomic::Ordering::Relaxed)
4260    }
4261
4262    /// Remember a structural refusal of this pipeline's token graph.
4263    pub fn mark_graph_refused(&self) {
4264        if !self
4265            .graph_refused
4266            .swap(true, std::sync::atomic::Ordering::Relaxed)
4267        {
4268            tracing::info!(
4269                "token graph: unsupported for this pipeline (seq {}) — not retrying",
4270                self.graph_kv_id
4271            );
4272        }
4273    }
4274
4275    /// Position the resident Embryo graph holds for this pipeline's
4276    /// sequence (`Some(next position)`), `None` when the device holds no
4277    /// image of it (host-owned sequence, or none started).
4278    pub fn device_sequence_position(&self) -> Option<usize> {
4279        crate::gpu::embryo_device_next_position(self.graph_kv_id)
4280    }
4281
4282    /// Is the sequence this pipeline's reuse key describes owned by the
4283    /// device path it would take now? A prefix recorded on the resident
4284    /// graph continues only there (at exactly `n`), a host prefix only on
4285    /// the host; any mismatch means the next turn re-prefills from zero.
4286    fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4287        let dev = self.device_sequence_position();
4288        if recorded_on_device {
4289            dev == Some(n) && self.embryo_resident_wanted()
4290        } else {
4291            dev.is_none()
4292        }
4293    }
4294
4295    /// The weights under the sequence changed (a real skill switch): every
4296    /// cached state was computed by other weights. Clear the host KV /
4297    /// ring / recurrent state AND the reuse keys — a surviving `kv_prefix`
4298    /// would let the next turn "extend" a prefix the new weights never
4299    /// saw — drop the packed resident graph (it holds the old FFN
4300    /// tensors; the next build packs the live ones under a fresh id) and
4301    /// reset its device sequence.
4302    pub(crate) fn invalidate_for_weight_change(&mut self) {
4303        self.clear_sequence_state();
4304        self.embryo_graph = None;
4305    }
4306
4307    /// Prompt positions the cache already holds when `input_ids`
4308    /// strictly EXTENDS the consumed prefix (0 otherwise). Bounded-native
4309    /// models answer from the fixed-size `kv_prefix` record; everything
4310    /// else from the legacy `kv_history` vector (which the network split
4311    /// also reads and writes).
4312    fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4313        let (n, on_device) = if self.bounded_native() {
4314            (
4315                self.kv_prefix.extension(input_ids),
4316                self.kv_prefix.on_device(),
4317            )
4318        } else {
4319            let h = &self.kv_history;
4320            if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4321                (h.len(), self.kv_history_device)
4322            } else if !h.is_empty() && !self.kv_history_device {
4323                (self.rollback_prefix_len(input_ids), false)
4324            } else {
4325                (0, false)
4326            }
4327        };
4328        // The owner tag: the state the key describes must live where this
4329        // turn will continue it (R4) — else a fresh sequence.
4330        if n > 0 && !self.prefix_owner_matches(n, on_device) {
4331            tracing::warn!(
4332                "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4333                 the device now holds {:?} — re-prefilling from zero",
4334                if on_device { "resident device" } else { "host" },
4335                self.device_sequence_position()
4336            );
4337            return 0;
4338        }
4339        n
4340    }
4341
4342    /// Positions a turn whose ids DIVERGE from the consumed prefix keeps:
4343    /// their longest common prefix, when every layer can drop its rows past
4344    /// it exactly — full attention whose f32 rows the host owns (no wgpu
4345    /// mirror ahead of the host, no recurrent, O(1) or bounded state) and
4346    /// every trimmed sliding tail still holding the window the query at the
4347    /// cut reads. A chat app re-renders the previous answer through the
4348    /// template (a thinking model's history drops its `<think>` block), so a
4349    /// conversation almost never extends token for token: extension-only
4350    /// reuse re-prefilled the whole history every turn. One id at least is
4351    /// left to prefill. `CMF_KV_ROLLBACK=0` keeps extension-only reuse.
4352    fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
4353        if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
4354            || self.device_sequence_position().is_some()
4355        {
4356            return 0;
4357        }
4358        let h = &self.kv_history;
4359        let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
4360        let n = lcp.min(input_ids.len().saturating_sub(1));
4361        if n == 0 || self.kv_cache.layers.len() < self.num_layers {
4362            return 0;
4363        }
4364        let kv_id = self.graph_kv_id;
4365        for li in 0..self.num_layers {
4366            let l = &self.kv_cache.layers[li];
4367            let tail_ok = l.base() == 0
4368                || self
4369                    .layer_window(li)
4370                    .is_some_and(|w| l.base() + w <= n + 1);
4371            if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
4372                || l.o1.is_some()
4373                || l.bounded.is_some()
4374                || !l.linear_state.is_empty()
4375                || l.mode != crate::kv_cache::KvMode::F32
4376                || l.pos_len() != h.len()
4377                || !tail_ok
4378                || crate::gpu::graph_kv_stored(kv_id, li).is_some()
4379                || crate::gpu::graph_state_resident(kv_id, li)
4380            {
4381                return 0;
4382            }
4383        }
4384        n
4385    }
4386
4387    /// Public view of the prefix-reuse decision for `input_ids` (positions
4388    /// the next `generate*` would take from the cache; 0 = fresh sequence).
4389    pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4390        self.cached_prefix_len(input_ids)
4391    }
4392
4393    /// Record the forwarded prefix as the next turn's reuse key. A
4394    /// bounded-native model extends the rolling record (`reused` = the
4395    /// positions this turn found cached); others keep the literal vector.
4396    /// Either way the key carries its OWNER: the resident device graph
4397    /// (it holds an image of this sequence) or the host.
4398    fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4399        let on_device = self.device_sequence_position().is_some();
4400        if self.bounded_native() {
4401            let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4402            let prev_device = self.kv_prefix.on_device();
4403            self.kv_history.clear();
4404            self.kv_history_device = false;
4405            if keep && prev_device == on_device {
4406                self.kv_prefix.extend(&consumed[reused..]);
4407            } else {
4408                self.kv_prefix.set(consumed);
4409            }
4410            self.kv_prefix.set_on_device(on_device);
4411        } else {
4412            self.kv_history = consumed.to_vec();
4413            self.kv_history_device = on_device;
4414        }
4415    }
4416
4417    /// True when at least one layer runs the O(1) kernel.
4418    pub fn o1_active(&self) -> bool {
4419        self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4420    }
4421
4422    /// Whether generation's prompt ingest is routed through the whole-token
4423    /// graph.  The bench uses this to label the measured generation prefill
4424    /// honestly; keep the predicate in Pipeline so CLI labels cannot drift
4425    /// from the production route.
4426    /// Positions per batched-graph submit for the prompt: `CMF_BATCH_K`
4427    /// when set (0 = one position at a time through the token graph),
4428    /// otherwise 128 (32 for a MoE stack) on a discrete card whose prompt
4429    /// takes the graph route.
4430    /// The batched graph read a 2048-token prompt at 53 tok/s against 28.5
4431    /// one position at a time on an RTX PRO 4000 (Qwen3.8-27B q4tp: TTFT
4432    /// 39 s against 72), and its states are the speculative verify's,
4433    /// measured identical to the plain path. macOS keeps its own arm.
4434    /// 128, not 32: the q4tp prefill GEMM tiles 64 batch rows, so a
4435    /// 32-row chunk left half of every tile idle. Qwen3.8-27B q4tp on an
4436    /// RTX 3090, 1000-token prompt: 51 tok/s at 32, 82 at 64, 95 at 128,
4437    /// 97 at 256; greedy output byte-identical to the 32-row chunks
4438    /// (every GEMM row and the k-looped recurrences are per position).
4439    pub fn generation_batch_k(&self) -> usize {
4440        if let Some(k) = std::env::var("CMF_BATCH_K")
4441            .ok()
4442            .and_then(|v| v.parse::<usize>().ok())
4443        {
4444            return k;
4445        }
4446        // Gemma-4 (`g4_batch_prefill`): 128-row chunks, the GEMM tile's
4447        // two 64-row halves full.
4448        if self.g4_batch_prefill() {
4449            return 128;
4450        }
4451        // The sealed O(1) suffix of a bounded prompt (`CMF_O1_PREFILL`) on
4452        // the device route rides the batch graph, whose O(1) kernels take
4453        // the chunk in 64-row sub-chunks (`o1_batch`); 128 as the exact
4454        // prompt below. Without it the suffix went one position at a time.
4455        #[cfg(not(target_os = "macos"))]
4456        if self.o1_active()
4457            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
4458            && crate::gpu::enabled_here()
4459            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
4460        {
4461            return 128;
4462        }
4463        #[cfg(not(target_os = "macos"))]
4464        if self.graph_prefill_preferred() && !self.o1_active() {
4465            // 128 is measured on the dense hybrids; MoE stacks other than
4466            // the two expert codecs below keep the 32 their expert-grouped
4467            // prefill was measured at.
4468            let moe = self
4469                .weights
4470                .layers
4471                .iter()
4472                .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4473            // q8_2f experts (moe_q82's grouped prefill) read each expert
4474            // once per group of up to 16 of its entries, so the chunk that
4475            // fills those groups is the faster ingest: Mellum2.1 q8_2f on an
4476            // RTX 3090, 4000-token prompt, 503 tok/s at 64 rows, 542 at 128
4477            // (and 542 at 256).
4478            let q82 = crate::gpu::moe_q82_graph_on()
4479                && self.weights.layers.iter().any(|lw| {
4480                    matches!(&lw.ffn, FfnKind::Moe(m)
4481                        if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4482                });
4483            // q4tp experts: the grouped GEMM reads every routed expert once
4484            // per chunk, and 32 rows (256 entries over 256 experts, about
4485            // one row each) re-read nearly the whole expert set every 32
4486            // tokens. Apex-35B q4tp on an A40, 4096-token prompt, MTP off:
4487            // 295 tok/s at 32 rows, 357 at 64, 385 at 128, 401 at 256,
4488            // 396 at 512.
4489            let q4tp_moe = self.weights.layers.iter().any(|lw| {
4490                matches!(&lw.ffn, FfnKind::Moe(m)
4491                    if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q4tp().is_some()))
4492            });
4493            return if q82 {
4494                128
4495            } else if q4tp_moe || self.onevision2_prefill() {
4496                256
4497            } else if moe {
4498                32
4499            } else {
4500                128
4501            };
4502        }
4503        0
4504    }
4505
4506    pub fn generation_graph_prefill(&self) -> bool {
4507        let graph = self.graph_prefill_preferred();
4508        // On wgpu, an active MTP head now consumes the trunk's graph batches
4509        // and warms its own block from those returned rows.  The selected
4510        // generation measurement is therefore the batched path, even though
4511        // the underlying GDN model still satisfies the graph-prefill
4512        // predicate.  Keep the CLI label tied to the actual route.  Native
4513        // Metal has a separate prefill-batch arm and retains its historical
4514        // label here.
4515        // A batched prompt (`generation_batch_k` > 0) is the batched graph
4516        // for every model on the graph route, not only those with an MTP
4517        // head — the label follows the route.
4518        #[cfg(not(target_os = "macos"))]
4519        if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4520            return false;
4521        }
4522        graph
4523    }
4524
4525    /// Device-side O(1) mirrors currently uploaded for this pipeline's
4526    /// sequence.  The count/bytes are zero before seal or after a fresh
4527    /// reset; callers use this to distinguish logical host state from the
4528    /// GPU allocation that actually serves decode.
4529    pub fn o1_device_stats(&self) -> (usize, u64) {
4530        crate::gpu::o1_device_stats(self.graph_kv_id)
4531    }
4532
4533    /// Arm query collection on the o1 layers (fresh prompt pass).
4534    /// Reset the o1 layers to Collecting for a fresh sequence. Pub for the
4535    /// network split: each side runs the o1 lifecycle over ITS OWN layers
4536    /// (begin before prefill, seal at the prefill barrier).
4537    pub fn o1_begin(&mut self) {
4538        self.o1_begin_with_prefix(None);
4539    }
4540
4541    /// Arm collection and optionally request a positive calibration prefix.
4542    /// The effective barrier is always at least the skeleton-safe floor, so
4543    /// a short requested prefix cannot create an exact-only runtime state.
4544    pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4545        if let Some(c) = &self.o1_cfg {
4546            let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4547            let boundary = requested_prefix.map(|p| {
4548                p.max(
4549                    crate::nystrom::o1_deferred_boundary(w, sink)
4550                        .expect("o1 config boundary validated in set_o1"),
4551                )
4552            });
4553            for (li, &f) in self.o1_flags.iter().enumerate() {
4554                if f {
4555                    self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4556                }
4557            }
4558        }
4559    }
4560
4561    /// Effective deferred boundary for a positive prefix request.
4562    fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4563        self.o1_cfg.as_ref().and_then(|c| {
4564            crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4565                .map(|floor| requested_prefix.max(floor))
4566        })
4567    }
4568
4569    fn o1_note_transition(&mut self) {
4570        // Drain every layer's one-shot bit before publishing one pipeline
4571        // epoch. `any()` would short-circuit on the first layer and leak the
4572        // remaining bits into later forwards, causing one epoch per layer.
4573        let mut transitioned = false;
4574        for (li, &flagged) in self.o1_flags.iter().enumerate() {
4575            if flagged {
4576                transitioned |= self.kv_cache.layers[li].take_o1_transition();
4577            }
4578        }
4579        if transitioned {
4580            self.o1_epoch = self.o1_epoch.wrapping_add(1);
4581        }
4582    }
4583
4584    fn o1_pending(&self) -> bool {
4585        self.o1_flags.iter().enumerate().any(|(li, &f)| {
4586            f && self.kv_cache.layers[li].seq_len > 0
4587                && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4588        })
4589    }
4590
4591    fn o1_fail(&mut self, err: String) {
4592        tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4593        self.clear_sequence_state();
4594        self.graph_failed
4595            .store(true, std::sync::atomic::Ordering::Relaxed);
4596        self.cancel
4597            .store(true, std::sync::atomic::Ordering::Relaxed);
4598    }
4599
4600    /// Seal participating layers while retaining the exact state when the
4601    /// prompt is below the deferred boundary. A split worker may have
4602    /// collecting layers outside its owned span; zero-depth layers remain
4603    /// armed and are intentionally skipped until their peer runs them.
4604    pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4605        if self.o1_cfg.is_none() {
4606            return Ok(false);
4607        }
4608        let mut participating = false;
4609        for li in 0..self.num_layers {
4610            if !self.o1_flags.get(li).copied().unwrap_or(false) {
4611                continue;
4612            }
4613            if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4614                return Err(err);
4615            }
4616            if self.kv_cache.layers[li].seq_len == 0 {
4617                continue;
4618            }
4619            participating = true;
4620            let num_heads = self.layer_num_heads(li);
4621            self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4622        }
4623        self.o1_note_transition();
4624        for li in 0..self.num_layers {
4625            if self.o1_flags.get(li).copied().unwrap_or(false) {
4626                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4627                    return Err(err);
4628                }
4629            }
4630        }
4631        Ok(participating
4632            && (0..self.num_layers).all(|li| {
4633                !self.o1_flags.get(li).copied().unwrap_or(false)
4634                    || self.kv_cache.layers[li].seq_len == 0
4635                    || self.kv_cache.layers[li].o1_sealed()
4636            }))
4637    }
4638
4639    /// Complete a deferred boundary after a full position/span forward.
4640    /// This is the pipeline owner for epoch publication and failure cleanup.
4641    fn o1_progress(&mut self) {
4642        if !self.o1_active() {
4643            return;
4644        }
4645        for li in 0..self.num_layers {
4646            if self.o1_flags.get(li).copied().unwrap_or(false) {
4647                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4648                    self.o1_fail(err);
4649                    return;
4650                }
4651            }
4652        }
4653        // A qwen_attention row can seal in the middle of a complete layer
4654        // walk. Consume its transition even though the pending boundary has
4655        // already disappeared from the cache.
4656        self.o1_note_transition();
4657        if !self.o1_pending() {
4658            return;
4659        }
4660        if let Err(err) = self.o1_seal_checked() {
4661            self.o1_fail(err);
4662        }
4663    }
4664
4665    /// Turn a deferred O(1) failure raised by a hidden-only forward into the
4666    /// Result error its public batch/span caller must return. The failure
4667    /// path already cleared host/device sequence state; consume only the
4668    /// side-channel marker here and leave the pipeline reusable.
4669    fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4670        if self
4671            .graph_failed
4672            .swap(false, std::sync::atomic::Ordering::Relaxed)
4673        {
4674            self.cancel
4675                .store(false, std::sync::atomic::Ordering::Relaxed);
4676            self.clear_sequence_state();
4677            return Err(format!("{phase}: deferred O(1) transition failed"));
4678        }
4679        Ok(())
4680    }
4681
4682    /// Freeze landmarks + skeleton state after the prompt pass and drop
4683    /// the o1 layers' full KV; decode then runs `step()` per token.
4684    /// Pub for the network split (see `o1_begin`).
4685    pub fn o1_seal(&mut self) {
4686        if let Err(err) = self.o1_seal_checked() {
4687            self.o1_fail(err);
4688        }
4689    }
4690
4691    /// Run a bounded O(1) prefix (`ids`, positions 0..) through the batch
4692    /// graph instead of the host prefill: the same exact attention, with the
4693    /// O(1) layers' rotated queries captured on the device and their K/V rows
4694    /// read back from the mirror, so each layer's collector holds what the
4695    /// host walk would have given its seal. The recurrent and exact layers'
4696    /// state stays on the device for the sealed suffix batches.
4697    /// `Ok(None)`: declined before any device state moved — the caller runs
4698    /// the host prefill. `Ok(Some(h))`: the last prefix row's hidden.
4699    fn o1_prefix_on_graph(
4700        &mut self,
4701        ids: &[u32],
4702        chunk: usize,
4703    ) -> Result<Option<Vec<f32>>, String> {
4704        let o1_layers: Vec<usize> = (0..self.num_layers)
4705            .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
4706            .collect();
4707        let Some(&first) = o1_layers.first() else {
4708            return Ok(None);
4709        };
4710        let (nkv, hd) = {
4711            let c = &self.kv_cache.layers[first];
4712            (c.num_kv_heads, c.head_dim)
4713        };
4714        let nh = self.num_heads;
4715        if chunk == 0
4716            || o1_layers.iter().any(|&li| {
4717                let c = &self.kv_cache.layers[li];
4718                self.layer_num_heads(li) != nh
4719                    || (c.num_kv_heads, c.head_dim) != (nkv, hd)
4720                    || c.seq_len != 0
4721                    || c.o1_pending_boundary().is_none()
4722            })
4723            || !crate::gpu::o1_qcap_begin(&o1_layers)
4724        {
4725            return Ok(None);
4726        }
4727        let hs = self.hidden_size;
4728        let n = ids.len();
4729        let mut last = vec![0f32; hs];
4730        let mut pos = 0;
4731        while pos < n {
4732            let end = (pos + chunk).min(n);
4733            let bk = end - pos;
4734            let mut hiddens = vec![0f32; bk * hs];
4735            for (j, &id) in ids[pos..end].iter().enumerate() {
4736                hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
4737            }
4738            let positions: Vec<usize> = (pos..end).collect();
4739            match self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None) {
4740                crate::gpu::BatchGraphOutcome::Completed => {
4741                    last.copy_from_slice(&hiddens[(bk - 1) * hs..]);
4742                    pos = end;
4743                }
4744                crate::gpu::BatchGraphOutcome::Declined if pos == 0 => {
4745                    let _ = crate::gpu::o1_qcap_take();
4746                    return Ok(None);
4747                }
4748                _ => {
4749                    let _ = crate::gpu::o1_qcap_take();
4750                    return Err("O(1) prefix batch graph stopped after admission".into());
4751                }
4752            }
4753        }
4754        let q = crate::gpu::o1_qcap_take().ok_or("O(1) prefix: query readback failed")?;
4755        let pulls: Vec<(usize, usize, usize)> = o1_layers.iter().map(|&li| (li, 0, n)).collect();
4756        let kv = crate::gpu::graph_kv_read_rows(self.graph_kv_id, &pulls, nkv, hd)
4757            .ok_or("O(1) prefix: K/V readback failed")?;
4758        let (row, qrow) = (nkv * hd, nh * hd);
4759        for ((li, qs), (k, v)) in q.into_iter().zip(kv) {
4760            if k.len() != n * row || v.len() != n * row || qs.len() != n * qrow {
4761                return Err(format!(
4762                    "O(1) prefix: layer {li} read back {}/{}/{} values, expected {}/{}/{}",
4763                    k.len(),
4764                    v.len(),
4765                    qs.len(),
4766                    n * row,
4767                    n * row,
4768                    n * qrow
4769                ));
4770            }
4771            let cache = &mut self.kv_cache.layers[li];
4772            for p in 0..n {
4773                cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
4774                cache.o1_push_q(&qs[p * qrow..(p + 1) * qrow]);
4775            }
4776        }
4777        Ok(Some(last))
4778    }
4779
4780    /// Enable/disable the structured per-token telemetry trace (B4).
4781    pub fn set_trace(&mut self, on: bool) {
4782        self.trace = on;
4783    }
4784
4785    /// Replace all request-scoped sampler options and reset the random stream.
4786    /// This is required for deterministic `seed` semantics in pooled servers.
4787    pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4788        self.rng = match config.seed {
4789            Some(seed) => SplitMix64::new(seed),
4790            None => SplitMix64::from_entropy(),
4791        };
4792        self.sampler_config = config;
4793    }
4794
4795    /// Toggle the per-token confidence reduction (a full-vocab
4796    /// softmax each token). `bench --core` turns it off so the timed
4797    /// loop matches llama-bench's core contract; the result's
4798    /// `confidence` vec is empty while off.
4799    pub fn set_confidence(&mut self, on: bool) {
4800        self.confidence_on = on;
4801    }
4802
4803    /// Set the confidence-calibration temperature (B1). Values ≤0 are
4804    /// clamped to raw (1.0).
4805    pub fn set_calib_temp(&mut self, t: f32) {
4806        self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4807    }
4808
4809    /// The active calibration temperature (1.0 = raw probability).
4810    pub fn calib_temp(&self) -> f32 {
4811        self.calib_temp
4812    }
4813
4814    /// The primary (full-attention) RoPE inverse-frequency table selected
4815    /// from the CMF architecture.  This is read-only diagnostic state for
4816    /// parity checks; local sliding layers may use `inv_freq_local` instead.
4817    pub fn rope_inv_freq(&self) -> &[f32] {
4818        self.inv_freq.as_slice()
4819    }
4820
4821    /// Partial rotary (Qwen3.5): rotate only the first `rotary_dim` dims;
4822    /// the frequency table is rebuilt over the rotary dims.
4823    pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4824        self.rotary_dim = rotary_dim.min(self.head_dim);
4825        self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4826        // The packed resident graph owns its own inverse-frequency plane;
4827        // changing RoPE after it was built must not leave a stale device
4828        // model behind the exact host configuration.
4829        self.embryo_graph = None;
4830    }
4831
4832    fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4833        QwenAttnCfg {
4834            mrope: None,
4835            num_heads: self.num_heads,
4836            num_kv_heads: self.num_kv_heads,
4837            head_dim: self.head_dim,
4838            hidden_size: self.hidden_size,
4839            position,
4840            inv_freq: &self.inv_freq,
4841            rotary_dim: self.rotary_dim,
4842            scale: self.attn_scale,
4843            softcap: self.attn_softcap,
4844            window: None,
4845            v_norm: false,
4846            qk_norm_after_rope: self.qk_norm_after_rope,
4847            gate_sigmoid: self.proj_gate_sigmoid,
4848            q_norm: None,
4849            k_norm: None,
4850            output_gate: false,
4851            softplus_gate: None,
4852            rope_scale: self.rope_scale,
4853            bias: None,
4854            rms_eps: self.rms_eps,
4855            norm_style: self.norm_style,
4856            pool: self.pool.as_deref(),
4857            v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4858        }
4859    }
4860
4861    /// Generate text from a plain-text prompt. Streams tokens via `on_token`.
4862    pub fn generate(
4863        &mut self,
4864        prompt: &str,
4865        max_tokens: usize,
4866        task_mask: Option<&TaskMask>,
4867        on_token: Option<TokenCallback>,
4868    ) -> Result<GenerateResult, String> {
4869        let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4870        self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4871    }
4872
4873    /// Generate from a V4.1 multimodal prompt prepared by the vision module.
4874    /// Vision rows are encoded once and fed through the same bounded token walk as text.
4875    pub fn generate_from_vl(
4876        &mut self,
4877        input: &crate::dsv41_vision::PreparedVlInputs,
4878        max_tokens: usize,
4879        task_mask: Option<&TaskMask>,
4880        on_token: Option<TokenCallback>,
4881    ) -> Result<GenerateResult, String> {
4882        let Some(dsv41) = &self.dsv41 else {
4883            return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4884        };
4885        if input.token_ids.is_empty() {
4886            return Err("empty V4.1 multimodal prompt".into());
4887        }
4888        if input.token_types.len() != input.token_ids.len() {
4889            return Err(format!(
4890                "V4.1 token type count {} != token count {}",
4891                input.token_types.len(),
4892                input.token_ids.len()
4893            ));
4894        }
4895        let dim = dsv41.2.dim;
4896        let mut embeddings = vec![None; input.token_ids.len()];
4897        let mut participates = vec![true; input.token_ids.len()];
4898        if !input.images.is_empty() {
4899            let vision = self
4900                .dsv41_vision
4901                .as_ref()
4902                .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4903            for image in &input.images {
4904                let end = image.start.saturating_add(image.types.len());
4905                if end > input.token_ids.len() {
4906                    return Err(format!(
4907                        "V4.1 image span {}..{} exceeds prompt length {}",
4908                        image.start,
4909                        end,
4910                        input.token_ids.len()
4911                    ));
4912                }
4913                let mut span = vec![0.0f32; image.types.len() * dim];
4914                vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4915                for (offset, &kind) in image.types.iter().enumerate() {
4916                    let pos = image.start + offset;
4917                    if input.token_types[pos] != kind {
4918                        return Err(format!(
4919                            "V4.1 image type mismatch at position {pos}: {} != {kind}",
4920                            input.token_types[pos]
4921                        ));
4922                    }
4923                    embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4924                    participates[pos] = false;
4925                }
4926            }
4927        }
4928        for (pos, &kind) in input.token_types.iter().enumerate() {
4929            if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4930                return Err(format!("V4.1 text position {pos} has an image embedding"));
4931            }
4932            if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4933                return Err(format!("V4.1 image position {pos} has no image embedding"));
4934            }
4935        }
4936        self.dsv41_prefill = Some((embeddings, participates));
4937        let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4938        self.dsv41_prefill = None;
4939        result
4940    }
4941
4942    /// `None` when the mask forbids nothing (see `TaskMask::fully_open`).
4943    fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4944        m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4945    }
4946
4947    /// Generate from prepared token ids (e.g. a chat template).
4948    ///
4949    /// With an MTP head, greedy generation without a task mask takes the
4950    /// speculative path: the MTP module drafts the token after next and
4951    /// the main model verifies both in one fused two-position forward
4952    /// (weights streamed once). The output is EXACTLY the vanilla greedy
4953    /// sequence — a rejected draft is rolled back — MTP only buys speed.
4954    pub fn generate_from_ids(
4955        &mut self,
4956        input_ids: &[u32],
4957        max_tokens: usize,
4958        task_mask: Option<&TaskMask>,
4959        on_token: Option<TokenCallback>,
4960    ) -> Result<GenerateResult, String> {
4961        self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4962    }
4963
4964    /// Generate from complete prompt embeddings [token_count, hidden_size].
4965    /// Text rows can be obtained with `embed_id`; media rows replace only
4966    /// their expanded placeholder positions. Rows are already scaled and
4967    /// enter `PrefillIn::Hidden`, so a device graph must not re-embed them.
4968    /// Token-only KV reuse is disabled both into and out of this request.
4969    pub fn generate_from_embeds(
4970        &mut self,
4971        input_ids: &[u32],
4972        prompt_rows: &[f32],
4973        max_tokens: usize,
4974        task_mask: Option<&TaskMask>,
4975        on_token: Option<TokenCallback>,
4976    ) -> Result<GenerateResult, String> {
4977        if input_ids.is_empty()
4978            || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4979        {
4980            return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4981        }
4982        if prompt_rows.iter().any(|x| !x.is_finite()) {
4983            return Err("embedded prompt contains non-finite values".into());
4984        }
4985        if !self.can_prefill_batched()
4986            || self.dyn_router.is_some()
4987            || self.o1_active()
4988            || self.mtp.is_some()
4989            || self.gpu_plan.is_some()
4990        {
4991            return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4992        }
4993        self.generate_with_prompt_rows(
4994            input_ids,
4995            Some(prompt_rows),
4996            max_tokens,
4997            task_mask,
4998            on_token,
4999        )
5000    }
5001
5002    fn generate_with_prompt_rows(
5003        &mut self,
5004        input_ids: &[u32],
5005        prompt_rows: Option<&[f32]>,
5006        max_tokens: usize,
5007        task_mask: Option<&TaskMask>,
5008        mut on_token: Option<TokenCallback>,
5009    ) -> Result<GenerateResult, String> {
5010        #[cfg(target_os = "macos")]
5011        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
5012        if std::env::var("CMF_TRACE_H").is_ok() {
5013            eprintln!("input_ids: {input_ids:?}");
5014        }
5015        if input_ids.is_empty() {
5016            return Err("empty prompt: nothing to generate from".to_string());
5017        }
5018        // A prior graph failure is terminal for that sequence but must not
5019        // poison the next independent request.  Keep this flag separate from
5020        // the externally-owned cooperative cancel bit.
5021        self.graph_failed
5022            .store(false, std::sync::atomic::Ordering::Relaxed);
5023        // A mask that forbids nothing still costs every fused path and
5024        // whole-token graph, all of which are gated on `is_none()`. A
5025        // narrowed file whose one segment is always on carries exactly
5026        // such a mask — drop it here rather than pay 5x for a no-op.
5027        let task_mask = self.drop_open_mask(task_mask);
5028        // A block-diffusion model generates whole blocks by denoising
5029        // previews, never one causal token at a time.
5030        if self.clover.is_some() {
5031            if prompt_rows.is_some() || task_mask.is_some() {
5032                return Err("block-diffusion generation takes token prompts without task masks".into());
5033            }
5034            return self.clover_generate(input_ids, max_tokens, on_token);
5035        }
5036
5037        // Cross-turn KV reuse: a chat app resends the whole history
5038        // every turn; when the new ids strictly EXTEND what the cache
5039        // already holds, prefill only the tail — turn latency stays
5040        // proportional to the new text instead of the whole session.
5041        // Extension-only (no rollback), so it is exact for every layer
5042        // kind including recurrent state; MTP/o1/task-mask runs keep
5043        // the fresh-sequence path. CMF_KV_REUSE=0 disables.
5044        let mut reuse_from = {
5045            let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
5046            if on
5047                && prompt_rows.is_none()
5048                && task_mask.is_none()
5049                && self.mtp.is_none()
5050                && !(self.mimo_mtp.is_some() && self.speculative)
5051                && self.o1_cfg.is_none()
5052                && self.dsv41.is_none()
5053            {
5054                self.cached_prefix_len(input_ids)
5055            } else {
5056                0
5057            }
5058        };
5059        // The device may own rows the host tail prefill needs (wgpu decode
5060        // writes only its mirror): hand them to the host, or start fresh.
5061        if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
5062            reuse_from = 0;
5063        }
5064        // A diverging turn (`rollback_prefix_len`): drop every row past the
5065        // common prefix, so the tail prefill appends at `reuse_from`.
5066        if reuse_from > 0 && !self.bounded_native() && reuse_from < self.kv_history.len() {
5067            for l in &mut self.kv_cache.layers {
5068                let d = l.pos_len() - reuse_from;
5069                l.truncate_last(d);
5070            }
5071            if std::env::var("CMF_PREFILL_PROF").is_ok() {
5072                eprintln!(
5073                    "kv-reuse: rolled back {} answer position(s) to the common prefix",
5074                    self.kv_history.len() - reuse_from
5075                );
5076            }
5077            self.kv_history.truncate(reuse_from);
5078            self.graph_logits = None;
5079        }
5080        self.swa_trim_anchor = input_ids.len();
5081        self.last_prefill_tokens = input_ids.len() - reuse_from;
5082        let bounded_native = self.bounded_native();
5083        if reuse_from == 0 {
5084            // Fresh sequence — the cache holds absolute positions.
5085            // MTP never participates in cross-turn KV reuse, so a
5086            // high-vocabulary token from the previous request must not keep
5087            // this request on its expensive full draft head.
5088            self.reset_fresh_mtp_draft_state();
5089            self.clear_sequence_state();
5090        } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
5091            eprintln!(
5092                "kv-reuse: {} of {} prompt positions already cached",
5093                reuse_from,
5094                input_ids.len()
5095            );
5096        }
5097        crate::gpu::graph_race_begin_generation();
5098        // Optional bounded calibration prefix. Keep the requested value
5099        // even when it is longer than the prompt; the collecting layer will
5100        // defer at the effective boundary and remain exact for short input.
5101        let o1_prefill = if self.o1_active() && task_mask.is_none() {
5102            std::env::var("CMF_O1_PREFILL")
5103                .ok()
5104                .and_then(|v| v.parse::<usize>().ok())
5105                .filter(|&p| p > 0)
5106        } else {
5107            None
5108        };
5109        if task_mask.is_none() {
5110            self.o1_begin_with_prefix(o1_prefill);
5111        }
5112
5113        // Speculative decode is off under o1: a rejected draft can't be
5114        // rolled back out of the far accumulators / ring window (the
5115        // Nyström insertion is irreversible by design).
5116        // The wgpu token graph owns a device K/V mirror that speculative
5117        // rollback would desync — the two are mutually exclusive.
5118        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
5119        // Graph speculative decode (`CMF_GRAPH_SPEC=1`): the MTP head
5120        // drafts, ONE batched graph submit verifies the whole chain.
5121        //
5122        // It now PAYS on Qwen3.6-27B / RTX 5090 — 51.1 tok/s against a
5123        // plain 49.4 at k=3, medians of three, 89% of drafts accepted,
5124        // and the greedy continuation is byte-identical to the plain
5125        // path. That took the batch matvec sharing its nibble unpack
5126        // across the batch (`CMF_MV_BK=2`); before it, the same round
5127        // measured 43.6, an 11% LOSS, which is what the earlier note
5128        // here described.
5129        //
5130        // Still opt-in. One model's win is not a default: the verify
5131        // rides `gdn_spec_restore` and a batched frame whose numerics
5132        // are the batch kernels', and that has to be shown on more than
5133        // one architecture before every greedy decode takes it.
5134        // Greedy (with or without penalties) verifies by argmax equality.
5135        // Sampling (temperature > 0) can go through speculative SAMPLING —
5136        // draft from the MTP head's own post-chain distribution, accept
5137        // with min(1, p/q), correct from max(0, p − q); the emitted stream
5138        // is distributed exactly as the plain sampler's — but it is
5139        // OPT-IN (`CMF_GRAPH_SPEC_SAMPLE=1`): measured on Qwen3.8-27B /
5140        // RTX 5090 at the instruct row (0.7 / 0.80 / 20 / presence 1.5)
5141        // it decoded 19-22 tok/s against a plain 40 — nine post-chain
5142        // distributions a round plus a lower acceptance than greedy's,
5143        // against a verify that costs 2.7 single tokens. The greedy arms
5144        // pay +10%; the sampling arm needs a cheaper verify first.
5145        // Native Metal has a fast row-batch verify, but it is NOT admitted
5146        // by default: a real-model exactness check found an emitted greedy
5147        // divergence from the serial path.  Every native-Metal speculative
5148        // arm therefore requires the explicit `CMF_GRAPH_SPEC=1` experiment
5149        // gate until the batch kernels are numerically qualified.  If it is
5150        // explicitly enabled, its sampling arm is useful only for a config
5151        // the SPARSE chain serves (a top-k within `sparse_ok`): without it a
5152        // round builds nine 248k-float distributions on the host, which is
5153        // the 5090's measured loss and not a cost the round-token proxy
5154        // below can see. A top-k-less sampling config keeps the plain path
5155        // unless asked for by name.
5156        #[cfg(target_os = "macos")]
5157        let metal_graph = crate::gpu::q1_force()
5158            && crate::gpu::enabled_here()
5159            && std::env::var("CMF_GPU_BLOCK")
5160                .map(|v| v != "0")
5161                .unwrap_or(true);
5162        // `CMF_GPU_BLOCK=0` declines the ordinary native block route but
5163        // does not change the backend identity: a separately requested wgpu
5164        // graph can still reach `graph_spec_step`, whose native branch uses
5165        // VerifyGraph. Keep the quality policy tied to that identity, not to
5166        // the particular block-route admission above.
5167        #[cfg(target_os = "macos")]
5168        let native_metal_backend = crate::gpu::q1_force();
5169        #[cfg(not(target_os = "macos"))]
5170        let metal_graph = false;
5171        #[cfg(not(target_os = "macos"))]
5172        let native_metal_backend = false;
5173        let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
5174        // A round whose cost is the MEASURED one: greedy (argmax rows), or
5175        // sampling through the sparse chain. Anything else pays the dense
5176        // chain's host time, which no proxy can price.
5177        let spec_cheap_round =
5178            self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
5179        let spec_sampling_ok = self.sampler_config.temperature < 1e-6
5180            || match spec_sample_env.as_deref() {
5181                Some("1") => true,
5182                Some(_) => false,
5183                None => metal_graph && spec_cheap_round,
5184            };
5185        // ON by default for greedy on the wgpu graph: with the draft on
5186        // the graph and the verify bit-exact, it measured 58.7 tok/s
5187        // against a plain 48.1 on Qwen3.8-27B q4tp / RTX 5090 (k=4) and
5188        // 51.1 against 49.4 on Qwen3.6-27B, and a round that stops
5189        // paying turns itself off below (acceptance watchdog).
5190        // `CMF_GRAPH_SPEC=0` disables; `=1` is the explicit opt-in spelling.
5191        // …but only where the batched verify has its register-blocked
5192        // kernel: q4tp dense FFNs (graph kind 6). q4t and q8_2f verify
5193        // through tile GEMMs today and measured a LOSS (q8_2f 22 against
5194        // 29 tok/s), the 2-bit plane the same; those stay opt-in
5195        // (`CMF_GRAPH_SPEC=1`).
5196        // …at least in nine dense FFNs of ten: a healed file carries its
5197        // last two layers at q8_2f, and two tile-GEMM verifies among 64 do
5198        // not change the arithmetic (measured: the healed q4tp file
5199        // decodes at the plain file's rate and would otherwise sit out).
5200        let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
5201        for lw in &self.weights.layers {
5202            if let FfnKind::Dense(d) = &lw.ffn {
5203                dense_n += 1;
5204                if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
5205                    && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
5206                    && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
5207                {
5208                    dense_q4tp += 1;
5209                }
5210            }
5211        }
5212        // …and only where the MTP block itself runs on the graph. A MoE
5213        // draft block (Qwen3.6-35B-A3B, Apex-35B: 256 experts in the MTP
5214        // layer) fails `mtp_graph_ok`, so it drafts — and warms its cache
5215        // over the whole prompt — op by op on the host. Apex-35B q4tp on an
5216        // A40 + Xeon Gold 6342: greedy 51.8 against 65.6 tok/s plain (300
5217        // tokens, 57 % accepted), and 2.5 ms per prompt position (a
5218        // 4096-token ingest 171 against 295 tok/s); the published
5219        // Qwen3.6-35B-A3B 79.7 against 86.6. `CMF_GRAPH_SPEC=1` opts in.
5220        #[cfg(feature = "gpu")]
5221        let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| self.mtp_graph_ok(m));
5222        #[cfg(not(feature = "gpu"))]
5223        let mtp_on_graph = true;
5224        let spec_default_ok = (dense_n == 0 || dense_q4tp * 10 >= dense_n * 9) && mtp_on_graph;
5225        // Penalties break the draft head's agreement with the trunk (a
5226        // 1.1 repetition penalty measured 2 of 16 accepted): not by
5227        // default there either — off Metal that rule is untouched, and
5228        // suppressed ids keep counting as a penalty there, because no
5229        // measurement on a discrete card says otherwise.
5230        //
5231        // On native Metal the penalized arms DO pay: the draft applies
5232        // the same penalty and the verify scores the penalized rows
5233        // exactly (`greedy_pen`, the plain loop's arithmetic), so the
5234        // text is the plain path's and only the round's shape changes.
5235        // Measured on this M4 — see the report for the interleaved run.
5236        let penalized = !metal_graph
5237            && (self.sampler_config.repetition_penalty != 1.0
5238                || self.sampler_config.presence_penalty != 0.0
5239                || !self.sampler_config.suppress_tokens.is_empty());
5240        // …and not on wgpu-over-Metal: the batched verify graph there
5241        // returned 0 accepted drafts and garbage text on a GDN hybrid
5242        // (16.08, Qwen3.5-0.8B) while Vulkan is bit-exact; the Mac's
5243        // default backend is native Metal without a batch graph anyway.
5244        #[cfg(feature = "gpu")]
5245        let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
5246        #[cfg(not(feature = "gpu"))]
5247        let metal_wgpu = false;
5248        let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
5249        let spec_wanted = graph_spec_wanted_by_policy(
5250            spec_env.as_deref(),
5251            spec_default_ok,
5252            penalized,
5253            metal_wgpu,
5254            native_metal_backend,
5255        );
5256        if spec_wanted && metal_wgpu {
5257            tracing::warn!(
5258                "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
5259                 verified on this backend (garbage measured on Qwen3.5-0.8B)"
5260            );
5261        }
5262        // Native Metal: the b-row verify graph (`try_batch_graph_metal`)
5263        // stands where the wgpu batch graph stands on discrete cards
5264        // (`metal_graph`, above).
5265        let graph_spec = self.speculative
5266            && (graph_on || metal_graph)
5267            && self.mtp.is_some()
5268            && task_mask.is_none()
5269            && !self.o1_active()
5270            && spec_sampling_ok
5271            && spec_wanted;
5272        // The host fused pair is an optimization, not a verifier. On real
5273        // q4tp Qwen3.5 a near-tied row produced a different greedy argmax
5274        // from two serial target forwards. That makes a seemingly accepted
5275        // draft change text, which is worse than losing speculative speed.
5276        // Keep it explicitly experimental until a model/backend has passed
5277        // a long exact-ID gate; the native Metal batch verifier has its own
5278        // separate graph admission and is not controlled by this switch.
5279        //
5280        // GDN hybrids are also excluded by default: their recurrence is
5281        // sequential, so the pair lane cannot parallelize and measured 2x
5282        // slower on the 35B. `CMF_MTP_PAIR=1` is deliberately the only
5283        // opt-in for the host fused-pair experiment (not `CMF_MTP=1`).
5284        let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
5285        let pair_pays = host_pair_opt_in
5286            && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
5287        // Native Metal: say the route ONCE (RUST_LOG=info), including the
5288        // deliberate quality gate that keeps the unqualified batch verifier
5289        // off unless the user explicitly requested the experiment.
5290        #[cfg(target_os = "macos")]
5291        if metal_graph {
5292            static SAID: std::sync::Once = std::sync::Once::new();
5293            SAID.call_once(|| {
5294                let spec = if graph_spec {
5295                    let k = std::env::var("CMF_GRAPH_SPEC_K")
5296                        .ok()
5297                        .and_then(|v| v.parse::<usize>().ok())
5298                        .filter(|&v| (1..=8).contains(&v))
5299                        .unwrap_or(7);
5300                    let arm = if self.sampler_config.temperature < 1e-6 {
5301                        "greedy"
5302                    } else {
5303                        "sampling"
5304                    };
5305                    format!(
5306                        "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
5307                        Self::draft_vocab_rows(usize::MAX)
5308                    )
5309                } else if !self.speculative {
5310                    "spec off (CMF_MTP=0)".to_string()
5311                } else if self.mtp.is_none() {
5312                    "spec off (no MTP head)".to_string()
5313                } else if !spec_sampling_ok {
5314                    if spec_cheap_round {
5315                        "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
5316                    } else {
5317                        "spec off (sampling without a top-k: the dense chain \
5318                         costs more than it saves)"
5319                            .to_string()
5320                    }
5321                } else if !spec_wanted {
5322                    if spec_env.is_none() {
5323                        "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
5324                    } else {
5325                        "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
5326                    }
5327                } else if task_mask.is_some() {
5328                    "spec off (task mask)".to_string()
5329                } else if !graph_spec && !pair_pays {
5330                    "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
5331                        .to_string()
5332                } else {
5333                    "spec off (O(1) attention)".to_string()
5334                };
5335                let on = |var: &str| {
5336                    if std::env::var(var).as_deref() == Ok("0") {
5337                        "off"
5338                    } else {
5339                        "on"
5340                    }
5341                };
5342                tracing::info!(
5343                    "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
5344                     MTP graph {}, attend {}, probe {}",
5345                    if crate::gpu_metal::state4_on() {
5346                        "on"
5347                    } else {
5348                        "off"
5349                    },
5350                    if crate::gpu_metal::async_replay_on() {
5351                        "on"
5352                    } else {
5353                        "off"
5354                    },
5355                    on("CMF_METAL_PREFILL"),
5356                    on("CMF_MTP_GRAPH"),
5357                    std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
5358                    if crate::gpu::probe_enabled() {
5359                        "bypassed (q1 force)"
5360                    } else {
5361                        "off"
5362                    },
5363                );
5364            });
5365        }
5366        let spec_active = self.speculative
5367            && self.mtp.is_some()
5368            && task_mask.is_none()
5369            && !self.o1_active()
5370            && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
5371        // The MTP module is detached during generation so its mutable
5372        // state does not fight the borrow on `self`.
5373        let mut mtp = if spec_active { self.mtp.take() } else { None };
5374        if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
5375            eprintln!(
5376                "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
5377                mtp.is_some(),
5378                self.speculative,
5379                self.sampler_config.temperature < 1e-6,
5380            );
5381        }
5382        if let Some(m) = &mut mtp {
5383            m.kv.clear();
5384            // The MTP block's own device mirror starts over with its cache.
5385            crate::gpu::graph_kv_reset(self.mtp_kv_id());
5386            self.mtp_graph_mode = None;
5387        }
5388        // MiMo-V2's draft stack: greedy rounds (draft K with the chained
5389        // MTP layers, verify K+1 rows in one batched forward). Sampling
5390        // decodes plain; `CMF_MTP=0` / `CMF_MIMO_MTP=0` turn it off.
5391        let mimo_spec = self.speculative
5392            && self.mimo_mtp.is_some()
5393            && task_mask.is_none()
5394            && !self.o1_active()
5395            && self.dyn_router.is_none()
5396            && self.sampler_config.temperature < 1e-6
5397            && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
5398        if let Some(st) = self.mimo_mtp.as_mut() {
5399            st.reset();
5400            if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
5401                Self::mimo_mtp_hist_cap(st, input_ids.len());
5402            }
5403        }
5404        // Dynamic router detached during decode (same borrow trick as MTP).
5405        // Speculative decode and dynamic routing are mutually exclusive
5406        // for now — the fused-pair path doesn't carry per-token φ.
5407        let mut router = if mtp.is_none() {
5408            self.dyn_router.take()
5409        } else {
5410            None
5411        };
5412        let mut reuse_from = reuse_from;
5413        if let Some(r) = &mut router {
5414            r.reset(); // active=backbone, matching a fresh overlay
5415            self.dyn_phi_seen = 0; // fresh φ EMA per generation
5416            if self.dyn_active.is_some() {
5417                // A real switch back to the backbone invalidates the
5418                // cache the reuse key was computed against.
5419                let _ = self.set_active_skill(None);
5420                reuse_from = 0;
5421                self.last_prefill_tokens = input_ids.len();
5422            }
5423        }
5424
5425        let mut all_ids = input_ids.to_vec();
5426        let mut generated = 0usize;
5427        let mut finish_reason = "max_tokens".to_string();
5428        let mut drafted = 0usize;
5429        let mut accepted = 0usize;
5430        // DeepSeek-V4's draft quality is strongly content-dependent.  Two
5431        // consecutive paid rounds with no extra token put it on a bounded
5432        // cooldown; predictable text keeps batching, ordinary prose falls
5433        // back to the exact walk instead of paying a slow draft forever.
5434        // Local to one generation so one difficult request cannot poison the
5435        // next one, and deliberately automatic — this is not a user knob.
5436        let mut dsv4_spec_bad = 0usize;
5437        let mut dsv4_spec_retry_at = 0usize;
5438        let mut confidence: Vec<f32> = Vec::new();
5439        let trace_on = self.trace;
5440        let calib_temp = self.calib_temp;
5441        let mut traces: Vec<TokenTrace> = Vec::new();
5442
5443        // ── Prefill: forward each prompt token once, KEEP the last hidden.
5444        //    Dense prefill runs in fused pairs (weights streamed once per
5445        //    two positions — bit-identical to sequential, proven by the
5446        //    pair tests). With MTP: warm the draft head on
5447        //    (hidden_p, token_{p+1}) pairs.
5448        let mut hidden = vec![0.0f32; self.hidden_size];
5449        let mut pos = reuse_from;
5450        // lm_head-in-graph is only sound when the very next logits
5451        // consumer is this loop's own (MTP and skill routing interleave
5452        // other forwards / can swap lm_head between forward and sample).
5453        // CMF_GPU_LMHEAD=0 keeps lm_head off the graph: the token reads back
5454        // the 8 KB hidden instead of ~1 MB of logits, and the head runs on
5455        // the host. A probe for how much of the graph's fixed per-token cost
5456        // is the logits readback (the layer sweep puts that fixed part at
5457        // 3.88 ms of an 18.5 ms frame).
5458        let fuse_lm = mtp.is_none()
5459            && router.is_none()
5460            && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5461        self.graph_logits = None;
5462        self.graph_want_logits = false;
5463        let _tpf = std::time::Instant::now();
5464        let batch_k = self.generation_batch_k();
5465        if let Some(rows) = prompt_rows {
5466            let hs = self.hidden_size;
5467            // The batched graph's row count when the prompt rides it.
5468            let chunk = if batch_k > 0 { batch_k } else { self.prefill_chunk().max(1) };
5469            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5470                let end = (pos + chunk).min(input_ids.len());
5471                let hb = match self.prefill_input_rows(
5472                    PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5473                    pos,
5474                    task_mask,
5475                ) {
5476                    Ok(hb) => hb,
5477                    Err(err) => {
5478                        self.prompt_mrope = None;
5479                        self.finish_generation(&mut mtp, &mut router, true);
5480                        return Err(err);
5481                    }
5482                };
5483                if mimo_spec {
5484                    self.mimo_note_rows(&hb, pos);
5485                }
5486                hidden.copy_from_slice(&hb[hb.len() - hs..]);
5487                pos = end;
5488            }
5489        }
5490        // The prompt's MRoPE rows are in the cache; decode positions are
5491        // plain (`sensenova::mrope_positions` shifted them so).
5492        self.prompt_mrope = None;
5493        // DeepSeek-V4 owns a separate hyper-connection stack. Route it
5494        // before the generic prefill choices: those correctly reject an
5495        // empty `weights.layers`, but their final per-position fallback used
5496        // to consume the whole prompt before `dsv4::forward_chunk` could see
5497        // it. The batch implementation therefore existed without a live
5498        // production entry point.
5499        //
5500        // Bounded chunks preserve cancellation responsiveness. Only the
5501        // prompt's final chunk asks for logits; every earlier head projection
5502        // would produce 129 280 values that no caller reads.
5503        while self.qwen4_exp.is_some()
5504            && mtp.is_none()
5505            && pos < input_ids.len()
5506            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5507        {
5508            // The device path takes several prompt tokens per layer frame;
5509            // the host path runs them one by one inside the same call.
5510            let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5511            let want_logits = end == input_ids.len();
5512            let mut lg = Vec::new();
5513            if let Some(b) = &mut self.qwen4_exp {
5514                crate::qwen4_exp::forward_tokens(
5515                    &b.0,
5516                    &b.1,
5517                    &b.2,
5518                    &mut b.3,
5519                    &input_ids[pos..end],
5520                    pos,
5521                    &self.inv_freq,
5522                    self.pool.as_deref(),
5523                    &mut lg,
5524                    want_logits,
5525                );
5526            }
5527            if want_logits {
5528                self.graph_logits = Some(lg);
5529            }
5530            pos = end;
5531            hidden.fill(0.0);
5532        }
5533        while self.dsv4.is_some()
5534            && mtp.is_none()
5535            && pos < input_ids.len()
5536            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5537        {
5538            let end = (pos + prefill_chunk()).min(input_ids.len());
5539            let ids: Vec<u32> = input_ids[pos..end].to_vec();
5540            let mut lg = Vec::new();
5541            if let Some(b) = &mut self.dsv4 {
5542                let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5543                crate::dsv4::forward_chunk(
5544                    g,
5545                    layers,
5546                    &cfg,
5547                    st,
5548                    &ids,
5549                    pos,
5550                    &self.inv_freq,
5551                    self.pool.as_deref(),
5552                    &mut lg,
5553                    end == input_ids.len(),
5554                );
5555            }
5556            if end == input_ids.len() {
5557                self.graph_logits = Some(lg);
5558            }
5559            pos = end;
5560            hidden = vec![0.0; self.hidden_size];
5561        }
5562        let dsv41_prefill = self.dsv41_prefill.take();
5563        while self.dsv41.is_some()
5564            && mtp.is_none()
5565            && pos < input_ids.len()
5566            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5567        {
5568            let end = (pos + prefill_chunk()).min(input_ids.len());
5569            let ids: Vec<u32> = input_ids[pos..end].to_vec();
5570            let mut lg = Vec::new();
5571            if let Some(b) = &mut self.dsv41 {
5572                let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5573                if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5574                    crate::dsv41::forward_chunk_masked_with_embeddings(
5575                        g,
5576                        layers,
5577                        cfg,
5578                        st,
5579                        &ids,
5580                        pos,
5581                        &embeddings[pos..end],
5582                        &participates[pos..end],
5583                        self.pool.as_deref(),
5584                        &mut lg,
5585                    );
5586                } else {
5587                    crate::dsv41::forward_chunk(
5588                        g,
5589                        layers,
5590                        cfg,
5591                        st,
5592                        &ids,
5593                        pos,
5594                        self.pool.as_deref(),
5595                        &mut lg,
5596                    );
5597                }
5598            }
5599            if end == input_ids.len() {
5600                self.graph_logits = Some(lg);
5601            }
5602            pos = end;
5603            hidden = vec![0.0; self.hidden_size];
5604        }
5605        // With dynamic routing, prefill sequentially so the φ hook fires
5606        // over the PROMPT — the router enters decode with a warm φ (the
5607        // fused-pair path skips the per-layer φ capture). o1 layers
5608        // collect their query trace in both the single and pair paths.
5609        let dyn_prefill = router.is_some();
5610        // Optional bounded calibration prefix for generation.  The normal
5611        // O(1) path seals after the full prompt; this explicit knob instead
5612        // runs only the requested prefix through exact attention, seals the
5613        // Nyström state, and streams the rest of the prompt through the same
5614        // O(1) step used by decode.  It keeps the O(1) layers' Q trace and
5615        // temporary full KV bounded by the prefix while leaving the default
5616        // full-prompt quality profile untouched.
5617        let o1_prefill_limit = o1_prefill
5618            .and_then(|requested| self.o1_effective_boundary(requested))
5619            .map(|boundary| boundary.min(input_ids.len()));
5620        let mut o1_sealed = false;
5621        if let Some(limit) = o1_prefill_limit {
5622            // On the device route the prefix takes the batch graph and its
5623            // sealed suffix the O(1) batches after it (`o1_prefix_on_graph`):
5624            // the host walk read it at 16 tok/s on Qwen3.8-27B / A40, 33 s
5625            // of a 72 s TTFT at 4k. Only when a suffix follows — a prompt
5626            // inside the boundary stays exact on the host as before.
5627            // `CMF_O1_PREFIX_GPU=0` keeps the host prefill.
5628            if pos == 0
5629                && limit < input_ids.len()
5630                && batch_k > 0
5631                && task_mask.is_none()
5632                && mtp.is_none()
5633                && !dyn_prefill
5634                && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5635                && std::env::var("CMF_O1_PREFIX_GPU").as_deref() != Ok("0")
5636                && (0..self.num_layers).all(|li| self.phys_layer(li) == li)
5637            {
5638                let t_px = std::time::Instant::now();
5639                match self.o1_prefix_on_graph(&input_ids[..limit], batch_k) {
5640                    Ok(Some(last)) => {
5641                        hidden.copy_from_slice(&last);
5642                        pos = limit;
5643                        if std::env::var("CMF_PREFILL_PROF").is_ok() {
5644                            eprintln!(
5645                                "o1 prefix on the batch graph: {limit} rows in {:.1} ms",
5646                                t_px.elapsed().as_secs_f64() * 1e3
5647                            );
5648                        }
5649                    }
5650                    Ok(None) => {}
5651                    Err(err) => {
5652                        self.finish_generation(&mut mtp, &mut router, true);
5653                        return Err(err);
5654                    }
5655                }
5656            }
5657            // Reuse the exact batched prefix machinery when available; it
5658            // records the same per-position Q trace as the full prefill.
5659            if pos < limit && self.can_prefill_batched() && limit > 2 {
5660                let chunk = self.prefill_chunk();
5661                let hs = self.hidden_size;
5662                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5663                    let end = (pos + chunk).min(limit);
5664                    let hb = self.prefill_batch(&input_ids[pos..end], pos);
5665                    hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5666                    pos = end;
5667                }
5668            } else {
5669                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5670                    hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5671                    pos += 1;
5672                }
5673            }
5674            if pos >= limit {
5675                o1_sealed = match self.o1_seal_checked() {
5676                    Ok(sealed) => sealed,
5677                    Err(err) => {
5678                        self.finish_generation(&mut mtp, &mut router, true);
5679                        return Err(err);
5680                    }
5681                };
5682                tracing::info!(
5683                    "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5684                    o1_prefill.unwrap_or(0),
5685                    self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5686                        .unwrap_or(limit),
5687                    limit,
5688                    input_ids.len()
5689                );
5690            }
5691        }
5692        // q1 hybrids on Metal: the per-position GPU token graph beats
5693        // the CPU chunk-GEMM (whose wall is the sequential scalar GDN
5694        // recurrence), so prefill goes position-by-position through the
5695        // same graph as decode. Pure-attention models keep the batched
5696        // path — there the chunk-GEMM amortization wins.
5697        let graph_prefill = self.graph_prefill_preferred();
5698        // Native Metal, q4tp GDN hybrids: the prompt through the b-row
5699        // rows graph — projections as GEMMs over up to 512 positions, the
5700        // GDN recurrence in registers on the device, K/V rows appended by
5701        // the chunk — instead of one token-graph submit per position (the
5702        // 27B: 8 tok/s → GEMM-bound). The MTP warm-up rows come out of one
5703        // batched run of the block per chunk. Any refusal leaves the rest
5704        // of the prompt to the sequential paths below.
5705        #[cfg(target_os = "macos")]
5706        if task_mask.is_none()
5707            && !dyn_prefill
5708            && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5709            && crate::gpu::enabled_here()
5710            && self.gdn_cfg.is_some()
5711            && !self.per_token_stack()
5712            && input_ids.len() > 8
5713            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5714            && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5715        {
5716            let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5717                .ok()
5718                .and_then(|v| v.parse().ok())
5719                .filter(|&v| (16..=512).contains(&v))
5720                .unwrap_or(256);
5721            let hs = self.hidden_size;
5722            let _tp = std::time::Instant::now();
5723            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5724                let end = (pos + chunk).min(input_ids.len());
5725                let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5726                    MetalPrefillOutcome::Completed(hb) => hb,
5727                    MetalPrefillOutcome::Declined => break,
5728                    MetalPrefillOutcome::Failed => {
5729                        self.finish_generation(&mut mtp, &mut router, true);
5730                        return Err("ordinary Metal prefill failed after admission".into());
5731                    }
5732                };
5733                if let Some(m) = &mut mtp {
5734                    let n_pairs = if end < input_ids.len() {
5735                        end - pos
5736                    } else {
5737                        end - pos - 1
5738                    };
5739                    if n_pairs > 0 {
5740                        let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5741                            .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5742                            .collect();
5743                        if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5744                            for (j, (h, t)) in pairs.iter().enumerate() {
5745                                let h = h.to_vec();
5746                                let _ = self.mtp_step(m, &h, *t, pos + j);
5747                            }
5748                        }
5749                    }
5750                }
5751                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5752                pos = end;
5753            }
5754            if std::env::var("CMF_PREFILL_PROF").is_ok() {
5755                eprintln!(
5756                    "metal-prefill: {} of {} tokens in {:.1} ms",
5757                    pos,
5758                    input_ids.len(),
5759                    _tp.elapsed().as_secs_f64() * 1e3
5760                );
5761            }
5762        }
5763        // Gemma-4 on a discrete card: the prompt through the batched wgpu
5764        // graph (`g4_batch_ingest`); its K/V land in the device mirrors
5765        // decode reads. A refusal leaves the rest of the prompt to the
5766        // chunked host prefill below.
5767        #[cfg(not(target_os = "macos"))]
5768        if task_mask.is_none()
5769            && !dyn_prefill
5770            && mtp.is_none()
5771            && o1_prefill.is_none()
5772            && batch_k > 0
5773            && pos + 1 < input_ids.len()
5774            && self.g4_batch_prefill()
5775        {
5776            let t_g4 = std::time::Instant::now();
5777            match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5778                Ok((n, last)) => {
5779                    if n > 0 {
5780                        hidden.copy_from_slice(&last);
5781                    }
5782                    if std::env::var("CMF_PREFILL_PROF").is_ok() {
5783                        eprintln!(
5784                            "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5785                            input_ids.len() - pos,
5786                            t_g4.elapsed().as_secs_f64() * 1e3
5787                        );
5788                    }
5789                    pos += n;
5790                }
5791                Err(e) => {
5792                    self.finish_generation(&mut mtp, &mut router, true);
5793                    return Err(e);
5794                }
5795            }
5796        }
5797        self.mimo_moe_prepare();
5798        // A MoE stack larger than the card (MiMo-V2 q4tp on 96 GB): the
5799        // batched wgpu graph runs the device prefix of every chunk — its
5800        // experts resident — and the host's batched layer walk finishes
5801        // the chunk. Any refusal leaves the rest of the prompt to the
5802        // chunked prefill below.
5803        #[cfg(not(target_os = "macos"))]
5804        if task_mask.is_none()
5805            && !dyn_prefill
5806            && !graph_prefill
5807            && mtp.is_none()
5808            && o1_prefill.is_none()
5809            && !self.o1_active()
5810            && input_ids.len() > 2
5811            && self.batch_prefix_prefill()
5812        {
5813            let chunk = self.prefill_chunk().max(1);
5814            let hs = self.hidden_size;
5815            let t_bp = std::time::Instant::now();
5816            let pos0 = pos;
5817            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5818                let end = (pos + chunk).min(input_ids.len());
5819                let bk = end - pos;
5820                let mut hiddens = vec![0f32; bk * hs];
5821                for (j, &id) in input_ids[pos..end].iter().enumerate() {
5822                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5823                }
5824                let positions: Vec<usize> = (pos..end).collect();
5825                let mut run = 0usize;
5826                let outcome = self.try_batch_graph_wgpu_prefix(
5827                    &mut hiddens,
5828                    &positions,
5829                    bk,
5830                    None,
5831                    Some(&mut run),
5832                );
5833                match outcome {
5834                    crate::gpu::BatchGraphOutcome::Completed => {
5835                        let hb = if run < self.num_layers {
5836                            self.prefill_batch_span(
5837                                PrefillIn::Hidden(&hiddens),
5838                                pos,
5839                                None,
5840                                run,
5841                                self.num_layers,
5842                            )
5843                        } else {
5844                            hiddens
5845                        };
5846                        if mimo_spec {
5847                            self.mimo_note_rows(&hb, pos);
5848                        }
5849                        hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5850                        pos = end;
5851                    }
5852                    crate::gpu::BatchGraphOutcome::Failed => {
5853                        self.finish_generation(&mut mtp, &mut router, true);
5854                        return Err("batched prefix prefill failed after admission".into());
5855                    }
5856                    crate::gpu::BatchGraphOutcome::Declined => {
5857                        // Earlier chunks left their prefix rows on the
5858                        // device only: the host walk below needs them.
5859                        #[cfg(feature = "gpu")]
5860                        if pos > pos0 {
5861                            self.pull_lagging_host_kv(0, self.num_layers, pos);
5862                        }
5863                        break;
5864                    }
5865                }
5866            }
5867            if std::env::var("CMF_PREFILL_PROF").is_ok() {
5868                eprintln!(
5869                    "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5870                    pos - pos0,
5871                    input_ids.len(),
5872                    t_bp.elapsed().as_secs_f64() * 1e3
5873                );
5874            }
5875        }
5876        if task_mask.is_none()
5877            && !dyn_prefill
5878            && !graph_prefill
5879            && self.can_prefill_batched()
5880            && !self.per_token_stack()
5881            && o1_prefill.is_none()
5882            && input_ids.len() > 2
5883        {
5884            // Production prefill = the same chunked prefill-GEMM that
5885            // bench/PPL measure (roadmap §3 P0: generation used to warm
5886            // the prompt with the slower pair path — the published
5887            // prefill number didn't match real TTFT). MTP warm-up reads
5888            // each position's hidden straight from the chunk result.
5889            let chunk = self.prefill_chunk();
5890            let hs = self.hidden_size;
5891            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5892                let end = (pos + chunk).min(input_ids.len());
5893                let hb = self.prefill_batch(&input_ids[pos..end], pos);
5894                if mimo_spec {
5895                    self.mimo_note_rows(&hb, pos);
5896                }
5897                if let Some(m) = &mut mtp {
5898                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5899                        .ok()
5900                        .and_then(|v| v.parse().ok())
5901                        .unwrap_or(0);
5902                    for p in pos..end {
5903                        if p + 1 < input_ids.len() {
5904                            if probe >= 1 && p + 2 < input_ids.len() {
5905                                // Teacher-forced chain acceptance (see the
5906                                // tail loop's twin): the warm-up row stays,
5907                                // the chain's rows roll back.
5908                                let (d1, mut hx) = self.mtp_step_h(
5909                                    m,
5910                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
5911                                    input_ids[p + 1],
5912                                    p,
5913                                );
5914                                let mut ok = d1 == input_ids[p + 2];
5915                                Self::chain_probe_note(0, ok);
5916                                let mut d_prev = d1;
5917                                let mut extra = 0usize;
5918                                for j in 1..probe {
5919                                    if p + 2 + j >= input_ids.len() {
5920                                        break;
5921                                    }
5922                                    let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5923                                    extra += 1;
5924                                    ok = ok && dj == input_ids[p + 2 + j];
5925                                    Self::chain_probe_note(j, ok);
5926                                    d_prev = dj;
5927                                    hx = hj;
5928                                }
5929                                m.kv.truncate_last(extra);
5930                            } else {
5931                                let _ = self.mtp_step(
5932                                    m,
5933                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
5934                                    input_ids[p + 1],
5935                                    p,
5936                                );
5937                            }
5938                        }
5939                    }
5940                }
5941                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5942                pos = end;
5943            }
5944        }
5945        let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5946        if task_mask.is_none()
5947            && !dyn_prefill
5948            && !graph_prefill
5949            && !pair_off
5950            && self.pair_supported()
5951            && o1_prefill.is_none()
5952        {
5953            while pos + 1 < input_ids.len()
5954                && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5955            {
5956                let e1 = self.embed_single(input_ids[pos]);
5957                let e2 = self.embed_single(input_ids[pos + 1]);
5958                let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5959                if mimo_spec {
5960                    self.mimo_note_rows(&h1, pos);
5961                    self.mimo_note_rows(&h2, pos + 1);
5962                }
5963                // Both prefill tokens are real → commit lane-2 states.
5964                self.commit_linear_scratch();
5965                if let Some(m) = &mut mtp {
5966                    let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5967                    if pos + 2 < input_ids.len() {
5968                        let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5969                            .ok()
5970                            .and_then(|v| v.parse().ok())
5971                            .unwrap_or(0);
5972                        if probe >= 1 && pos + 3 < input_ids.len() {
5973                            // Same teacher-forced chain table as the tail
5974                            // loop below, fed from the pair path that owns
5975                            // most prefill positions.
5976                            let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5977                            let mut ok = d1 == input_ids[pos + 3];
5978                            Self::chain_probe_note(0, ok);
5979                            let mut d_prev = d1;
5980                            let mut extra = 0usize;
5981                            for j in 1..probe {
5982                                if pos + 3 + j >= input_ids.len() {
5983                                    break;
5984                                }
5985                                let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5986                                extra += 1;
5987                                ok = ok && dj == input_ids[pos + 3 + j];
5988                                Self::chain_probe_note(j, ok);
5989                                d_prev = dj;
5990                                hx = hj;
5991                            }
5992                            m.kv.truncate_last(extra);
5993                        } else {
5994                            let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5995                        }
5996                    }
5997                }
5998                hidden = h2;
5999                pos += 2;
6000            }
6001        }
6002        // Batched GPU prefill for the wgpu decode graph (GDN hybrids): K prompt
6003        // positions per submit — projections/FFN as GEMMs (weight once per K),
6004        // attention/GDN looped inside — instead of one whole-graph submit per
6005        // position. Falls through to the per-position graph on any refusal.
6006        // Batched prefill is opt-in (CMF_BATCH_K>0). Default 0 = per-position
6007        // graph prefill. (Steady-state decode is provably identical either way —
6008        // token-graph submit and lm_head both unchanged — so this only trades
6009        // prefill wall.)
6010        // A bounded O(1) prefix is the one post-seal prompt interval: only
6011        // admit its batch when the device O(1) route is explicitly enabled and
6012        // every sealed layer exposes a portable view. The same batch size and
6013        // refusal behavior remain the ordinary controls/comparator.
6014        let o1_batch_ready = o1_sealed
6015            && o1_prefill.is_some()
6016            && mtp.is_none()
6017            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6018            && (0..self.num_layers).all(|li| {
6019                let cache = &self.kv_cache.layers[self.phys_layer(li)];
6020                cache.o1.is_none() || cache.o1_views().is_some()
6021            });
6022        // The ordinary graph-prefill route can share each completed trunk
6023        // chunk with an attached MTP head.  Keep chain probing on its
6024        // established per-position path: the probe deliberately needs every
6025        // teacher-forced draft row and its rollback table.
6026        let mtp_batch_prefill = mtp.is_some()
6027            && graph_prefill
6028            && task_mask.is_none()
6029            && !dyn_prefill
6030            && !self.o1_active()
6031            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
6032        if batch_k > 0
6033            && (graph_prefill || o1_batch_ready)
6034            && task_mask.is_none()
6035            && (!self.o1_active() || o1_batch_ready)
6036            && (mtp.is_none() || mtp_batch_prefill)
6037            && !dyn_prefill
6038            && pos + 1 < input_ids.len()
6039        {
6040            let hs = self.hidden_size;
6041            let chunk = batch_k;
6042            while pos < input_ids.len() {
6043                let end = (pos + chunk).min(input_ids.len());
6044                let bk = end - pos;
6045                let mut hiddens = vec![0f32; bk * hs];
6046                for (j, &id) in input_ids[pos..end].iter().enumerate() {
6047                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
6048                }
6049                let positions: Vec<usize> = (pos..end).collect();
6050                let t_chunk = std::time::Instant::now();
6051                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6052                let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
6053                if std::env::var("CMF_GRAPH_PROF").is_ok() {
6054                    let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
6055                    eprintln!(
6056                        "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6057                        if o1_batch_ready {
6058                            "o1"
6059                        } else if mtp_batch_prefill {
6060                            "ordinary_mtp"
6061                        } else {
6062                            "ordinary"
6063                        },
6064                        bk as f64 / (ms / 1000.0)
6065                    );
6066                }
6067                {
6068                    use std::sync::atomic::{AtomicBool, Ordering};
6069                    static SAID: AtomicBool = AtomicBool::new(false);
6070                    if !SAID.swap(true, Ordering::Relaxed) {
6071                        if ok_b {
6072                            tracing::info!(
6073                                "batched prefill: ACTIVE mode={} (k={bk})",
6074                                if o1_batch_ready {
6075                                    "o1"
6076                                } else if mtp_batch_prefill {
6077                                    "ordinary_mtp"
6078                                } else {
6079                                    "ordinary"
6080                                }
6081                            );
6082                        } else {
6083                            tracing::warn!("batched prefill {:?} — per-position graph", outcome);
6084                        }
6085                    }
6086                }
6087                if ok_b {
6088                    if mimo_spec {
6089                        self.mimo_note_rows(&hiddens, pos);
6090                    }
6091                    if mtp_batch_prefill {
6092                        let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
6093                        if n_pairs > 0 {
6094                            // `hiddens` is owned by this chunk, so materialize
6095                            // row slices before borrowing the detached MTP
6096                            // module.  The last prompt row has no successor;
6097                            // the helper above is the single source of that
6098                            // boundary rule.
6099                            let rows: Vec<Vec<f32>> = (0..n_pairs)
6100                                .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
6101                                .collect();
6102                            let pairs: Vec<(&[f32], u32)> = rows
6103                                .iter()
6104                                .enumerate()
6105                                .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
6106                                .collect();
6107                            if std::env::var("CMF_GRAPH_PROF").is_ok() {
6108                                eprintln!(
6109                                    "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
6110                                    pos,
6111                                    n_pairs,
6112                                    pos + n_pairs - 1,
6113                                );
6114                            }
6115                            let warm_error = if let Some(m) = mtp.as_mut() {
6116                                self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
6117                            } else {
6118                                None
6119                            };
6120                            if let Some(err) = warm_error {
6121                                // The trunk batch was already admitted.  A
6122                                // failed MTP warm-up therefore clears both
6123                                // mirrors and exits; continuing would pair a
6124                                // current trunk state with a stale MTP cache.
6125                                self.finish_generation(&mut mtp, &mut router, true);
6126                                return Err(err.to_string());
6127                            }
6128                        }
6129                    }
6130                    hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
6131                    pos = end;
6132                } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
6133                    // A failed batch may have advanced a device recurrent
6134                    // state (ordinary GDN or sealed O(1)). A CPU fallback
6135                    // would then observe stale accumulators, so clear the
6136                    // request state and make the failure explicit.
6137                    self.finish_generation(&mut mtp, &mut router, true);
6138                    return Err(if o1_batch_ready {
6139                        "sealed O(1) batch graph failed after admission".to_string()
6140                    } else {
6141                        "ordinary recurrent batch graph failed after admission".to_string()
6142                    });
6143                } else {
6144                    break; // unsupported → per-position graph handles the rest
6145                }
6146            }
6147        }
6148        // Resident Embryo graph: the prompt in chunks of one submit each
6149        // instead of one whole-graph submit per position; the last chunk
6150        // carries the logits exactly as the per-position walk would.
6151        if graph_prefill
6152            && task_mask.is_none()
6153            && mtp.is_none()
6154            && !dyn_prefill
6155            && pos == 0
6156            && input_ids.len() > 1
6157            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
6158        {
6159            if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
6160                self.graph_logits = Some(lg);
6161                hidden = vec![0.0; self.hidden_size];
6162                pos = input_ids.len();
6163            }
6164        }
6165        while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
6166            self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
6167            hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
6168            if mimo_spec {
6169                self.mimo_note_rows(&hidden, pos);
6170            }
6171            if let Some(m) = &mut mtp {
6172                if pos + 1 < input_ids.len() {
6173                    // `CMF_MTP_CHAIN_PROBE=k`: teacher-forced acceptance of a
6174                    // CHAINED draft — iterate the head on its own hidden k
6175                    // deep and score every depth against the prompt's real
6176                    // continuation. The economics of a k-token speculative
6177                    // round stand or fall on this table.
6178                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
6179                        .ok()
6180                        .and_then(|v| v.parse().ok())
6181                        .unwrap_or(0);
6182                    if probe >= 1 && pos + 2 < input_ids.len() {
6183                        let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
6184                        let mut ok = d1 == input_ids[pos + 2];
6185                        Self::chain_probe_note(0, ok);
6186                        let mut d_prev = d1;
6187                        let mut extra = 0usize;
6188                        for j in 1..probe {
6189                            if pos + 2 + j >= input_ids.len() {
6190                                break;
6191                            }
6192                            let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
6193                            extra += 1;
6194                            ok = ok && dj == input_ids[pos + 2 + j];
6195                            Self::chain_probe_note(j, ok);
6196                            d_prev = dj;
6197                            hx = hj;
6198                        }
6199                        // The chain's rows are speculation, not the prompt —
6200                        // keep only the warmup row the plain path would add.
6201                        m.kv.truncate_last(extra);
6202                    } else {
6203                        let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
6204                    }
6205                }
6206            }
6207            pos += 1;
6208        }
6209        if std::env::var("CMF_PREFILL_PROF").is_ok() {
6210            eprintln!(
6211                "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
6212                input_ids.len(),
6213                _tpf.elapsed().as_secs_f64() * 1000.0
6214            );
6215        }
6216        if self
6217            .graph_failed
6218            .swap(false, std::sync::atomic::Ordering::Relaxed)
6219        {
6220            // MTP is detached for speculative generation.  Restore the
6221            // module before returning the terminal graph error; otherwise a
6222            // failed request would silently remove the head from a pooled
6223            // pipeline and the next request would lose its configured route.
6224            self.finish_generation(&mut mtp, &mut router, true);
6225            return Err("GPU token graph failed during prefill".to_string());
6226        }
6227        // Cancelled mid-prefill: the cache holds a partial prompt —
6228        // drop the reuse history and return an empty generation.
6229        if self
6230            .cancel
6231            .swap(false, std::sync::atomic::Ordering::Relaxed)
6232        {
6233            // A cancelled prefill can already have advanced the device
6234            // mirror. Drop the whole partial sequence so a pooled pipeline
6235            // cannot carry that state into its next request.
6236            self.finish_generation(&mut mtp, &mut router, true);
6237            return Ok(GenerateResult {
6238                text: String::new(),
6239                token_ids: Vec::new(),
6240                prompt_tokens: input_ids.len(),
6241                tokens_generated: 0,
6242                finish_reason: "cancelled".to_string(),
6243                mtp_drafted: 0,
6244                mtp_accepted: 0,
6245                token_confidence: Vec::new(),
6246                traces: Vec::new(),
6247            });
6248        }
6249
6250        // Prompt absorbed → freeze the o1 layers' skeletons; from here
6251        // every decode step on those layers is O(W + m·dv + m²).
6252        if !o1_sealed {
6253            match self.o1_seal_checked() {
6254                Ok(_) => {}
6255                Err(err) => {
6256                    self.finish_generation(&mut mtp, &mut router, true);
6257                    return Err(err);
6258                }
6259            }
6260        }
6261
6262        // Commit one token: push, check EOS, stream. Returns false = stop.
6263        macro_rules! commit {
6264            ($id:expr) => {{
6265                all_ids.push($id);
6266                generated += 1;
6267                self.note_draft_id($id);
6268                if self.tokenizer.is_eos($id) && !self.ignore_eos {
6269                    finish_reason = "stop".to_string();
6270                    false
6271                } else {
6272                    let token_text = self.tokenizer.decode_token($id);
6273                    let mut go = true;
6274                    if let Some(ref mut cb) = on_token {
6275                        if !cb(&token_text) {
6276                            finish_reason = "cancelled".to_string();
6277                            go = false;
6278                        }
6279                    }
6280                    go
6281                }
6282            }};
6283        }
6284
6285        // Speculation is decided by MEASUREMENT, not by an acceptance
6286        // model. A k=4 round costs ~3.8 plain tokens on the 5090 (draft
6287        // 6.6 + verify 66.6 + commit 4.8 ms against a 20.6 ms token), so it
6288        // pays only when the head lands ~2.8 of 4 — predictable text (code,
6289        // structured output) does, free prose often does not, and the
6290        // ratio at which the two cross depends on the card and the context
6291        // depth. So: four speculative rounds timed, then eight plain
6292        // tokens timed, and the faster arm runs until a re-check 256
6293        // tokens later (context growth moves the balance). The trial
6294        // costs at most a few tokens of the slower arm per 256.
6295        let mut spec_trial = SpecTrial::Spec {
6296            t0: std::time::Instant::now(),
6297            gen0: generated,
6298            rounds: 0,
6299        };
6300        // The token-count proxy prices a round at ~1.9 plain tokens. That
6301        // holds for the Metal rounds whose cost was measured — greedy and
6302        // the sparse sampling chain — so an expensive round (the dense
6303        // chain, reachable only by `CMF_GRAPH_SPEC_SAMPLE=1`) still times
6304        // the plain path before it decides.
6305        let mut spec_mon = SpecMon {
6306            metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
6307            ..SpecMon::default()
6308        };
6309        let mut spec_watchdog_off = false;
6310        // CMF_GRAPH_SPEC_TIME: the round walls so far (round 1 excluded —
6311        // it pays the scratch), for the outlier test on each new one
6312        let mut spec_walls: Vec<f32> = Vec::new();
6313        // ... and the end of the last round: the host time between rounds
6314        // (token commits, streaming, the loop top) is printed at level 2
6315        let mut spec_round_end: Option<std::time::Instant> = None;
6316        if mimo_spec {
6317            if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
6318                if let Some(mut st) = self.mimo_mtp.take() {
6319                    self.mimo_mtp_probe(&mut st, input_ids, &path);
6320                    self.mimo_mtp = Some(st);
6321                }
6322            }
6323        }
6324        // ── Decode ──
6325        let mut next_pos = input_ids.len();
6326        'decode: while generated < max_tokens {
6327            if self
6328                .graph_failed
6329                .swap(false, std::sync::atomic::Ordering::Relaxed)
6330            {
6331                // Keep the detached MTP module attached after a terminal
6332                // graph error so the pipeline can be reused for a fresh
6333                // sequence.  `clear_sequence_state` only clears mirrors and
6334                // host KV; it cannot recover a module dropped here.
6335                self.finish_generation(&mut mtp, &mut router, true);
6336                return Err("GPU token graph failed during decode".to_string());
6337            }
6338            if self
6339                .cancel
6340                .swap(false, std::sync::atomic::Ordering::Relaxed)
6341            {
6342                finish_reason = "cancelled".to_string();
6343                break 'decode;
6344            }
6345            // A rejected speculative draft already drew this position's
6346            // token from the residual distribution (graph_spec_step); it
6347            // is committed as-is — sampling again from the row's logits
6348            // would bias the stream toward the target's mode.
6349            if mimo_spec && next_pos > 0 {
6350                // Every path leaves `hidden` = the backbone output at
6351                // next_pos-1; the draft layers read it (idempotent).
6352                self.mimo_note_rows(&hidden, next_pos - 1);
6353            }
6354            let forced = self.spec_forced.take();
6355            let mut logits = match (forced, self.graph_logits.take()) {
6356                (Some(_), _) => Vec::new(),
6357                (None, Some(lg)) => lg,
6358                (None, None) => {
6359                    let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
6360                    // Pooled-tail mode: the head as one layer-less device
6361                    // pass (final-norm + matvec + readback). Any decline —
6362                    // no pool, unresolvable weight, backend off — keeps the
6363                    // host head exactly as before.
6364                    #[cfg(feature = "gpu")]
6365                    let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
6366                        lg
6367                    } else {
6368                        inference::rms_norm_into(
6369                            &hidden,
6370                            &self.weights.final_norm,
6371                            self.rms_eps,
6372                            self.norm_style,
6373                            &mut self.ws.n1,
6374                        );
6375                        self.lm_head_forward(&self.ws.n1)
6376                    };
6377                    #[cfg(not(feature = "gpu"))]
6378                    let logits_out = {
6379                        inference::rms_norm_into(
6380                            &hidden,
6381                            &self.weights.final_norm,
6382                            self.rms_eps,
6383                            self.norm_style,
6384                            &mut self.ws.n1,
6385                        );
6386                        self.lm_head_forward(&self.ws.n1)
6387                    };
6388                    logits_out
6389                }
6390            };
6391            // CMF_LOGIT_DUMP=<path>: the first decode step's hidden + logits
6392            // as raw f32 (hidden first) — cross-backend numerics diffing.
6393            if generated
6394                == std::env::var("CMF_LOGIT_DUMP_STEP")
6395                    .ok()
6396                    .and_then(|v| v.parse().ok())
6397                    .unwrap_or(0)
6398            {
6399                if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
6400                    let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
6401                    for v in hidden.iter().chain(logits.iter()) {
6402                        bytes.extend_from_slice(&v.to_le_bytes());
6403                    }
6404                    if let Err(e) = std::fs::write(&path, &bytes) {
6405                        eprintln!("logit dump: failed to write {path}: {e}");
6406                        self.finish_generation(&mut mtp, &mut router, true);
6407                        return Err(format!("logit dump write failed: {e}"));
6408                    }
6409                }
6410            }
6411            // CMF_LOGIT_DUMP_ALL=<dir>: every decode step's logits as raw
6412            // f32, `<dir>/step{n:05}.f32` — step-by-step backend diffing
6413            // (a greedy run on two backends compares until they diverge).
6414            if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
6415                if !logits.is_empty() {
6416                    let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
6417                    let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
6418                    if let Err(e) =
6419                        std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
6420                    {
6421                        eprintln!("logit dump: failed to write {}: {e}", path.display());
6422                    }
6423                }
6424            }
6425            let t_next = match forced.or_else(|| forced_ids_at(generated)) {
6426                Some(c) => c,
6427                None => {
6428                    let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
6429                    sampler::sample_with_scratch_pool(
6430                        &logits,
6431                        &self.sampler_config,
6432                        self.sampler_config.penalty_past(&all_ids, bounded_native),
6433                        &mut self.rng,
6434                        &mut self.sampler_scratch,
6435                        self.pool.as_deref(),
6436                    )
6437                }
6438            };
6439            if self.confidence_on {
6440                confidence.push(if logits.is_empty() {
6441                    0.0
6442                } else {
6443                    sampler::top1_prob_pool(
6444                        self.pool.as_deref(),
6445                        &mut self.sampler_scratch,
6446                        &logits,
6447                        t_next,
6448                        calib_temp,
6449                    )
6450                });
6451            }
6452            if !logits.is_empty() {
6453                attention::recycle_buf(&mut logits);
6454            }
6455            if trace_on {
6456                // active_skill = the overlay in force while this token was
6457                // generated; recon/switched are filled after the post-emit
6458                // routing eval below (freshest coherence for this token).
6459                let skill = router.as_ref().and_then(|r| r.active_id());
6460                traces.push(TokenTrace {
6461                    t: generated,
6462                    token_id: t_next,
6463                    confidence: confidence.last().copied().unwrap_or(0.0),
6464                    active_skill: skill,
6465                    recon: None,
6466                    switched: false,
6467                });
6468            }
6469            if !commit!(t_next) {
6470                break 'decode;
6471            }
6472            if generated >= max_tokens {
6473                break 'decode;
6474            }
6475
6476            // Catch-all for decode paths that bypass the walks' own trim
6477            // (a graph step, a speculative round).
6478            self.swa_trim_tails();
6479            if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
6480                // Say it ONCE, loudly: past this point the model keeps
6481                // talking but has lost half its context, and on a GDN
6482                // hybrid the graph's device state goes stale on top. The
6483                // Qwen3.8 bring-up spent a day reading this cliff as
6484                // three different model bugs.
6485                static SAID: std::sync::Once = std::sync::Once::new();
6486                SAID.call_once(|| {
6487                    tracing::warn!(
6488                        "KV cache full at {} positions — evicting half; quality \
6489                         will degrade. Raise CMF_MAX_SEQ.",
6490                        self.kv_cache.max_seq_len,
6491                    );
6492                });
6493                let keep = (self.kv_cache.max_seq_len / 2).max(1);
6494                self.kv_cache.evict(keep);
6495            }
6496
6497            // Advance the speculation trial: plain-phase accounting and
6498            // the periodic re-check happen here, on every token.
6499            if graph_spec {
6500                match spec_trial {
6501                    SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6502                        spec_mon.plain_ms =
6503                            t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6504                        let keep = spec_mon.pays();
6505                        tracing::info!(
6506                            "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6507                            spec_mon.tokens,
6508                            spec_mon.round_ms,
6509                            spec_mon.plain_ms,
6510                            if keep { "speculating" } else { "plain" }
6511                        );
6512                        spec_mon.fails = 0;
6513                        spec_trial = SpecTrial::Decided {
6514                            spec: keep,
6515                            recheck_at: if keep { usize::MAX } else { generated + 128 },
6516                        };
6517                    }
6518                    SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6519                        spec_mon.n = 0;
6520                        spec_mon.zero_accept_rounds = 0;
6521                        spec_trial = SpecTrial::Spec {
6522                            t0: std::time::Instant::now(),
6523                            gen0: generated,
6524                            rounds: 0,
6525                        };
6526                    }
6527                    _ => {}
6528                }
6529                spec_watchdog_off = matches!(
6530                    spec_trial,
6531                    SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6532                );
6533            }
6534            // ── MiMo-V2 draft stack: draft K, verify K+1 rows in one batch ──
6535            if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6536                let budget = max_tokens - generated - 1;
6537                if let Some(mut st) = self.mimo_mtp.take() {
6538                    let k = st.depth.min(budget);
6539                    let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6540                    self.mimo_mtp = Some(st);
6541                    let r = match r {
6542                        Ok(r) => r,
6543                        Err(err) => {
6544                            self.finish_generation(&mut mtp, &mut router, true);
6545                            return Err(err);
6546                        }
6547                    };
6548                    if let Some(r) = r {
6549                        drafted += r.drafted;
6550                        accepted += r.accepted.len();
6551                        let mut stopped = false;
6552                        for &id in &r.accepted {
6553                            if self.confidence_on {
6554                                confidence.push(0.0);
6555                            }
6556                            if !commit!(id) {
6557                                stopped = true;
6558                                break;
6559                            }
6560                        }
6561                        if stopped {
6562                            break 'decode;
6563                        }
6564                        next_pos += r.accepted.len() + 1;
6565                        hidden = r.hidden;
6566                        // The loop top chooses the round's own token from
6567                        // these logits — the same sampler, same history.
6568                        self.graph_logits = Some(r.logits);
6569                        continue 'decode;
6570                    }
6571                }
6572            }
6573            // ── Qwen3.8-Flash-Next draft head: k greedy drafts from the MTP
6574            //    sidecar, one batched verify window on the device ──
6575            #[cfg(feature = "gpu")]
6576            if self.speculative
6577                && self.qwen4_exp.is_some()
6578                && task_mask.is_none()
6579                && self.sampler_config.temperature < 1e-6
6580                && generated + 1 < max_tokens
6581                && next_pos > 0
6582                && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6583            {
6584                let r = match &mut self.qwen4_exp {
6585                    Some(b) => crate::qwen4_exp::spec_round(
6586                        &b.0,
6587                        &b.1,
6588                        &b.2,
6589                        &mut b.3,
6590                        next_pos,
6591                        &all_ids,
6592                        &self.inv_freq,
6593                        self.pool.as_deref(),
6594                    ),
6595                    None => None,
6596                };
6597                if let Some(r) = r {
6598                    drafted += r.drafted;
6599                    accepted += r.accepted.len();
6600                    let mut stopped = false;
6601                    for &id in &r.accepted {
6602                        if self.confidence_on {
6603                            confidence.push(0.0);
6604                        }
6605                        if !commit!(id) {
6606                            stopped = true;
6607                            break;
6608                        }
6609                    }
6610                    if stopped {
6611                        break 'decode;
6612                    }
6613                    next_pos += r.accepted.len() + 1;
6614                    hidden.fill(0.0);
6615                    self.graph_logits = Some(r.logits);
6616                    continue 'decode;
6617                }
6618            }
6619            match &mut mtp {
6620                // ── Graph speculation: chain-draft, batch-verify on device ──
6621                #[cfg(feature = "gpu")]
6622                Some(m)
6623                    if graph_spec
6624                        && !spec_watchdog_off
6625                        && generated + 1 < max_tokens
6626                        && next_pos > 0 =>
6627                {
6628                    let t_round = std::time::Instant::now();
6629                    if spec_time_level() >= 2 {
6630                        if let Some(t) = spec_round_end.take() {
6631                            eprintln!(
6632                                "spec-gap {:.2} ms (host between rounds)",
6633                                t.elapsed().as_secs_f64() * 1e3
6634                            );
6635                        }
6636                    }
6637                    spec_stamps_begin();
6638                    // device buffers allocated during this round: a
6639                    // first-touch Shared allocation is zero-filled inside
6640                    // the command buffer that uses it, which is what the
6641                    // long outlier rounds were
6642                    #[cfg(target_os = "macos")]
6643                    let allocs0 =
6644                        crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6645                    #[cfg(not(target_os = "macos"))]
6646                    let allocs0 = 0u64;
6647                    if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6648                        m,
6649                        &hidden,
6650                        t_next,
6651                        next_pos,
6652                        &mut drafted,
6653                        &mut accepted,
6654                        &mut all_ids,
6655                        max_tokens - generated,
6656                    ) {
6657                        next_pos = n_pos;
6658                        hidden = new_h;
6659                        let level = spec_time_level();
6660                        if level > 0 {
6661                            let wall = t_round.elapsed().as_secs_f32() * 1e3;
6662                            let stamps = spec_stamps_take();
6663                            // the running median of the rounds before this
6664                            // one (round 1 pays the scratch: not a sample)
6665                            let median = if spec_walls.len() >= 3 {
6666                                let mut s = spec_walls.clone();
6667                                s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6668                                Some(s[s.len() / 2])
6669                            } else {
6670                                None
6671                            };
6672                            let outlier = median.is_some_and(|m| wall > 1.4 * m);
6673                            #[cfg(target_os = "macos")]
6674                            let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6675                                .load(std::sync::atomic::Ordering::Relaxed)
6676                                - allocs0;
6677                            #[cfg(not(target_os = "macos"))]
6678                            let allocs = allocs0;
6679                            eprintln!(
6680                                "spec-round wall {wall:.1} ms → {} tokens{}{}",
6681                                extra.len() + 1,
6682                                if allocs > 0 {
6683                                    format!(" [{allocs} new device buffers]")
6684                                } else {
6685                                    String::new()
6686                                },
6687                                match (outlier, median) {
6688                                    (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6689                                    _ => String::new(),
6690                                }
6691                            );
6692                            if level >= 2 || outlier {
6693                                let sum: f32 = stamps.iter().map(|s| s.1).sum();
6694                                eprintln!(
6695                                    "spec-stamps: {}| untracked {:.1}",
6696                                    spec_stamps_format(&stamps),
6697                                    wall - sum
6698                                );
6699                            }
6700                            if spec_mon.n >= 1 {
6701                                spec_walls.push(wall);
6702                            }
6703                        }
6704                        // One speculative round done: the monitor counts it
6705                        // (round 1 untimed — it pays the batch scratch and
6706                        // the draft mirror), and the trial advances.
6707                        spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6708                        // the round's tokens land in `generated` below; the
6709                        // plain phase must start counting AFTER them
6710                        spec_trial = Self::spec_trial_round(
6711                            spec_trial,
6712                            &mut spec_mon,
6713                            generated + extra.len() + 1,
6714                        );
6715                        let mut stopped = false;
6716                        for &id in &extra {
6717                            if self.confidence_on {
6718                                confidence.push(0.0);
6719                            }
6720                            // `graph_spec_step` commits the accepted MTP
6721                            // drafts as a batch.  They do not pass through
6722                            // the ordinary loop-top sampling block below,
6723                            // which is normally where the telemetry row is
6724                            // created.  Keep the public trace contract
6725                            // (one row per emitted token) here as well.
6726                            //
6727                            // The graph-spec path does not surface a
6728                            // per-row probability for accepted extras.  It
6729                            // deliberately matches the zero placed in the
6730                            // confidence channel above instead of inventing
6731                            // one or adding a separate probability pass.
6732                            if trace_on {
6733                                traces.push(TokenTrace {
6734                                    t: generated,
6735                                    token_id: id,
6736                                    confidence: confidence.last().copied().unwrap_or(0.0),
6737                                    active_skill: None,
6738                                    recon: None,
6739                                    switched: false,
6740                                });
6741                            }
6742                            if !commit!(id) {
6743                                stopped = true;
6744                                break;
6745                            }
6746                        }
6747                        if stopped {
6748                            break 'decode;
6749                        }
6750                        if spec_time_level() >= 2 {
6751                            spec_round_end = Some(std::time::Instant::now());
6752                        }
6753                        continue 'decode;
6754                    }
6755                    if self
6756                        .graph_failed
6757                        .swap(false, std::sync::atomic::Ordering::Relaxed)
6758                    {
6759                        // `graph_spec_step` may have detached MTP while a
6760                        // warm-up was in flight.  Do not reinterpret its
6761                        // terminal device failure as a plain decode step;
6762                        // restore the head, clear both mirrors, and surface
6763                        // one explicit error to the caller.
6764                        self.finish_generation(&mut mtp, &mut router, true);
6765                        return Err("GPU MTP graph failed during speculative decode".to_string());
6766                    }
6767                    // Declined (batch graph refused): plain forward below —
6768                    // and a round that produced one token for the trial's
6769                    // ledger, so a graph that keeps refusing is measured out
6770                    // like a head that keeps missing (it was spinning
6771                    // forever on a file whose batch graph declines).
6772                    // A declined round is not a cheap one-token round — it
6773                    // is a verify that does not exist for this file (a
6774                    // healed q8_2f tail measured 760 drafts, 0 accepted, 33
6775                    // against 48.8 tok/s while the monitor called the draft
6776                    // alone "paying"). Count it as the losing streak in one.
6777                    spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6778                    spec_mon.tokens = 0.0;
6779                    spec_mon.fails = 3;
6780                    spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6781                    hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6782                    next_pos += 1;
6783                    continue 'decode;
6784                }
6785                // ── Speculative: draft t+2, verify in a fused pair ──
6786                Some(m) if !graph_spec && generated + 1 < max_tokens => {
6787                    let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6788                    drafted += 1;
6789                    let emb1 = self.embed_single(t_next);
6790                    let emb2 = self.embed_single(draft);
6791                    let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6792
6793                    inference::rms_norm_into(
6794                        &h1,
6795                        &self.weights.final_norm,
6796                        self.rms_eps,
6797                        self.norm_style,
6798                        &mut self.ws.n1,
6799                    );
6800                    let mut logits1 = self.lm_head_forward(&self.ws.n1);
6801                    let t_after = sampler::sample_with_scratch_pool(
6802                        &logits1,
6803                        &self.sampler_config,
6804                        self.sampler_config.penalty_past(&all_ids, bounded_native),
6805                        &mut self.rng,
6806                        &mut self.sampler_scratch,
6807                        self.pool.as_deref(),
6808                    );
6809                    if self.confidence_on {
6810                        confidence.push(sampler::top1_prob_pool(
6811                            self.pool.as_deref(),
6812                            &mut self.sampler_scratch,
6813                            &logits1,
6814                            t_after,
6815                            calib_temp,
6816                        ));
6817                    }
6818                    attention::recycle_buf(&mut logits1);
6819                    if trace_on {
6820                        // Speculative decode is mutually exclusive with
6821                        // dynamic routing (router is None here) — no skill.
6822                        traces.push(TokenTrace {
6823                            t: generated,
6824                            token_id: t_after,
6825                            confidence: confidence.last().copied().unwrap_or(0.0),
6826                            active_skill: None,
6827                            recon: None,
6828                            switched: false,
6829                        });
6830                    }
6831                    let stop = !commit!(t_after);
6832
6833                    if t_after == draft {
6834                        accepted += 1;
6835                        self.commit_linear_scratch();
6836                        let _ = self.mtp_step(m, &h1, t_after, next_pos);
6837                        hidden = h2;
6838                        next_pos += 2;
6839                    } else {
6840                        // The draft lane is wrong: roll its KV entry back.
6841                        for layer in &mut self.kv_cache.layers {
6842                            layer.truncate_last(1);
6843                        }
6844                        if !stop {
6845                            let _ = self.mtp_step(m, &h1, t_after, next_pos);
6846                            hidden = self.forward_layers(
6847                                &self.embed_single(t_after),
6848                                next_pos + 1,
6849                                None,
6850                            );
6851                        }
6852                        next_pos += 2;
6853                    }
6854                    if stop {
6855                        break 'decode;
6856                    }
6857                }
6858                // ── Vanilla: forward the sampled token ──
6859                _ => {
6860                    // ── DeepSeek-V4 speculative decode (CMF_DSV4_SPEC=1):
6861                    // draft five on the card, verify batched, commit the
6862                    // accepted prefix. Greedy only; a rejected token's state
6863                    // is restored and replayed, so output equals the walk. ──
6864                    #[cfg(feature = "gpu")]
6865                    if Self::dsv4_spec_on() && self.dsv4.is_some() {
6866                        static SAID: std::sync::Once = std::sync::Once::new();
6867                        SAID.call_once(|| {
6868                            eprintln!(
6869                                "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6870                                !self.dsv4_mtp.is_empty(),
6871                                task_mask.is_none(),
6872                                router.is_none(),
6873                                !trace_on,
6874                                self.sampler_config.temperature < 1e-6,
6875                                self.sampler_config.repetition_penalty == 1.0,
6876                            );
6877                        });
6878                    }
6879                    #[cfg(feature = "gpu")]
6880                    if Self::dsv4_spec_on()
6881                        && self.dsv4.is_some()
6882                        && !self.dsv4_mtp.is_empty()
6883                        && task_mask.is_none()
6884                        && router.is_none()
6885                        && !trace_on
6886                        && self.sampler_config.temperature < 1e-6
6887                        && self.sampler_config.repetition_penalty == 1.0
6888                        && generated + 1 < max_tokens
6889                        && all_ids.len() >= 2
6890                        && generated >= dsv4_spec_retry_at
6891                    {
6892                        let tip_token = all_ids[all_ids.len() - 2];
6893                        let drafted0 = drafted;
6894                        let round = self.dsv4_spec_step(
6895                            tip_token,
6896                            t_next,
6897                            next_pos,
6898                            max_tokens.saturating_sub(generated),
6899                            &mut drafted,
6900                            &mut accepted,
6901                        );
6902                        if drafted > drafted0 {
6903                            let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6904                            if useful {
6905                                dsv4_spec_bad = 0;
6906                            } else {
6907                                dsv4_spec_bad += 1;
6908                                if dsv4_spec_bad >= 2 {
6909                                    dsv4_spec_bad = 0;
6910                                    dsv4_spec_retry_at = generated.saturating_add(32);
6911                                    tracing::info!(
6912                                        "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6913                                    );
6914                                }
6915                            }
6916                        }
6917                        if let Some((extra, n_pos)) = round {
6918                            next_pos = n_pos;
6919                            let mut stopped = false;
6920                            for &id in &extra {
6921                                if self.confidence_on {
6922                                    confidence.push(0.0);
6923                                }
6924                                if !commit!(id) {
6925                                    stopped = true;
6926                                    break;
6927                                }
6928                            }
6929                            if stopped {
6930                                break 'decode;
6931                            }
6932                            continue 'decode;
6933                        }
6934                    }
6935                    self.graph_want_logits = fuse_lm;
6936                    // Greedy burst (CMF_MULTISTEP, off unless explicitly set): while
6937                    // nothing observes per-token state — pure argmax sampling,
6938                    // no router/trace/confidence/mask — decode k tokens per
6939                    // submit and commit them wholesale. The trailing normal
6940                    // forward leaves logits for the loop top, as always.
6941                    let mut t_fwd = t_next;
6942                    let pure_greedy = self.sampler_config.temperature < 1e-6
6943                        && self.sampler_config.repetition_penalty == 1.0
6944                        && self.sampler_config.presence_penalty == 0.0
6945                        && self.sampler_config.suppress_tokens.is_empty();
6946                    // Off by default: at every k the burst measured at or
6947                    // below the plain path on this graph shape (k=1 loses
6948                    // the argmax dispatches vs a 1 MB readback, k>=8 loses
6949                    // inter-step drains vs the saved sync). Experimental.
6950                    let burst_k = std::env::var("CMF_MULTISTEP")
6951                        .ok()
6952                        .and_then(|v| v.parse::<usize>().ok())
6953                        .unwrap_or(0);
6954                    if pure_greedy
6955                        && burst_k >= 1
6956                        && fuse_lm
6957                        && task_mask.is_none()
6958                        && router.is_none()
6959                        && !trace_on
6960                        && !self.confidence_on
6961                        // Diagnostics and teacher forcing observe every
6962                        // individual logit row.  A device-only burst returns
6963                        // only ids, so decline it rather than silently
6964                        // skipping requested evidence or forced ids.
6965                        && std::env::var_os("CMF_FORCE_IDS").is_none()
6966                        && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6967                        && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6968                    {
6969                        let mut stopped = false;
6970                        loop {
6971                            // The outer loop checks cancellation too, but a
6972                            // long run can stay in this burst loop for many
6973                            // submits.  Bound cancellation latency to one
6974                            // exact k-token frame.
6975                            if self
6976                                .cancel
6977                                .swap(false, std::sync::atomic::Ordering::Relaxed)
6978                            {
6979                                finish_reason = "cancelled".to_string();
6980                                stopped = true;
6981                                break;
6982                            }
6983                            let room = max_tokens.saturating_sub(generated);
6984                            if room <= 2 {
6985                                break;
6986                            }
6987                            let k = burst_k.min(room - 1);
6988                            if k < 1 {
6989                                break;
6990                            }
6991                            let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6992                                if self
6993                                    .graph_failed
6994                                    .swap(false, std::sync::atomic::Ordering::Relaxed)
6995                                {
6996                                    self.finish_generation(&mut mtp, &mut router, true);
6997                                    return Err(
6998                                        "GPU token graph failed during greedy burst".to_string()
6999                                    );
7000                                }
7001                                break;
7002                            };
7003                            next_pos += k;
7004                            for &id in &ids {
7005                                if !commit!(id) {
7006                                    stopped = true;
7007                                    break;
7008                                }
7009                            }
7010                            if stopped {
7011                                break;
7012                            }
7013                            t_fwd = *ids.last().unwrap();
7014                        }
7015                        if stopped {
7016                            break 'decode;
7017                        }
7018                    }
7019                    // Metal: keep the draft head's cache in step through
7020                    // the trial's plain phase and a paused speculation —
7021                    // the pair (hidden, t_fwd) at next_pos−1, the step the
7022                    // round's draft 0 would take. Without it the head's
7023                    // cache lagged the trunk by every plain token for the
7024                    // rest of the generation: the batched warm-up declined
7025                    // every later round and its rows went one by one (a
7026                    // whole MTP step per accepted token), and the drafts
7027                    // attended a context with those tokens missing.
7028                    #[cfg(target_os = "macos")]
7029                    if graph_spec
7030                        && spec_watchdog_off
7031                        && next_pos > 0
7032                        && self.mtp_graph_mode == Some(true)
7033                        && crate::gpu::q1_force()
7034                    {
7035                        if let Some(m) = mtp.as_mut() {
7036                            let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
7037                        }
7038                    }
7039                    hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
7040                    next_pos += 1;
7041                    // Dynamic routing: the forward updated φ; ask the
7042                    // router whether to switch skills before the next token.
7043                    if let Some(r) = &mut router {
7044                        let phi = self.dyn_phi_ema.clone();
7045                        let decision = r.step(&phi, generated);
7046                        if let Some(new_active) = decision {
7047                            let _ = self.set_active_skill(new_active);
7048                        }
7049                        // Backfill this token's coherence + switch flag from
7050                        // the just-run eval (freshest measured values).
7051                        if trace_on {
7052                            if let Some(last) = traces.last_mut() {
7053                                let e = r.last_best_e();
7054                                last.recon = e.is_finite().then_some(e);
7055                                last.switched = decision.is_some();
7056                            }
7057                        }
7058                    }
7059                }
7060            }
7061        }
7062
7063        let cancelled = finish_reason == "cancelled";
7064        // A generation during which the router switched weights holds no
7065        // state any single overlay would produce (each switch cleared the
7066        // cache mid-sequence), so it leaves no reuse key behind either.
7067        let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
7068        if mimo_spec {
7069            if let Some(st) = self.mimo_mtp.as_ref() {
7070                let line = st.stats.line();
7071                tracing::info!("{line}");
7072                if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
7073                    eprintln!("{line}");
7074                }
7075            }
7076        }
7077        self.finish_generation(&mut mtp, &mut router, cancelled);
7078
7079        let output_ids = &all_ids[input_ids.len()..];
7080        // Forwarded = prompt + all generated but the LAST sampled token
7081        // (emitted without being fed back). Exact only without MTP —
7082        // reuse is gated off when MTP is active.
7083        let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
7084        // A MiMo speculative round that stopped on an accepted draft (EOS,
7085        // cancel) leaves verify rows past the committed stream in the cache:
7086        // never offer that cache for reuse. Neither does a router that
7087        // switched weights mid-sequence (no single overlay produced it).
7088        let consumed = std::mem::take(&mut all_ids);
7089        if dyn_switched {
7090            self.clear_sequence_state();
7091        } else if cancelled || mimo_spec || prompt_rows.is_some() {
7092            self.clear_history();
7093        } else {
7094            self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
7095        }
7096        all_ids = consumed;
7097        let output_ids = &all_ids[input_ids.len()..];
7098        confidence.truncate(output_ids.len()); // guard against any overshoot
7099        traces.truncate(output_ids.len());
7100        Ok(GenerateResult {
7101            text: self.tokenizer.decode(output_ids),
7102            token_ids: output_ids.to_vec(),
7103            prompt_tokens: input_ids.len(),
7104            tokens_generated: generated,
7105            finish_reason,
7106            mtp_drafted: drafted,
7107            mtp_accepted: accepted,
7108            token_confidence: confidence,
7109            traces,
7110        })
7111    }
7112
7113    /// One MTP step: feed `(hidden_p, token_{p+1})` into the draft head,
7114    /// advance its KV cache at position `p`, return the drafted token
7115    /// for position `p+2`.
7116    fn mtp_step(
7117        &mut self,
7118        m: &mut MtpModule,
7119        hidden: &[f32],
7120        next_token: u32,
7121        position: usize,
7122    ) -> u32 {
7123        self.mtp_step_h(m, hidden, next_token, position).0
7124    }
7125
7126    /// Tally for `CMF_MTP_CHAIN_PROBE`: per depth, how often the CHAIN is
7127    /// still an exact prefix of the real continuation. Printed every 128
7128    /// depth-0 samples so a killed run still shows its table.
7129    fn chain_probe_note(depth: usize, prefix_ok: bool) {
7130        use std::sync::Mutex;
7131        static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
7132        let mut t = T.lock().unwrap();
7133        if t.len() <= depth {
7134            t.resize(depth + 1, (0, 0));
7135        }
7136        t[depth].0 += 1;
7137        t[depth].1 += prefix_ok as u64;
7138        if depth == 0 && t[0].0 % 128 == 0 {
7139            let line: Vec<String> = t
7140                .iter()
7141                .enumerate()
7142                .map(|(d, (n, k))| {
7143                    format!(
7144                        "d{}={:.0}%({n})",
7145                        d + 1,
7146                        100.0 * *k as f64 / (*n).max(1) as f64
7147                    )
7148                })
7149                .collect();
7150            eprintln!("mtp-chain: {}", line.join(" "));
7151        }
7152    }
7153
7154    /// `mtp_step` that also hands back the block's own output hidden — the
7155    /// state a CHAINED draft feeds the next step, the way a multi-token
7156    /// speculative round iterates the head on itself.
7157    /// One MTP block step from (trunk hidden, token): the head's LOGITS
7158    /// and the block's own hidden for chaining. The draft is argmax of the
7159    /// logits on the greedy path and a draw from their post-chain
7160    /// distribution on the sampling path.
7161    fn mtp_step_hl(
7162        &mut self,
7163        m: &mut MtpModule,
7164        hidden: &[f32],
7165        next_token: u32,
7166        position: usize,
7167    ) -> (Vec<f32>, Vec<f32>) {
7168        // The graph arm: the MTP block as a one-layer token graph with the
7169        // head fused — device attention over the block's own KV mirror,
7170        // one submit for block + head, hidden and logits back together.
7171        // Decided once per generation (see `mtp_graph_mode`).
7172        #[cfg(target_os = "macos")]
7173        if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
7174            if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
7175                self.mtp_graph_mode = Some(true);
7176                return r;
7177            }
7178            if self.mtp_graph_mode == Some(true) {
7179                tracing::error!("mtp Metal graph failed after admission");
7180                self.clear_sequence_state();
7181                self.graph_failed
7182                    .store(true, std::sync::atomic::Ordering::Relaxed);
7183                self.cancel
7184                    .store(true, std::sync::atomic::Ordering::Relaxed);
7185                return (Vec::new(), Vec::new());
7186            }
7187            self.mtp_graph_mode = Some(false);
7188        }
7189        #[cfg(feature = "gpu")]
7190        if self.mtp_graph_mode != Some(false) {
7191            if !self.mtp_graph_ok(m) {
7192                if self.mtp_graph_mode == Some(true) {
7193                    // A mirror was already admitted, so a capability change
7194                    // cannot safely switch this request to the stale CPU
7195                    // cache.  Keep the same terminal contract as a failed
7196                    // token graph.
7197                    tracing::error!("mtp graph became unavailable after admission");
7198                    self.clear_sequence_state();
7199                    self.graph_failed
7200                        .store(true, std::sync::atomic::Ordering::Relaxed);
7201                    self.cancel
7202                        .store(true, std::sync::atomic::Ordering::Relaxed);
7203                    return (Vec::new(), Vec::new());
7204                }
7205                self.mtp_graph_mode = Some(false);
7206            } else {
7207                if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
7208                    self.mtp_graph_mode = Some(true);
7209                    return r;
7210                }
7211                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7212                    // A token graph can have admitted a persistent MTP/GDN
7213                    // mirror before its readback failed.  The CPU MTP cache
7214                    // is not a valid continuation in that state; leave the
7215                    // flag set so the generation caller returns through its
7216                    // terminal error path instead of silently switching
7217                    // arithmetic.
7218                    return (Vec::new(), Vec::new());
7219                }
7220                // `mtp_graph_ok` was true, so a None here means a refusal or
7221                // failure after graph admission.  Do not fall through to a
7222                // CPU cache whose rows may lag the device mirror.
7223                tracing::error!("mtp graph failed or declined after admission");
7224                self.clear_sequence_state();
7225                self.graph_failed
7226                    .store(true, std::sync::atomic::Ordering::Relaxed);
7227                self.cancel
7228                    .store(true, std::sync::atomic::Ordering::Relaxed);
7229                return (Vec::new(), Vec::new());
7230            }
7231        }
7232        // fc concat order is [enorm(embed); hnorm(hidden)] — EMBEDDING
7233        // FIRST. Verified by the oracle (converter/mtp_oracle.py):
7234        // [emb;hid] → 45.8% acceptance, [hid;emb] → 0.00%.
7235        let e = self.embed_single(next_token);
7236        let mut cat = vec![0.0f32; 2 * self.hidden_size];
7237        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7238        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7239        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7240        let mut x = vec![0.0f32; self.hidden_size];
7241        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7242
7243        // One standard transformer block over the MTP's own cache.
7244        let lw = &m.layer;
7245        inference::rms_norm_into(
7246            &x,
7247            &lw.input_norm,
7248            self.rms_eps,
7249            self.norm_style,
7250            &mut self.ws.n1,
7251        );
7252        let attn = match &lw.attn {
7253            // MLA models carry no MTP head; this path cannot see them.
7254            AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
7255            AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
7256            AttnKind::Full {
7257                wq,
7258                wk,
7259                wv,
7260                wo,
7261                q_norm,
7262                k_norm,
7263                output_gate,
7264                softplus_gate,
7265                bias,
7266            } => {
7267                let mut cfg = self.attn_cfg(position);
7268                cfg.q_norm = q_norm.as_deref();
7269                cfg.k_norm = k_norm.as_deref();
7270                cfg.output_gate = *output_gate;
7271                cfg.softplus_gate = softplus_gate
7272                    .as_ref()
7273                    .map(|(gate, per_head)| (gate, *per_head));
7274                cfg.bias = bias
7275                    .as_ref()
7276                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7277                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7278            }
7279            AttnKind::Linear(_)
7280            | AttnKind::LinearGdn(_)
7281            | AttnKind::ShortConv(_)
7282            | AttnKind::Bounded(_) => {
7283                unreachable!("MTP block is full attention")
7284            }
7285        };
7286        for (i, &a) in attn.iter().enumerate() {
7287            x[i] += a;
7288        }
7289        inference::rms_norm_into(
7290            &x,
7291            &lw.post_norm,
7292            self.rms_eps,
7293            self.norm_style,
7294            &mut self.ws.p1,
7295        );
7296        let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
7297        for (i, &f) in ffn.iter().enumerate() {
7298            x[i] += f;
7299        }
7300
7301        inference::rms_norm_into(
7302            &x,
7303            &m.final_norm,
7304            self.rms_eps,
7305            self.norm_style,
7306            &mut self.ws.n1,
7307        );
7308        let lg = self.lm_head_forward(&self.ws.n1);
7309        (lg, x)
7310    }
7311
7312    /// `mtp_step_hl` reduced to the greedy draft: argmax of the head.
7313    fn mtp_step_h(
7314        &mut self,
7315        m: &mut MtpModule,
7316        hidden: &[f32],
7317        next_token: u32,
7318        position: usize,
7319    ) -> (u32, Vec<f32>) {
7320        let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
7321        let draft = sampler::argmax(&lg);
7322        attention::recycle_buf(&mut lg);
7323        (draft, x)
7324    }
7325
7326    /// One speculative round for the trial: rounds 1..5 of a `Spec` phase
7327    /// advance it (the monitor already averaged this round); after five,
7328    /// the plain phase runs (once — a known plain rate decides at once);
7329    /// a decided speculation keeps re-checking the rule every round and
7330    /// stops after four losing rounds in a row.
7331    fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
7332        match trial {
7333            SpecTrial::Spec { t0, gen0, rounds } => {
7334                let rounds = rounds + 1;
7335                // A round that accepts no draft still has to run the MTP
7336                // chain and a K+1 verifier, but emits only its verifier tip.
7337                // After two such real rounds the speculative arm cannot win
7338                // on any backend; time plain immediately rather than spend
7339                // the remaining exploratory rounds on the same one-token
7340                // result. A later 128-token re-check still gives changing
7341                // context a chance to re-admit it.
7342                if mon.repeated_zero_acceptance() {
7343                    tracing::info!(
7344                        "speculation trial: no accepted drafts in two rounds — timing plain"
7345                    );
7346                    return SpecTrial::Plain {
7347                        t0: std::time::Instant::now(),
7348                        gen0: generated,
7349                    };
7350                }
7351                if rounds >= 5 {
7352                    if mon.plain_ms > 0.0 {
7353                        let keep = mon.pays();
7354                        mon.fails = 0;
7355                        tracing::info!(
7356                            "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
7357                            mon.tokens,
7358                            mon.round_ms,
7359                            mon.plain_ms,
7360                            if keep { "speculating" } else { "plain" }
7361                        );
7362                        SpecTrial::Decided {
7363                            spec: keep,
7364                            recheck_at: if keep { usize::MAX } else { generated + 128 },
7365                        }
7366                    } else if mon.pays() {
7367                        // Metal: the rounds land enough tokens each that no
7368                        // plain measurement is needed — keep speculating,
7369                        // and re-check every round (a losing streak sends
7370                        // the loop to the plain phase, below).
7371                        mon.fails = 0;
7372                        tracing::info!(
7373                            "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
7374                            mon.tokens,
7375                            mon.round_ms,
7376                        );
7377                        SpecTrial::Decided {
7378                            spec: true,
7379                            recheck_at: usize::MAX,
7380                        }
7381                    } else {
7382                        SpecTrial::Plain {
7383                            t0: std::time::Instant::now(),
7384                            gen0: generated,
7385                        }
7386                    }
7387                } else {
7388                    SpecTrial::Spec { t0, gen0, rounds }
7389                }
7390            }
7391            SpecTrial::Decided { spec: true, .. } => {
7392                if mon.pays() {
7393                    mon.fails = 0;
7394                    trial
7395                } else {
7396                    mon.fails += 1;
7397                    if mon.fails >= 4 {
7398                        if mon.plain_ms <= 0.0 {
7399                            // Metal, plain never timed: four doubtful rounds
7400                            // buy the (bounded) plain measurement, and the
7401                            // exact rule decides from it.
7402                            tracing::info!(
7403                                "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
7404                                mon.tokens,
7405                                mon.round_ms,
7406                            );
7407                            return SpecTrial::Plain {
7408                                t0: std::time::Instant::now(),
7409                                gen0: generated,
7410                            };
7411                        }
7412                        tracing::info!(
7413                            "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
7414                            mon.tokens,
7415                            mon.round_ms,
7416                            mon.plain_ms
7417                        );
7418                        SpecTrial::Decided {
7419                            spec: false,
7420                            recheck_at: generated + 128,
7421                        }
7422                    } else {
7423                        trial
7424                    }
7425                }
7426            }
7427            other => other,
7428        }
7429    }
7430
7431    /// The MTP block's device-mirror id: the trunk's id with a high bit,
7432    /// so the (kv_id, layer) mirror keys never collide.
7433    fn mtp_kv_id(&self) -> u64 {
7434        self.graph_kv_id | (1u64 << 40)
7435    }
7436
7437    /// The MTP block's mirror layer index: 0 — its own kv_id keeps it
7438    /// apart from the trunk, and the BATCH graph (the warm-up path) keys
7439    /// its mirrors at layer 0 with no base of its own, so the draft's
7440    /// token graph must key the same slot.
7441    const MTP_LAYER_BASE: usize = 0;
7442
7443    /// The wgpu MTP draft writes speculative rows straight into its device
7444    /// mirror while the CPU owner retains only the real prompt/decode anchor.
7445    /// After verification, move that mirror cursor back to the anchor before
7446    /// replaying accepted pairs.  The next graph append then sees the same
7447    /// contiguous position as the CPU/Metal path without uploading stale
7448    /// speculative rows.
7449    #[cfg(feature = "gpu")]
7450    fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
7451        self.mtp_graph_mode != Some(true)
7452            || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
7453    }
7454
7455    /// A speculative verify graph appends the full `k+1` trunk rows before
7456    /// the acceptance count is known.  GDN state already has a snapshot
7457    /// restore; Full-attention mirrors need the matching logical cursor
7458    /// rewind so the next graph call does not reject an ahead-of-position KV
7459    /// cache after a partial acceptance.
7460    #[cfg(feature = "gpu")]
7461    fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
7462        let mut ok = true;
7463        let mut expected = false;
7464        for li in 0..self.num_layers {
7465            if matches!(
7466                self.weights.layers[self.phys_layer(li)].attn,
7467                AttnKind::Full { .. }
7468            ) {
7469                expected = true;
7470                ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
7471            }
7472        }
7473        !expected || ok
7474    }
7475
7476    /// Count the recurrent layers participating in the trunk verify graph.
7477    /// Snapshot restore is all-or-nothing across that set; deriving the count
7478    /// from the model keeps the restore contract valid for looped models too.
7479    fn graph_gdn_layer_count(&self) -> usize {
7480        (0..self.num_layers)
7481            .filter(|&li| {
7482                matches!(
7483                    &self.weights.layers[self.phys_layer(li)].attn,
7484                    AttnKind::LinearGdn(_)
7485                )
7486            })
7487            .count()
7488    }
7489
7490    /// The block's input from (trunk hidden, token): eh_proj · [enorm(e);
7491    /// hnorm(h)] — the same arithmetic the per-op path starts with.
7492    fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
7493        let e = self.embed_single(next_token);
7494        let mut cat = vec![0.0f32; 2 * self.hidden_size];
7495        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7496        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7497        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7498        let mut x = vec![0.0f32; self.hidden_size];
7499        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7500        x
7501    }
7502
7503    /// Is the MTP block graphable at all (device up, full attention
7504    /// without softplus, dense FFN)? The plan itself is built per call.
7505    #[cfg(feature = "gpu")]
7506    fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7507        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7508            return false;
7509        }
7510        if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7511            || !crate::gpu::enabled_here()
7512            || self.attn_softcap > 0.0
7513            || self.attention_heads_per_layer.is_some()
7514            // The block graph caches V as wide as K and feeds o_proj
7515            // nh·head_dim; a narrow-V model keeps its MTP block per-op.
7516            || self.v_head_dim.is_some()
7517        {
7518            return false;
7519        }
7520        matches!(
7521            &m.layer.attn,
7522            AttnKind::Full {
7523                softplus_gate: None,
7524                ..
7525            }
7526        ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7527    }
7528
7529    /// Full MTP token-graph eligibility, including the fused lm-head and all
7530    /// block projection weights.  Keep this distinct from the block-only
7531    /// check: prompt warm-up does not need the head, while a draft step does.
7532    #[cfg(feature = "gpu")]
7533    fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7534        if !self.mtp_block_graph_ok(m) {
7535            return false;
7536        }
7537        let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7538            return false;
7539        };
7540        let FfnKind::Dense(d) = &m.layer.ffn else {
7541            return false;
7542        };
7543        d.segs.is_empty()
7544            && wq.graph_weight().is_some()
7545            && wk.graph_weight().is_some()
7546            && wv.graph_weight().is_some()
7547            && wo.graph_weight().is_some()
7548            && d.gate_proj.graph_weight().is_some()
7549            && d.up_proj.graph_weight().is_some()
7550            && d.down_proj.graph_weight().is_some()
7551            && self.weights.lm_head.graph_weight().is_some()
7552    }
7553
7554    /// One MTP block step on the wgpu token graph: block + fused head in
7555    /// one submit, the block hidden and the logits read back together.
7556    /// None = the graph cannot take this block (softplus gate, non-dense
7557    /// FFN, unquantized head, no device) — the caller keeps the per-op
7558    /// path for the whole generation.
7559    #[cfg(feature = "gpu")]
7560    fn mtp_step_graph(
7561        &mut self,
7562        m: &mut MtpModule,
7563        hidden: &[f32],
7564        next_token: u32,
7565        position: usize,
7566    ) -> Option<(Vec<f32>, Vec<f32>)> {
7567        if !self.mtp_graph_ok(m) {
7568            return None;
7569        }
7570        let lw = &m.layer;
7571        let AttnKind::Full {
7572            wq,
7573            wk,
7574            wv,
7575            wo,
7576            q_norm,
7577            k_norm,
7578            output_gate,
7579            softplus_gate,
7580            bias,
7581        } = &lw.attn
7582        else {
7583            return None;
7584        };
7585        if softplus_gate.is_some() {
7586            return None;
7587        }
7588        let FfnKind::Dense(d) = &lw.ffn else {
7589            return None;
7590        };
7591        if !d.segs.is_empty() {
7592            return None; // tube layers run on the segmented path
7593        }
7594        // The block's input first: it borrows `self` mutably (embed scratch,
7595        // pool), the plan below borrows the weights immutably.
7596        let mut x = self.mtp_block_input(m, hidden, next_token);
7597        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7598            let (_, i, kind, rs) = t.graph_weight()?;
7599            Some(crate::gpu::GraphW {
7600                idx: i,
7601                kind,
7602                row_scale: rs,
7603                data: &[],
7604                prism: crate::gpu::GraphPrismOp::None,
7605                affine: false,
7606            })
7607        }
7608        let (model, _, _, _) = wq.graph_weight()?;
7609        let model = model.clone();
7610        let (lm_gw, lm_rows) = {
7611            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7612            // The draft's head over the CMF_DRAFT_VOCAB shortlist (the same
7613            // cut the native Metal draft takes): 662 MB a step on Qwen3.8
7614            // becomes 170 MB at 65536; the verify keeps the full head.
7615            let rows = if kind == 6 {
7616                self.draft_head_rows(self.weights.lm_head.rows())
7617            } else {
7618                self.weights.lm_head.rows()
7619            };
7620            (
7621                crate::gpu::GraphW {
7622                    idx: i,
7623                    kind,
7624                    row_scale: rs,
7625                    data: &[],
7626                    prism: crate::gpu::GraphPrismOp::None,
7627                    affine: false,
7628                },
7629                rows,
7630            )
7631        };
7632        let layer = crate::gpu::GraphLayer {
7633            mhc: None,
7634            input_norm: &lw.input_norm,
7635            attn: crate::gpu::GraphAttn::Full {
7636                wq: gw(wq)?,
7637                wk: gw(wk)?,
7638                wv: gw(wv)?,
7639                wo: gw(wo)?,
7640                q_norm: q_norm.as_deref(),
7641                k_norm: k_norm.as_deref(),
7642                late_qk_norm: self.qk_norm_after_rope,
7643                bias: bias
7644                    .as_ref()
7645                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7646                output_gate: *output_gate,
7647                cpu_k: m.kv.k_heads(),
7648                cpu_v: m.kv.v_heads(),
7649                cpu_base: m.kv.base(),
7650                geom: None,
7651                head_gate: None,
7652            },
7653            post_norm: &lw.post_norm,
7654            attn_out_norm: None,
7655            ffn_out_norm: None,
7656            layer_scale: None,
7657            ffn: crate::gpu::GraphFfn::Dense {
7658                gate: gw(&d.gate_proj)?,
7659                up: gw(&d.up_proj)?,
7660                down: gw(&d.down_proj)?,
7661                act: d.act.graph_act()?,
7662            },
7663        };
7664        let nh = self.num_heads;
7665        let (nkv, hd, rd) = self.layer_geom(0);
7666        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7667        let mut logits = Vec::new();
7668        let ok = crate::gpu::forward_token_graph(
7669            &model,
7670            self.mtp_kv_id(),
7671            std::slice::from_ref(&layer),
7672            &[None],
7673            self.o1_epoch,
7674            &self.inv_freq,
7675            &mut x,
7676            nh,
7677            nkv,
7678            hd,
7679            self.attn_scale,
7680            rd,
7681            self.hidden_size,
7682            // An MTP draft can have an independently compacted FFN. The
7683            // graph must use the checked local tensor width, not the trunk
7684            // arch-wide intermediate size.
7685            d.gate_proj.rows(),
7686            position,
7687            self.kv_cache.max_seq_len,
7688            gemma,
7689            self.rms_eps as f32,
7690            Some((&lm_gw, lm_rows)),
7691            &m.final_norm,
7692            &mut logits,
7693            &[],
7694            1,
7695            None,
7696            None,
7697            None,
7698            Self::MTP_LAYER_BASE,
7699            true,
7700        );
7701        match ok {
7702            crate::gpu::TokenGraphOutcome::Completed => {}
7703            crate::gpu::TokenGraphOutcome::Declined => return None,
7704            crate::gpu::TokenGraphOutcome::Failed => {
7705                // The backend has already admitted persistent state.  Keep
7706                // this distinct from a capability refusal so the caller
7707                // cannot switch to the stale CPU MTP cache.
7708                self.clear_sequence_state();
7709                self.graph_failed
7710                    .store(true, std::sync::atomic::Ordering::Relaxed);
7711                self.cancel
7712                    .store(true, std::sync::atomic::Ordering::Relaxed);
7713                return None;
7714            }
7715        }
7716        // The graph may deliberately evaluate only a prefix of the draft
7717        // head.  Do not turn every omitted token into a zero-logit candidate:
7718        // that can win an all-negative shortlist and collapses acceptance.
7719        pad_mtp_draft_logits(&mut logits, self.vocab_size);
7720        Some((logits, x))
7721    }
7722
7723    /// The warm-ups of one speculative round on the device: every accepted
7724    /// (hidden, token) pair as ONE batched graph run over the MTP block
7725    /// (no head) — its kv_append lands the pairs in the block's mirror.
7726    /// `pairs` are consecutive positions from `first_pos`.  The tri-state
7727    /// result is intentional: a refusal before admission may use the
7728    /// per-row/CPU route, while a failure after admission must terminate the
7729    /// sequence rather than fall through to a stale CPU cache.
7730    #[cfg(feature = "gpu")]
7731    fn mtp_warm_graph(
7732        &mut self,
7733        m: &mut MtpModule,
7734        pairs: &[(&[f32], u32)],
7735        first_pos: usize,
7736    ) -> crate::gpu::BatchGraphOutcome {
7737        if pairs.is_empty() {
7738            return crate::gpu::BatchGraphOutcome::Completed;
7739        }
7740        if !self.mtp_block_graph_ok(m) {
7741            return crate::gpu::BatchGraphOutcome::Declined;
7742        }
7743        let hs = self.hidden_size;
7744        // Block inputs for every pair (eh_proj on the per-op path, one
7745        // matvec each — the plan's own prologue).
7746        let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7747        for (h, t) in pairs {
7748            hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7749        }
7750        let lw = &m.layer;
7751        let AttnKind::Full {
7752            wq,
7753            wk,
7754            wv,
7755            wo,
7756            q_norm,
7757            k_norm,
7758            output_gate,
7759            bias,
7760            ..
7761        } = &lw.attn
7762        else {
7763            return crate::gpu::BatchGraphOutcome::Declined;
7764        };
7765        let FfnKind::Dense(d) = &lw.ffn else {
7766            return crate::gpu::BatchGraphOutcome::Declined;
7767        };
7768        if !d.segs.is_empty() {
7769            return crate::gpu::BatchGraphOutcome::Declined; // tube layers run on the segmented path
7770        }
7771        // The graph has no arm for a projected output gate, and only the
7772        // activations `graph_act` names.
7773        let (
7774            AttnKind::Full {
7775                softplus_gate: None,
7776                ..
7777            },
7778            Some(gact),
7779        ) = (&lw.attn, d.act.graph_act())
7780        else {
7781            return crate::gpu::BatchGraphOutcome::Declined;
7782        };
7783        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7784            let (_, i, kind, rs) = t.graph_weight()?;
7785            Some(crate::gpu::GraphW {
7786                idx: i,
7787                kind,
7788                row_scale: rs,
7789                data: &[],
7790                prism: crate::gpu::GraphPrismOp::None,
7791                affine: false,
7792            })
7793        }
7794        let Some((model, _, _, _)) = wq.graph_weight() else {
7795            return crate::gpu::BatchGraphOutcome::Declined;
7796        };
7797        let model = model.clone();
7798        let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7799            gw(wq),
7800            gw(wk),
7801            gw(wv),
7802            gw(wo),
7803            gw(&d.gate_proj),
7804            gw(&d.up_proj),
7805            gw(&d.down_proj),
7806        ) else {
7807            return crate::gpu::BatchGraphOutcome::Declined;
7808        };
7809        let layer = crate::gpu::GraphLayer {
7810            mhc: None,
7811            input_norm: &lw.input_norm,
7812            attn: crate::gpu::GraphAttn::Full {
7813                wq: gwq,
7814                wk: gwk,
7815                wv: gwv,
7816                wo: gwo,
7817                q_norm: q_norm.as_deref(),
7818                k_norm: k_norm.as_deref(),
7819                late_qk_norm: self.qk_norm_after_rope,
7820                bias: bias
7821                    .as_ref()
7822                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7823                output_gate: *output_gate,
7824                cpu_k: m.kv.k_heads(),
7825                cpu_v: m.kv.v_heads(),
7826                cpu_base: m.kv.base(),
7827                geom: None,
7828                head_gate: None,
7829            },
7830            post_norm: &lw.post_norm,
7831            attn_out_norm: None,
7832            ffn_out_norm: None,
7833            layer_scale: None,
7834            ffn: crate::gpu::GraphFfn::Dense {
7835                gate: gg,
7836                up: gu,
7837                down: gd,
7838                act: gact,
7839            },
7840        };
7841        let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7842        let nh = self.num_heads;
7843        let (nkv, hd, rd) = self.layer_geom(0);
7844        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7845        crate::gpu::forward_batch_graph(
7846            &model,
7847            self.mtp_kv_id(),
7848            std::slice::from_ref(&layer),
7849            &self.inv_freq,
7850            &mut hiddens,
7851            nh,
7852            nkv,
7853            hd,
7854            rd,
7855            hs,
7856            // See `mtp_step_graph`: draft FFN geometry is local to this
7857            // block after independent MTP compaction.
7858            d.gate_proj.rows(),
7859            &positions,
7860            self.kv_cache.max_seq_len,
7861            gemma,
7862            self.rms_eps as f32,
7863            self.attn_scale,
7864            pairs.len(),
7865            &[],
7866            0,
7867            None,
7868            None,
7869        )
7870    }
7871
7872    /// Complete an MTP warm-up after the batched graph has refused.  A
7873    /// graphable block is retried one row at a time; once any device row has
7874    /// been admitted, a CPU fallback would observe a stale mirror, so every
7875    /// token-graph refusal is terminal.  If the block is not graphable and no
7876    /// mirror exists yet, warming on the CPU is safe and records the CPU mode
7877    /// for the rest of the generation.
7878    #[cfg(feature = "gpu")]
7879    fn mtp_warm_graph_fallback(
7880        &mut self,
7881        m: &mut MtpModule,
7882        pairs: &[(&[f32], u32)],
7883        first_pos: usize,
7884    ) -> bool {
7885        if pairs.is_empty() {
7886            return true;
7887        }
7888        let graphable = self.mtp_block_graph_ok(m);
7889        if !graphable {
7890            // A previously admitted mirror cannot be made coherent by
7891            // appending to the host cache.  The caller turns this into a
7892            // terminal generation error and clears both mirrors.
7893            if self.mtp_graph_mode == Some(true) {
7894                return false;
7895            }
7896            self.mtp_graph_mode = Some(false);
7897            for (j, (h, t)) in pairs.iter().enumerate() {
7898                self.mtp_warm(m, h, *t, first_pos + j);
7899            }
7900            return true;
7901        }
7902
7903        // The batch refusal is recoverable only through the same device
7904        // state.  Keep rows owned until each token graph has completed; a
7905        // None is treated as unsafe because the token-graph API deliberately
7906        // collapses its backend refusal/failure into that result.
7907        for (j, (h, t)) in pairs.iter().enumerate() {
7908            if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7909                return false;
7910            }
7911        }
7912        self.mtp_graph_mode = Some(true);
7913        true
7914    }
7915
7916    /// Warm a contiguous set of MTP pairs using the existing graph seam, with
7917    /// an all-or-nothing error contract for callers that already admitted the
7918    /// trunk batch.  The non-GPU build keeps the same pair accounting while
7919    /// using the established CPU warm path.
7920    #[cfg(feature = "gpu")]
7921    fn mtp_warm_prefill_pairs(
7922        &mut self,
7923        m: &mut MtpModule,
7924        pairs: &[(&[f32], u32)],
7925        first_pos: usize,
7926    ) -> Result<(), &'static str> {
7927        // Keep unsupported token-graph heads on the established CPU MTP
7928        // route before admitting any block mirror.  Once a device mirror is
7929        // active, the same condition is terminal because CPU rows cannot
7930        // repair its state.
7931        if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7932            if self.mtp_graph_mode == Some(true) {
7933                return Err("MTP token graph became unavailable after admission");
7934            }
7935            self.mtp_graph_mode = Some(false);
7936            for (j, (h, t)) in pairs.iter().enumerate() {
7937                self.mtp_warm(m, h, *t, first_pos + j);
7938            }
7939            return Ok(());
7940        }
7941        match self.mtp_warm_graph(m, pairs, first_pos) {
7942            crate::gpu::BatchGraphOutcome::Completed => {
7943                if !pairs.is_empty() {
7944                    self.mtp_graph_mode = Some(true);
7945                }
7946                Ok(())
7947            }
7948            crate::gpu::BatchGraphOutcome::Declined => {
7949                if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7950                    Ok(())
7951                } else {
7952                    Err("MTP warm-up fallback failed after device admission")
7953                }
7954            }
7955            crate::gpu::BatchGraphOutcome::Failed => {
7956                Err("MTP warm batch graph failed after admission")
7957            }
7958        }
7959    }
7960
7961    #[cfg(not(feature = "gpu"))]
7962    fn mtp_warm_prefill_pairs(
7963        &mut self,
7964        m: &mut MtpModule,
7965        pairs: &[(&[f32], u32)],
7966        first_pos: usize,
7967    ) -> Result<(), &'static str> {
7968        for (j, (h, t)) in pairs.iter().enumerate() {
7969            self.mtp_warm(m, h, *t, first_pos + j);
7970        }
7971        Ok(())
7972    }
7973
7974    /// The MTP block alone — advance its KV with a (hidden, token) pair the
7975    /// verify just proved, without paying the head. What keeps the draft's
7976    /// attention context warm between speculative rounds.
7977    fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7978        let e = self.embed_single(next_token);
7979        let mut cat = vec![0.0f32; 2 * self.hidden_size];
7980        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7981        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7982        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7983        let mut x = vec![0.0f32; self.hidden_size];
7984        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7985        inference::rms_norm_into(
7986            &x,
7987            &m.layer.input_norm,
7988            self.rms_eps,
7989            self.norm_style,
7990            &mut self.ws.n1,
7991        );
7992        let attn = match &m.layer.attn {
7993            AttnKind::Full {
7994                wq,
7995                wk,
7996                wv,
7997                wo,
7998                q_norm,
7999                k_norm,
8000                output_gate,
8001                softplus_gate,
8002                bias,
8003            } => {
8004                let mut cfg = self.attn_cfg(position);
8005                cfg.q_norm = q_norm.as_deref();
8006                cfg.k_norm = k_norm.as_deref();
8007                cfg.output_gate = *output_gate;
8008                cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
8009                cfg.bias = bias
8010                    .as_ref()
8011                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
8012                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
8013            }
8014            _ => return,
8015        };
8016        let _ = attn;
8017    }
8018
8019    /// Speculative decode ON the wgpu whole-token graph: draft k with the
8020    /// MTP head, verify all of them plus the tip in ONE batched graph
8021    /// submit whose tail folds the head, commit the accepted prefix and
8022    /// roll the GDN state back to the last real position. Greedy only —
8023    /// output equals the plain graph's token for token, the way the DSV4
8024    /// verify equals the walk.
8025    #[cfg(feature = "gpu")]
8026    #[allow(clippy::too_many_arguments)]
8027    fn graph_spec_step(
8028        &mut self,
8029        m: &mut MtpModule,
8030        hidden: &[f32],
8031        t_next: u32,
8032        next_pos: usize,
8033        drafted: &mut usize,
8034        accepted: &mut usize,
8035        // The committed stream (prompt + generated so far, `t_next`
8036        // included): the sampler chain's penalties read it, and the
8037        // sampling arm extends it with the drafts position by position.
8038        all_ids: &mut Vec<u32>,
8039        // Tokens left before `max_tokens`. A round commits up to k
8040        // accepted drafts, and those positions are already in the cache,
8041        // so the depth is capped here — trimming the output afterwards
8042        // would leave cache rows the committed stream does not have.
8043        room: usize,
8044    ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
8045        // 3 is the measured optimum on Qwen3.6-27B / RTX 5090 (medians
8046        // of three, greedy): 51.1 tok/s against a plain 49.4, where k=2
8047        // gives 46.1, k=4 50.0, k=5 47.4, k=6 45.2. Acceptance is 89-91%
8048        // throughout — what turns the curve over is the verify, which
8049        // costs ~7.4 ms per extra position, and the draft ~3 ms a step.
8050        // 4 since the draft moved onto the graph (Qwen3.8-27B / 5090:
8051        // k=3 51.2, k=4 51.8 with the per-op draft; the graph draft
8052        // halves the draft cost, so the extra draft is cheaper still).
8053        // 5 with the int8 verify (the default: measured 76.5 against
8054        // k=4's 72-74 and k=6's 74 on the 5090), 4 with the f32 one.
8055        #[cfg(target_os = "macos")]
8056        let metal_native = crate::gpu::q1_force();
8057        #[cfg(not(target_os = "macos"))]
8058        let metal_native = false;
8059        #[cfg(feature = "gpu")]
8060        let k_default = if metal_native {
8061            // the Metal verify's GEMM tile is 8 rows wide and flat in b:
8062            // seven drafts + the tip fill it for free
8063            7
8064        } else if crate::gpu_wgpu::verify_i8_on() {
8065            5
8066        } else {
8067            4
8068        };
8069        #[cfg(not(feature = "gpu"))]
8070        let k_default = 4;
8071        let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
8072            .ok()
8073            .and_then(|v| v.parse().ok())
8074            .filter(|&v| (1..=8).contains(&v));
8075        // Adaptive depth: start below the card's flat-verify optimum and
8076        // let the accepted fraction move it — predictable text climbs to
8077        // the old default within a few rounds, prose settles at 2-3 where
8078        // the shorter verify pays.
8079        let (k_start, k_max) = if metal_native {
8080            (7, 7)
8081        } else {
8082            (3, k_default.max(5))
8083        };
8084        let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
8085        let k_spec = k_full.min(room).max(1);
8086        // a tail round cut short by `room` says nothing about the text:
8087        // it must not move the adaptive depth the next request starts at
8088        let k_capped = k_spec < k_full;
8089        if next_pos == 0 {
8090            return None;
8091        }
8092        let t_round = std::time::Instant::now();
8093        // Submissions per phase — and they say where the round's money is.
8094        // Qwen3.6-27B on an RTX 5090, k=3:
8095        //
8096        //   draft   9.3 ms / 12 submissions   (four per MTP step)
8097        //   verify 52.8 ms /  1               (the batched graph)
8098        //   commit  5.4 ms /  6               (two per warm)
8099        //
8100        // The verify is already one submit. The draft's own work is 834 MB
8101        // a step — 0.8 ms at this card's measured 1056 GB/s — against 3.1
8102        // ms measured, so ~0.58 ms of every step is round trip, not
8103        // arithmetic, and the same holds for the warms. Eighteen round
8104        // trips a round at roughly half a millisecond each is ~11 ms of a
8105        // 68 ms round: fusing the MTP block into ONE submit the way the
8106        // trunk already is projects to ~64 tok/s against today's 50.9.
8107        // That is the largest measured item left on this path.
8108        let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
8109        let sub0 = subs();
8110        // Greedy without penalties verifies by argmax equality (bit-exact
8111        // against the plain path). Anything else is speculative SAMPLING:
8112        // each draft is a DRAW from the MTP head's post-chain distribution
8113        // q_j, kept for the accept test; the verify's rows give p_j.
8114        let cfg = self.sampler_config.clone();
8115        let penalized = !(cfg.repetition_penalty == 1.0
8116            && cfg.presence_penalty == 0.0
8117            && cfg.suppress_tokens.is_empty());
8118        // Three verify regimes: plain greedy (argmax of the raw rows),
8119        // greedy WITH penalties (argmax of the penalized rows — a single
8120        // pass each, no distributions), and sampling (draw / accept /
8121        // correct on post-chain distributions).
8122        let greedy_pen = cfg.temperature < 1e-6 && penalized;
8123        let sampling = cfg.temperature >= 1e-6;
8124        // Sampling with a top-k goes through the SPARSE chain: the dense
8125        // one builds nine 248k-float distributions a round (four drafts,
8126        // five verify rows) and measured 19-22 tok/s against a plain 40 —
8127        // the host, not the card. Sparse, the same nine cost tens of
8128        // microseconds each.
8129        let sparse = sampling && sampler::sparse_ok(&cfg);
8130        let base_len = all_ids.len();
8131        if sampling && !sparse && self.spec_q.len() < k_spec {
8132            self.spec_q.resize_with(k_spec, Vec::new);
8133        }
8134        if sparse && self.spec_qs.len() < k_spec {
8135            self.spec_qs.resize_with(k_spec, Vec::new);
8136        }
8137        // Draft the chain: first from the trunk's tip hidden, then the head
8138        // iterating on itself. Rows land in the MTP KV; the chain rows past
8139        // the first are speculation over speculative state and roll back
8140        // below, replaced by verified pairs.
8141        let mut drafts = Vec::with_capacity(k_spec);
8142        let mut hx = hidden.to_vec();
8143        // CMF_SPEC_DBG=1: draft 0 through BOTH MTP arms (graph and per-op)
8144        // from the same inputs — are the arms the difference, or the inputs?
8145        let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
8146        spec_stamp("pro");
8147        // Plain greedy on native Metal: the whole chain as one command
8148        // buffer (device argmax + embedding gather between the steps).
8149        // A decline before commit hands the round to the per-step loop
8150        // below; a failure after commit is terminal, like any graph
8151        // failure after admission.
8152        #[cfg(target_os = "macos")]
8153        if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
8154            match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
8155                Ok(ids) => {
8156                    self.mtp_graph_mode = Some(true);
8157                    drafts = ids;
8158                }
8159                Err(true) => {
8160                    tracing::error!("mtp Metal draft chain failed after commit");
8161                    self.clear_sequence_state();
8162                    self.graph_failed
8163                        .store(true, std::sync::atomic::Ordering::Relaxed);
8164                    self.cancel
8165                        .store(true, std::sync::atomic::Ordering::Relaxed);
8166                    return None;
8167                }
8168                Err(false) => {}
8169            }
8170        }
8171        for j in drafts.len()..k_spec {
8172            let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
8173            let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
8174            if spec_dbg {
8175                let saved = self.mtp_graph_mode;
8176                self.mtp_graph_mode = Some(false);
8177                let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
8178                self.mtp_graph_mode = saved;
8179                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
8180                    return None;
8181                }
8182                m.kv.truncate_last(1);
8183                dbg_ref = Some(r);
8184            }
8185            let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
8186            if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
8187                return None;
8188            }
8189            if let Some((lg_cpu, h_cpu)) = dbg_ref {
8190                let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
8191                let dl = lg
8192                    .iter()
8193                    .zip(&lg_cpu)
8194                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
8195                let dh = hj
8196                    .iter()
8197                    .zip(&h_cpu)
8198                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
8199                eprintln!(
8200                    "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 {}",
8201                    next_pos - 1 + j,
8202                    sampler::argmax(&lg_cpu),
8203                    sampler::argmax(&lg),
8204                    n(&h_cpu),
8205                    n(&hj),
8206                    m.kv.seq_len
8207                );
8208            }
8209            let dj = if sparse {
8210                let mut q = std::mem::take(&mut self.spec_qs[j]);
8211                let ok = sampler::sparse_distribution_into(
8212                    &lg,
8213                    &cfg,
8214                    all_ids,
8215                    &mut self.sampler_scratch,
8216                    self.pool.as_deref(),
8217                    &mut q,
8218                );
8219                let d = if ok {
8220                    sampler::draw_sparse(&q, &mut self.rng)
8221                } else {
8222                    // everything filtered: the dense chain's greedy fallback
8223                    let t = sampler::argmax(&lg);
8224                    q.clear();
8225                    q.push((t, 1.0));
8226                    t
8227                };
8228                self.spec_qs[j] = q;
8229                all_ids.push(d);
8230                d
8231            } else if sampling {
8232                let mut q = std::mem::take(&mut self.spec_q[j]);
8233                sampler::distribution_into(
8234                    &lg,
8235                    &cfg,
8236                    all_ids,
8237                    &mut self.sampler_scratch,
8238                    self.pool.as_deref(),
8239                    &mut q,
8240                );
8241                let d = sampler::draw(&q, &mut self.rng);
8242                self.spec_q[j] = q;
8243                all_ids.push(d); // the next draft's penalties see this one
8244                d
8245            } else if greedy_pen {
8246                let d = sampler::argmax_penalized(
8247                    &lg,
8248                    &cfg,
8249                    all_ids,
8250                    &mut self.sampler_scratch,
8251                    self.pool.as_deref(),
8252                );
8253                all_ids.push(d);
8254                d
8255            } else {
8256                sampler::argmax(&lg)
8257            };
8258            attention::recycle_buf(&mut lg);
8259            drafts.push(dj);
8260            hx = hj;
8261            spec_stamp("d.pick");
8262        }
8263        all_ids.truncate(base_len);
8264        *drafted += k_spec;
8265        let t_draft = t_round.elapsed();
8266        let sub_draft = subs();
8267        // Verify batch: [t_next, d1 .. d_{k-1}] at next_pos.. — every row's
8268        // logits come back from the graph's own head.
8269        let b = k_spec + 1;
8270        let mut hiddens = vec![0.0f32; b * self.hidden_size];
8271        for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
8272            let e = self.embed_single(t);
8273            hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
8274        }
8275        let positions: Vec<usize> = (next_pos..next_pos + b).collect();
8276        spec_stamp("v.emb");
8277        let (lm_gw, lm_rows) = {
8278            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
8279            (
8280                crate::gpu::GraphW {
8281                    idx: i,
8282                    kind,
8283                    row_scale: rs,
8284                    data: &[],
8285                    prism: crate::gpu::GraphPrismOp::None,
8286                    affine: false,
8287                },
8288                self.weights.lm_head.rows(),
8289            )
8290        };
8291        let mut logits = Vec::new();
8292        let final_norm = self.weights.final_norm.clone();
8293        // Plain greedy on Metal: the b argmaxes come from the device
8294        // (`argmax_rows` after the head) and the 7.9 MB logits plane is
8295        // never read back — the round's decision needs only the ids, and
8296        // the loop top takes the last verified id as `spec_forced`, which
8297        // is exactly what its argmax of the row would give. The full rows
8298        // stay for anything that reads them: sampling, penalties,
8299        // confidence, the verify oracle, the logit dump.
8300        // `CMF_METAL_DEV_ARGMAX=0` keeps the host path.
8301        #[cfg(target_os = "macos")]
8302        let greedy_dev = metal_native
8303            && !sampling
8304            && !greedy_pen
8305            && !self.confidence_on
8306            && self.final_softcap.is_none()
8307            // The host acceptance argmax scans the WHOLE head row
8308            // (`lm_rows`), the sampler's own row only `vocab_size`: they
8309            // coincide exactly when the head has no padding rows, and
8310            // only then is the device argmax (which scores `vocab_size`)
8311            // bit-identical to both.
8312            && self.vocab_size == lm_rows
8313            && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
8314            && std::env::var_os("CMF_LOGIT_DUMP").is_none()
8315            && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
8316        #[cfg(not(target_os = "macos"))]
8317        let greedy_dev = false;
8318        let mut dev_ids: Vec<u32> = Vec::new();
8319        #[cfg(target_os = "macos")]
8320        let verify_outcome = if metal_native {
8321            let lm = self.weights.lm_head.q1_parts()?;
8322            let n_score = self.vocab_size.min(lm_rows);
8323            self.try_batch_graph_metal(
8324                &mut hiddens,
8325                &positions,
8326                b,
8327                Some((lm, &final_norm, &mut logits)),
8328                if greedy_dev {
8329                    Some((n_score, &mut dev_ids))
8330                } else {
8331                    None
8332                },
8333            )
8334        } else {
8335            self.try_batch_graph_wgpu(
8336                &mut hiddens,
8337                &positions,
8338                b,
8339                Some(crate::gpu::SpecTail {
8340                    lm: lm_gw,
8341                    lm_rows,
8342                    final_norm: &final_norm,
8343                    logits_out: &mut logits,
8344                    pick: None,
8345                }),
8346            )
8347        };
8348        #[cfg(not(target_os = "macos"))]
8349        let verify_outcome = self.try_batch_graph_wgpu(
8350            &mut hiddens,
8351            &positions,
8352            b,
8353            Some(crate::gpu::SpecTail {
8354                lm: lm_gw,
8355                lm_rows,
8356                final_norm: &final_norm,
8357                logits_out: &mut logits,
8358                pick: None,
8359            }),
8360        );
8361        match verify_outcome {
8362            crate::gpu::BatchGraphOutcome::Completed => {}
8363            crate::gpu::BatchGraphOutcome::Declined => {
8364                // The verifier refused before admission.  Its draft MTP
8365                // rows are still device-resident, so rewind the separate
8366                // mirror before the caller takes the exact one-token path.
8367                m.kv.truncate_last(k_spec);
8368                if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
8369                    self.clear_sequence_state();
8370                    self.graph_failed
8371                        .store(true, std::sync::atomic::Ordering::Relaxed);
8372                    self.cancel
8373                        .store(true, std::sync::atomic::Ordering::Relaxed);
8374                    tracing::error!("MTP graph mirror rewind failed after verify decline");
8375                }
8376                return None;
8377            }
8378            crate::gpu::BatchGraphOutcome::Failed => {
8379                // A failed batch may have advanced trunk/GDN state.  Clear
8380                // both mirrors and preserve the terminal outcome rather than
8381                // falling through to stale CPU state.
8382                self.clear_sequence_state();
8383                self.graph_failed
8384                    .store(true, std::sync::atomic::Ordering::Relaxed);
8385                self.cancel
8386                    .store(true, std::sync::atomic::Ordering::Relaxed);
8387                tracing::error!("MTP verify batch graph failed after admission");
8388                return None;
8389            }
8390        }
8391        // `CMF_METAL_VERIFY_CHECK=1`: run the same b tokens through the
8392        // plain per-token path and compare each row's argmax + logits with
8393        // the verify's — the bring-up oracle for the batched graph. The
8394        // plain forwards mutate the CPU state; it is snapshotted and put
8395        // back, and the K/V mirrors re-pointed, before the round goes on.
8396        #[cfg(target_os = "macos")]
8397        if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
8398            let snap: Vec<Vec<f32>> = self
8399                .kv_cache
8400                .layers
8401                .iter()
8402                .map(|l| l.linear_state.clone())
8403                .collect();
8404            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8405            let toks: Vec<u32> = std::iter::once(t_next)
8406                .chain(drafts.iter().copied())
8407                .collect();
8408            let want_save = self.graph_want_logits;
8409            self.graph_want_logits = false;
8410            for (i, &t) in toks.iter().enumerate() {
8411                let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8412                let _ = self.graph_logits.take();
8413                // CMF_SPEC_PLAIN_HIDDEN=1: the next round drafts from the
8414                // plain path's hidden instead of the verify's (an experiment
8415                // on the chain's sensitivity to the half-GEMM noise)
8416                if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
8417                    hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
8418                }
8419                let ref_lg = self.logits_from_hidden(&hi);
8420                let row = &logits[i * lm_rows..(i + 1) * lm_rows];
8421                let ra = sampler::argmax(&ref_lg);
8422                let va = sampler::argmax(row);
8423                let mut md = 0f32;
8424                let mut rms = 0f64;
8425                for j in 0..lm_rows.min(ref_lg.len()) {
8426                    let d = (ref_lg[j] - row[j]).abs();
8427                    md = md.max(d);
8428                    rms += (d as f64) * (d as f64);
8429                }
8430                let mut hd = 0f32;
8431                for j in 0..self.hidden_size {
8432                    hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
8433                }
8434                eprintln!(
8435                    "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
8436                    next_pos + i,
8437                    if ra == va { "OK" } else { "MISMATCH" },
8438                    (rms / lm_rows as f64).sqrt()
8439                );
8440            }
8441            self.graph_want_logits = want_save;
8442            // restore IN PLACE: the pending verify graph wraps these very
8443            // allocations (zero-copy) — replacing the Vec would strand it
8444            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8445                if l.linear_state.len() == st.len() {
8446                    l.linear_state.copy_from_slice(&st);
8447                } else {
8448                    l.linear_state = st;
8449                }
8450            }
8451            for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
8452                let extra = l.seq_len.saturating_sub(n0);
8453                if extra > 0 {
8454                    l.truncate_last(extra);
8455                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
8456                }
8457            }
8458        }
8459        let t_verify = t_round.elapsed();
8460        let sub_verify = subs();
8461        // Acceptance. Greedy: row i's argmax is the trunk's token after
8462        // input i. Sampling: accept draft i with min(1, p_i/q_i), and on
8463        // the first rejection draw the correction from max(0, p_i − q_i)
8464        // — that token is committed by the loop top as-is (spec_forced).
8465        let mut a = 0usize;
8466        let mut forced: Option<u32> = None;
8467        let ids: Vec<u32> = if sparse {
8468            let mut p = std::mem::take(&mut self.spec_ps);
8469            let mut res = std::mem::take(&mut self.spec_ress);
8470            while a < k_spec {
8471                let ok = sampler::sparse_distribution_into(
8472                    &logits[a * lm_rows..(a + 1) * lm_rows],
8473                    &cfg,
8474                    all_ids,
8475                    &mut self.sampler_scratch,
8476                    self.pool.as_deref(),
8477                    &mut p,
8478                );
8479                if !ok {
8480                    let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
8481                    p.clear();
8482                    p.push((t, 1.0));
8483                }
8484                match sampler::spec_accept_or_correct_sparse(
8485                    &p,
8486                    &self.spec_qs[a],
8487                    drafts[a],
8488                    &mut self.rng,
8489                    &mut res,
8490                ) {
8491                    None => {
8492                        all_ids.push(drafts[a]);
8493                        a += 1;
8494                    }
8495                    Some(c) => {
8496                        forced = Some(c);
8497                        break;
8498                    }
8499                }
8500            }
8501            all_ids.truncate(base_len);
8502            self.spec_ps = p;
8503            self.spec_ress = res;
8504            drafts.clone()
8505        } else if sampling {
8506            let mut p = std::mem::take(&mut self.spec_p);
8507            let mut res = std::mem::take(&mut self.spec_res);
8508            while a < k_spec {
8509                sampler::distribution_into(
8510                    &logits[a * lm_rows..(a + 1) * lm_rows],
8511                    &cfg,
8512                    all_ids,
8513                    &mut self.sampler_scratch,
8514                    self.pool.as_deref(),
8515                    &mut p,
8516                );
8517                match sampler::spec_accept_or_correct(
8518                    &p,
8519                    &self.spec_q[a],
8520                    drafts[a],
8521                    &mut self.rng,
8522                    &mut res,
8523                    self.pool.as_deref(),
8524                ) {
8525                    None => {
8526                        all_ids.push(drafts[a]);
8527                        a += 1;
8528                    }
8529                    Some(c) => {
8530                        forced = Some(c);
8531                        break;
8532                    }
8533                }
8534            }
8535            all_ids.truncate(base_len);
8536            self.spec_p = p;
8537            self.spec_res = res;
8538            // the accepted drafts ARE the verified tokens after inputs 0..a
8539            drafts.clone()
8540        } else if greedy_pen {
8541            // Row i's penalized argmax, penalties over the stream that
8542            // includes the accepted drafts before it — the plain loop's
8543            // exact arithmetic, one pass per row, no working copy.
8544            let mut ids: Vec<u32> = Vec::with_capacity(b);
8545            for i in 0..b {
8546                let t = sampler::argmax_penalized(
8547                    &logits[i * lm_rows..(i + 1) * lm_rows],
8548                    &cfg,
8549                    all_ids,
8550                    &mut self.sampler_scratch,
8551                    self.pool.as_deref(),
8552                );
8553                ids.push(t);
8554                if i < k_spec && t == drafts[i] {
8555                    all_ids.push(t);
8556                } else {
8557                    break;
8558                }
8559            }
8560            all_ids.truncate(base_len);
8561            while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8562                a += 1;
8563            }
8564            // rows past the first mismatch were never scored; the loop
8565            // top re-samples the last verified row itself.
8566            ids
8567        } else if greedy_dev && dev_ids.len() == b {
8568            let ids = std::mem::take(&mut dev_ids);
8569            while a < k_spec && ids[a] == drafts[a] {
8570                a += 1;
8571            }
8572            ids
8573        } else {
8574            if logits.len() < b * lm_rows {
8575                // the device argmax was asked for and came back short:
8576                // no rows to fall back on — terminal like a failed batch
8577                self.clear_sequence_state();
8578                self.graph_failed
8579                    .store(true, std::sync::atomic::Ordering::Relaxed);
8580                self.cancel
8581                    .store(true, std::sync::atomic::Ordering::Relaxed);
8582                tracing::error!("Metal verify returned neither logits nor argmax ids");
8583                return None;
8584            }
8585            let ids: Vec<u32> = (0..b)
8586                .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8587                .collect();
8588            while a < k_spec && ids[a] == drafts[a] {
8589                a += 1;
8590            }
8591            ids
8592        };
8593        spec_stamp("acc");
8594        if spec_dbg {
8595            eprintln!(
8596                "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8597                drafts, ids
8598            );
8599        }
8600        // CMF_METAL_VERIFY_CHECK=2: the commit oracle — plain-forward the
8601        // a+1 accepted tokens from a snapshot, then diff the replayed GDN
8602        // states and the appended K/V rows against that.
8603        #[cfg(target_os = "macos")]
8604        let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8605            && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8606        {
8607            let snap: Vec<Vec<f32>> = self
8608                .kv_cache
8609                .layers
8610                .iter()
8611                .map(|l| l.linear_state.clone())
8612                .collect();
8613            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8614            let toks: Vec<u32> = std::iter::once(t_next)
8615                .chain(drafts.iter().copied())
8616                .collect();
8617            let want_save = self.graph_want_logits;
8618            self.graph_want_logits = false;
8619            for (i, &t) in toks.iter().take(a + 1).enumerate() {
8620                let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8621                let _ = self.graph_logits.take();
8622            }
8623            self.graph_want_logits = want_save;
8624            let plain_states: Vec<Vec<f32>> = self
8625                .kv_cache
8626                .layers
8627                .iter()
8628                .map(|l| l.linear_state.clone())
8629                .collect();
8630            let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8631            let mut rows = Vec::new();
8632            for (li, (l, n0)) in self
8633                .kv_cache
8634                .layers
8635                .iter_mut()
8636                .zip(attn_lens.iter())
8637                .enumerate()
8638            {
8639                let extra = l.seq_len.saturating_sub(*n0);
8640                if extra > 0 {
8641                    let mut kk = Vec::new();
8642                    let mut vv = Vec::new();
8643                    for g in 0..nkv {
8644                        kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8645                        vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8646                    }
8647                    rows.push((li, kk, vv));
8648                    l.truncate_last(extra);
8649                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8650                }
8651            }
8652            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8653                if l.linear_state.len() == st.len() {
8654                    l.linear_state.copy_from_slice(&st);
8655                } else {
8656                    l.linear_state = st;
8657                }
8658            }
8659            Some((plain_states, rows))
8660        } else {
8661            None
8662        };
8663        let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8664        // Metal: the MTP cache cut and the round's warm-up SUBMIT come
8665        // BEFORE the trunk commit, so the warm-up's command buffer is
8666        // queued ahead of the GDN replay (second queue) and its wait
8667        // below no longer sits behind the replay — measured: the warm-up's
8668        // wait grew with the accepted count exactly like the replay does
8669        // (8 ms at a=1, 17 ms at a=3, 25 ms at a=5 for ~2 ms of its own
8670        // work). The replay now overlaps the warm-up's readback, the
8671        // round's return and the next draft chain.
8672        #[cfg(target_os = "macos")]
8673        let mut warm_pending: Option<MetalWarmPending> = None;
8674        #[cfg(target_os = "macos")]
8675        if metal_native {
8676            m.kv.truncate_last(k_spec.saturating_sub(1));
8677            if self.mtp_graph_mode == Some(true) {
8678                // the mirror rows below the cut are the CPU rows: re-point,
8679                // no re-upload
8680                crate::gpu_metal::kv_mirror_set_stored(
8681                    self.mtp_kv_id(),
8682                    Self::MTP_LAYER_BASE,
8683                    m.kv.seq_len,
8684                );
8685                if !warm_off && a > 0 {
8686                    let pairs: Vec<(&[f32], u32)> = (0..a)
8687                        .map(|j| {
8688                            (
8689                                &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8690                                ids[j],
8691                            )
8692                        })
8693                        .collect();
8694                    warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8695                }
8696            }
8697            spec_stamp("c.wsub");
8698        }
8699        // a fully-accepted round needs no restore: every input was real.
8700        #[cfg(target_os = "macos")]
8701        if metal_native {
8702            // the Metal verify never wrote its states: the commit replays the
8703            // accepted prefix into the CPU owners and appends the K/V rows
8704            if !self.metal_verify_commit(a) {
8705                self.clear_sequence_state();
8706                self.graph_failed
8707                    .store(true, std::sync::atomic::Ordering::Relaxed);
8708                self.cancel
8709                    .store(true, std::sync::atomic::Ordering::Relaxed);
8710                tracing::error!("Metal verify state/KV handoff failed after admission");
8711                return None;
8712            }
8713            if let Some((plain_states, rows)) = commit_ref {
8714                crate::gpu_metal::queue_fence();
8715                // the commit's replay runs on the second queue: collect it
8716                // before the oracle reads the CPU owners it writes into
8717                let _ = crate::gpu_metal::wait_replay();
8718                let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8719                let mut worst_s = 0f32;
8720                let mut worst_li = 0usize;
8721                for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8722                    if l.linear_state.len() != ps.len() || ps.is_empty() {
8723                        continue;
8724                    }
8725                    let d = l
8726                        .linear_state
8727                        .iter()
8728                        .zip(ps)
8729                        .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8730                    let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8731                    let rel = d / n.max(1e-6);
8732                    if rel > worst_s {
8733                        worst_s = rel;
8734                        worst_li = li;
8735                    }
8736                }
8737                let mut worst_k = 0f32;
8738                for (li, kk, vv) in &rows {
8739                    let l = &self.kv_cache.layers[*li];
8740                    let n0 = l.seq_len - (kk.len() / (nkv * hd));
8741                    let mut ck = Vec::new();
8742                    let mut cv = Vec::new();
8743                    for g in 0..nkv {
8744                        ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8745                        cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8746                    }
8747                    if ck.len() == kk.len() {
8748                        let dk = ck
8749                            .iter()
8750                            .zip(kk)
8751                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8752                        let dv = cv
8753                            .iter()
8754                            .zip(vv)
8755                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8756                        worst_k = worst_k.max(dk).max(dv);
8757                    } else {
8758                        eprintln!(
8759                            "commit-check L{li}: kv row count mismatch {} vs {}",
8760                            ck.len(),
8761                            kk.len()
8762                        );
8763                    }
8764                }
8765                eprintln!(
8766                    "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}"
8767                );
8768            }
8769        }
8770        if !metal_native && a + 1 < b {
8771            let expected_gdn_layers = self.graph_gdn_layer_count();
8772            if expected_gdn_layers > 0
8773                && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8774            {
8775                self.clear_sequence_state();
8776                self.graph_failed
8777                    .store(true, std::sync::atomic::Ordering::Relaxed);
8778                self.cancel
8779                    .store(true, std::sync::atomic::Ordering::Relaxed);
8780                tracing::error!("GDN speculative restore failed after verify");
8781                return None;
8782            }
8783        }
8784        if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8785            // The verify graph committed the full batch, but one of its
8786            // persistent Full-attention mirrors could not be re-pointed to
8787            // the accepted prefix.  Treat that as terminal state failure;
8788            // an exact CPU fallback would otherwise consume stale GDN/KV.
8789            self.clear_sequence_state();
8790            self.graph_failed
8791                .store(true, std::sync::atomic::Ordering::Relaxed);
8792            self.cancel
8793                .store(true, std::sync::atomic::Ordering::Relaxed);
8794            tracing::error!("trunk graph KV rewind failed after speculative verify");
8795            return None;
8796        }
8797        *accepted += a;
8798        // MTP cache: keep the first draft row (its inputs were real), drop
8799        // the chain's, then append the verified pairs the round produced.
8800        // Each of those is a whole MTP block on the per-op path and they
8801        // cost 5.8 ms of a 69 ms round at k=3 — a third of what the
8802        // round's own draft costs. PRICED, and they earn it: skipping
8803        // them (`CMF_SPEC_WARM=0`) drops acceptance from 89% to 81% at
8804        // k=3 and 85% to 74% at k=4, and the tok/s goes nowhere at k=3
8805        // (50.3 against 50.5) and backwards at k=4 (48.1 against 50.1).
8806        // The knob stays so the next person can re-price it after the
8807        // warms are batched instead of assuming either way.
8808        if !metal_native {
8809            // (Metal cut its MTP cache before the trunk commit, above)
8810            m.kv.truncate_last(k_spec.saturating_sub(1));
8811        }
8812        spec_stamp("c.trunc");
8813        if !metal_native
8814            && self.mtp_graph_mode == Some(true)
8815            && !self.rewind_mtp_graph_mirror(next_pos)
8816        {
8817            // The graph draft was admitted, so inability to move its cursor
8818            // back to the real anchor is a state failure, not a capability
8819            // refusal.  Do not warm or continue with a stale mirror.
8820            self.clear_sequence_state();
8821            self.graph_failed
8822                .store(true, std::sync::atomic::Ordering::Relaxed);
8823            self.cancel
8824                .store(true, std::sync::atomic::Ordering::Relaxed);
8825            tracing::error!("MTP graph mirror rewind failed after verify commit");
8826            return None;
8827        }
8828        if !warm_off && a > 0 {
8829            // Graph arm: all accepted pairs in ONE batched run over the
8830            // MTP block; the token graph one by one if the batch declines.
8831            let mut warmed = false;
8832            #[cfg(target_os = "macos")]
8833            if metal_native && self.mtp_graph_mode == Some(true) {
8834                // the batched warm-up was submitted before the trunk
8835                // commit: collect it here; one by one on the token graph
8836                // if it declined (or failed)
8837                warmed = match warm_pending.take() {
8838                    Some(p) => self.mtp_warm_batch_finish(m, p),
8839                    None => false,
8840                };
8841                if !warmed {
8842                    warmed = true;
8843                    for j in 0..a {
8844                        let row =
8845                            hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8846                        if self
8847                            .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8848                            .is_none()
8849                        {
8850                            warmed = false;
8851                            break;
8852                        }
8853                    }
8854                }
8855            }
8856            if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8857                let rows: Vec<Vec<f32>> = (0..a)
8858                    .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8859                    .collect();
8860                let pairs: Vec<(&[f32], u32)> = rows
8861                    .iter()
8862                    .zip(ids.iter())
8863                    .map(|(r, &t)| (r.as_slice(), t))
8864                    .collect();
8865                match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8866                    Ok(()) => warmed = true,
8867                    Err(err) => {
8868                        // A warm-up failure after graph admission cannot
8869                        // fall back to `mtp_warm`: the detached CPU cache is
8870                        // not authoritative for the device mirror.  Mark it
8871                        // terminal so the generation caller clears state and
8872                        // returns instead of drafting from stale attention.
8873                        tracing::error!("{err}");
8874                        self.clear_sequence_state();
8875                        self.graph_failed
8876                            .store(true, std::sync::atomic::Ordering::Relaxed);
8877                        self.cancel
8878                            .store(true, std::sync::atomic::Ordering::Relaxed);
8879                        return None;
8880                    }
8881                }
8882            }
8883            if !warmed {
8884                for j in 0..a {
8885                    let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8886                    let row = row.to_vec();
8887                    self.mtp_warm(m, &row, ids[j], next_pos + j);
8888                }
8889            }
8890        }
8891        // The sampler's contract: logits of the LAST verified position —
8892        // unless a rejected draft already drew the correction, in which
8893        // case the loop top commits that token and samples nothing.
8894        spec_stamp("c.warm");
8895        if let Some(c) = forced {
8896            self.spec_forced = Some(c);
8897            self.graph_logits = None;
8898        } else if greedy_dev && logits.is_empty() {
8899            // the row's argmax IS the token the loop top would pick from
8900            // it (plain greedy, no penalties): commit it as forced
8901            self.spec_forced = Some(ids[a]);
8902            self.graph_logits = None;
8903        } else {
8904            let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8905            row.resize(self.vocab_size, 0.0);
8906            if let Some(c) = self.final_softcap {
8907                for l in row.iter_mut() {
8908                    *l = c * (*l / c).tanh();
8909                }
8910            }
8911            self.graph_logits = Some(row);
8912        }
8913        let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8914        spec_stamp("c.row");
8915        // Three phases, not two. The round's wall clock was 4 ms longer
8916        // than draft+verify and the difference had nowhere to be seen:
8917        // the accepted prefix re-runs the MTP block once per token to
8918        // keep the draft head's attention cache warm, and the GDN state
8919        // rolls back on any rejection. Both live here, after the verify.
8920        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8921            let end = subs();
8922            eprintln!(
8923                "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8924                 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8925                t_draft.as_secs_f64() * 1e3,
8926                sub_draft - sub0,
8927                (t_verify - t_draft).as_secs_f64() * 1e3,
8928                sub_verify - sub_draft,
8929                (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8930                end - sub_verify,
8931                self.draft_full_streak,
8932            );
8933        }
8934        // Native Metal's verify tile is flat in b (eight rows for the price
8935        // of one), so a shorter round only forfeits tokens — measured on
8936        // the M4: an essay round at k=2 still verified in 260 ms. The
8937        // adaptation is for cards whose verify grows with the rows.
8938        if k_env.is_none() && !metal_native && !k_capped {
8939            // Slow average and a wide band: a fast one oscillated 2↔3 on
8940            // an essay every other round (measured), which forfeits the
8941            // draft it just paid for.
8942            let f = a as f32 / k_spec.max(1) as f32;
8943            self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8944            let mut k_next = k_spec;
8945            if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8946                k_next = k_spec + 1;
8947            } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8948                k_next = k_spec - 1;
8949            }
8950            if k_next != k_spec {
8951                self.spec_acc_ewma = 0.6;
8952                if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8953                    eprintln!("spec-k: {k_spec} → {k_next}");
8954                }
8955            }
8956            self.spec_k_adapt = Some(k_next);
8957        }
8958        spec_stamp("end");
8959        Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8960    }
8961
8962    /// Micro-benchmark: two single-position forwards vs one fused pair
8963    /// from the current cache state (KV rewound after each probe).
8964    /// Returns (two_singles_ms, fused_pair_ms) per probe, or the (0, 0)
8965    /// sentinel when this model has no pair path to measure — the same
8966    /// answer the o1 arm gives, and the bench prints it the same way.
8967    /// (An architecture that loads its own layers leaves `weights.layers`
8968    /// empty; walking it here was an index panic, found by `bench` on
8969    /// deepseek_v4.)
8970    pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8971        if !self.pair_supported() {
8972            return (0.0, 0.0);
8973        }
8974        // A GDN model that needs an automatic device prefix never uses this
8975        // fused-pair path in normal generation: its recurrence is serial and
8976        // the speculative pair is disabled by default.  More importantly,
8977        // this diagnostic suppresses the token graph, so measuring it would
8978        // stream the non-resident tail through per-op Vulkan just to print a
8979        // non-production number.  Leave inference untouched and report the
8980        // established unavailable sentinel instead.
8981        if self.gdn_cfg.is_some()
8982            && self.graph_prefill_preferred()
8983            && self.automatic_gpu_prefix().is_some()
8984        {
8985            tracing::info!(
8986                "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8987            );
8988            return (0.0, 0.0);
8989        }
8990        // This is a host-side pair micro-benchmark. It truncates the host KV
8991        // after every probe, so letting the whole-token graph participate
8992        // would leave its device GDN/KV mirror ahead of the next probe and
8993        // poison the process-wide graph verdict before the real generation
8994        // benchmark starts. Keep the existing per-op/GPU arithmetic while
8995        // suppressing only the stateful token graph for this measurement.
8996        let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8997        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8998        let emb1 = self.embed_single(1);
8999        let emb2 = self.embed_single(2);
9000        let pos = self.kv_cache.seq_len();
9001
9002        let t0 = std::time::Instant::now();
9003        for _ in 0..iters {
9004            let _ = self.forward_layers(&emb1, pos, None);
9005            let _ = self.forward_layers(&emb2, pos + 1, None);
9006            for l in &mut self.kv_cache.layers {
9007                l.truncate_last(2);
9008            }
9009        }
9010        let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
9011
9012        let t1 = std::time::Instant::now();
9013        for _ in 0..iters {
9014            let _ = self.forward_pair(&emb1, &emb2, pos);
9015            for l in &mut self.kv_cache.layers {
9016                l.truncate_last(2);
9017            }
9018        }
9019        let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
9020        match graph_env {
9021            Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
9022            None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
9023        }
9024        (singles_ms, pair_ms)
9025    }
9026
9027    /// Fused two-position forward: weight rows are streamed from memory
9028    /// once per layer for both positions. Full layers → fused GQA pair;
9029    /// linear layers → vmf_phase pair (lane 2 state is tentative in the
9030    /// per-layer scratch until the draft is accepted).
9031    /// Whether the fused two-position path covers every layer kind in
9032    /// this model. MLA and KDA run per position (their pair arms are
9033    /// unreachable); the seq prefill falls back to singles for them.
9034    fn pair_supported(&self) -> bool {
9035        // An EMPTY layer stack means the architecture loaded its own and
9036        // this path has nothing to walk. Checking that directly, rather
9037        // than naming each such architecture, is what makes the guard hold
9038        // for the next one: `any()` over no layers is false, so a
9039        // feature-by-feature test says "supported" for a model that has no
9040        // layers here at all.
9041        !self.weights.layers.is_empty()
9042            && !self.per_token_stack()
9043            && !self
9044                .weights
9045                .layers
9046                .iter()
9047                .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
9048    }
9049
9050    fn forward_pair(
9051        &mut self,
9052        emb1: &[f32],
9053        emb2: &[f32],
9054        position: usize,
9055    ) -> (Vec<f32>, Vec<f32>) {
9056        // A two-token prompt starts here, not in the layer walk: decide the
9057        // MiMo placement before the pair's per-op MoE uploads any expert.
9058        self.mimo_moe_prepare();
9059        let mut h1 = emb1.to_vec();
9060        let mut h2 = emb2.to_vec();
9061        let (_nkv, _hd, hs, _rd, eps) = (
9062            self.num_kv_heads,
9063            self.head_dim,
9064            self.hidden_size,
9065            self.rotary_dim,
9066            self.rms_eps,
9067        );
9068        let pool = self.pool.clone();
9069        // `forward_pair` bypasses `forward_layers_span`, but its per-op
9070        // body must still obey the ordinary layer placement. In particular,
9071        // an over-capacity Vulkan model has a device prefix and a CPU tail;
9072        // letting the pair walk submit the tail's selected MoE experts would
9073        // stream the whole tail through the residency arena and defeat the
9074        // measured VRAM bound.
9075        let automatic_gpu_prefix = self.automatic_gpu_prefix();
9076
9077        for li in 0..self.num_layers {
9078            let _capacity_tail = automatic_gpu_prefix
9079                .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
9080                .map(|_| crate::gpu::enter_cpu_scope());
9081            // Also makes an explicit CMF_GPU_LAYERS split apply to fused
9082            // pairs, just as it does to the ordinary layer walk.
9083            crate::gpu::set_layer(li as i64);
9084            let lw = &self.weights.layers[self.phys_layer(li)];
9085            // Norms into pipeline scratch (4 allocs/layer on the MTP
9086            // decode hot path before this).
9087            inference::rms_norm_into(
9088                &h1,
9089                &lw.input_norm,
9090                self.rms_eps,
9091                self.norm_style,
9092                &mut self.ws.n1,
9093            );
9094            inference::rms_norm_into(
9095                &h2,
9096                &lw.input_norm,
9097                self.rms_eps,
9098                self.norm_style,
9099                &mut self.ws.n2,
9100            );
9101
9102            let (a1, a2) = match &lw.attn {
9103                AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
9104                AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
9105                AttnKind::Bounded(w) => {
9106                    // Two sequential positions of the bounded operator
9107                    // (the ring's causal order is the pair's order).
9108                    let rope = self
9109                        .bounded_rope
9110                        .clone()
9111                        .expect("bounded layer without an installed rotation table");
9112                    let cfg = crate::bounded::BoundedAttnCfg {
9113                        num_heads: self.num_heads,
9114                        num_kv_heads: self.num_kv_heads,
9115                        head_dim: self.head_dim,
9116                        hidden_size: hs,
9117                        scale: self.attn_scale,
9118                        rope: &rope,
9119                        pool: pool.as_deref(),
9120                    };
9121                    let a1 = crate::bounded::bounded_attention(
9122                        &self.ws.n1,
9123                        w,
9124                        &mut self.kv_cache.layers[li],
9125                        &cfg,
9126                    );
9127                    let a2 = crate::bounded::bounded_attention(
9128                        &self.ws.n2,
9129                        w,
9130                        &mut self.kv_cache.layers[li],
9131                        &cfg,
9132                    );
9133                    (a1, a2)
9134                }
9135                AttnKind::Linear(w) => {
9136                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
9137                    let layer = &mut self.kv_cache.layers[li];
9138                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9139                    vmf_phase_pair(
9140                        &self.ws.n1,
9141                        &self.ws.n2,
9142                        w,
9143                        &cfg,
9144                        state,
9145                        scratch,
9146                        self.pool.as_deref(),
9147                    )
9148                }
9149                AttnKind::LinearGdn(w) => {
9150                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9151                    let layer = &mut self.kv_cache.layers[li];
9152                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9153                    gdn_pair(
9154                        &self.ws.n1,
9155                        &self.ws.n2,
9156                        w,
9157                        &cfg,
9158                        state,
9159                        scratch,
9160                        self.pool.as_deref(),
9161                    )
9162                }
9163                AttnKind::ShortConv(w) => {
9164                    let cfg = self
9165                        .short_conv_cfg
9166                        .expect("short-conv layer without short_conv_cfg");
9167                    let layer = &mut self.kv_cache.layers[li];
9168                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9169                    short_conv_pair(
9170                        &self.ws.n1,
9171                        &self.ws.n2,
9172                        w,
9173                        &cfg,
9174                        state,
9175                        scratch,
9176                        self.pool.as_deref(),
9177                    )
9178                }
9179                AttnKind::Full {
9180                    wq,
9181                    wk,
9182                    wv,
9183                    wo,
9184                    q_norm,
9185                    k_norm,
9186                    output_gate,
9187                    softplus_gate,
9188                    bias,
9189                } => {
9190                    let inv_freq_l = self.layer_inv_freq(li);
9191                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9192                    let cfg = QwenAttnCfg {
9193                        mrope: None,
9194                        num_heads: self.layer_num_heads(li),
9195                        num_kv_heads: nkv_l,
9196                        head_dim: hd_l,
9197                        hidden_size: hs,
9198                        position,
9199                        inv_freq: &inv_freq_l,
9200                        rotary_dim: rd_l,
9201                        scale: self.attn_scale,
9202                        softcap: self.attn_softcap,
9203                        window: self.layer_window(li),
9204                        v_norm: self.attn_v_norm,
9205                        qk_norm_after_rope: self.qk_norm_after_rope,
9206                        gate_sigmoid: self.proj_gate_sigmoid,
9207                        q_norm: q_norm.as_deref(),
9208                        k_norm: k_norm.as_deref(),
9209                        output_gate: *output_gate,
9210                        softplus_gate: softplus_gate
9211                            .as_ref()
9212                            .map(|(gate, per_head)| (gate, *per_head)),
9213                        rope_scale: self.layer_rope_scale(li),
9214                        bias: bias
9215                            .as_ref()
9216                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9217                        rms_eps: eps,
9218                        norm_style: self.norm_style,
9219                        pool: pool.as_deref(),
9220                        v_head_dim: self.layer_v_dim(li),
9221                    };
9222                    attention::qwen_attention_pair(
9223                        &self.ws.n1,
9224                        &self.ws.n2,
9225                        wq,
9226                        wk,
9227                        wv,
9228                        wo,
9229                        &mut self.kv_cache.layers[li],
9230                        &cfg,
9231                    )
9232                }
9233            };
9234            let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
9235                Some(w) => (
9236                    inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
9237                    inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
9238                ),
9239                None => (a1, a2),
9240            };
9241            for i in 0..self.hidden_size {
9242                h1[i] += a1[i];
9243                h2[i] += a2[i];
9244            }
9245            let (mut a1, mut a2) = (a1, a2);
9246            attention::recycle_buf(&mut a1);
9247            attention::recycle_buf(&mut a2);
9248
9249            let lw = &self.weights.layers[self.phys_layer(li)];
9250            inference::rms_norm_into(
9251                &h1,
9252                &lw.post_norm,
9253                self.rms_eps,
9254                self.norm_style,
9255                &mut self.ws.p1,
9256            );
9257            inference::rms_norm_into(
9258                &h2,
9259                &lw.post_norm,
9260                self.rms_eps,
9261                self.norm_style,
9262                &mut self.ws.p2,
9263            );
9264            let (f1, f2) = match &lw.ffn {
9265                // Dual-branch layers need the raw residuals — run the
9266                // two positions through the same fn decode uses.
9267                FfnKind::DenseMoe(dm) => (
9268                    dense_moe_ffn(
9269                        dm,
9270                        &self.ws.p1,
9271                        &h1,
9272                        self.rms_eps,
9273                        self.norm_style,
9274                        self.pool.as_deref(),
9275                    ),
9276                    dense_moe_ffn(
9277                        dm,
9278                        &self.ws.p2,
9279                        &h2,
9280                        self.rms_eps,
9281                        self.norm_style,
9282                        self.pool.as_deref(),
9283                    ),
9284                ),
9285                FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
9286                    moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
9287                    moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
9288                ),
9289                _ => ffn_forward_pair(
9290                    &lw.ffn,
9291                    &self.ws.p1,
9292                    &self.ws.p2,
9293                    self.pool.as_deref(),
9294                    None,
9295                ),
9296            };
9297            let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
9298                Some(w) => (
9299                    inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
9300                    inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
9301                ),
9302                None => (f1, f2),
9303            };
9304            for i in 0..self.hidden_size {
9305                h1[i] += f1[i];
9306                h2[i] += f2[i];
9307            }
9308            let (mut f1, mut f2) = (f1, f2);
9309            attention::recycle_buf(&mut f1);
9310            attention::recycle_buf(&mut f2);
9311            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
9312                for i in 0..self.hidden_size {
9313                    h1[i] *= sc;
9314                    h2[i] *= sc;
9315                }
9316            }
9317            // Looped Transformer: apply final norm at the end of each loop iteration.
9318            if self.is_loop_end(li) && li + 1 < self.num_layers {
9319                h1 = inference::rms_norm(
9320                    &h1,
9321                    &self.weights.final_norm,
9322                    self.rms_eps,
9323                    self.norm_style,
9324                );
9325                h2 = inference::rms_norm(
9326                    &h2,
9327                    &self.weights.final_norm,
9328                    self.rms_eps,
9329                    self.norm_style,
9330                );
9331            }
9332        }
9333        crate::gpu::set_layer(-1); // final/O(1) work is outside layer placement
9334        // Real O(1) prefill pairs may also carry tentative lane-2 recurrent
9335        // state. Commit it before publishing the transition epoch so the
9336        // next serial/device row cannot observe a new attention epoch with an
9337        // old GDN state. Speculative pairs run only when O(1) is inactive and
9338        // retain their existing caller-controlled commit/rollback semantics.
9339        if self.o1_active() {
9340            self.commit_linear_scratch();
9341        }
9342        self.o1_progress();
9343        self.swa_trim_tails();
9344        (h1, h2)
9345    }
9346
9347    /// Commit lane-2 linear states after an accepted draft.
9348    fn commit_linear_scratch(&mut self) {
9349        for layer in &mut self.kv_cache.layers {
9350            if !layer.linear_scratch.is_empty() {
9351                std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
9352                layer.linear_scratch.clear();
9353            }
9354        }
9355    }
9356
9357    /// This is the encoder counterpart to `forward_ids`: it runs a fresh
9358    /// sequence through the existing full/GDN hybrid stack and returns the
9359    /// final RMS-normalized hidden row for *every* token.  It is used by
9360    /// component pipelines whose text tower intentionally has no LM head
9361    /// (such as an image-conditioner Qwen3.5 model).  The sequence state is
9362    /// always released before returning, so a large text tower can be dropped
9363    /// immediately after conditioning a subsequent stage.
9364    pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9365        #[cfg(target_os = "macos")]
9366        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9367        if ids.is_empty() {
9368            return Err("empty id sequence".to_string());
9369        }
9370
9371        self.nll_begin()?;
9372        let result = (|| {
9373            let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9374            self.nll_check_graph("encoder prefill", 0)?;
9375            let expected = ids.len() * self.hidden_size;
9376            if hidden.len() != expected {
9377                return Err(format!(
9378                    "encoder prefill returned {} values for {} rows of hidden size {}",
9379                    hidden.len(),
9380                    ids.len(),
9381                    self.hidden_size
9382                ));
9383            }
9384
9385            let mut out = vec![0.0f32; expected];
9386            for (src, dst) in hidden
9387                .chunks_exact(self.hidden_size)
9388                .zip(out.chunks_exact_mut(self.hidden_size))
9389            {
9390                inference::rms_norm_into(
9391                    src,
9392                    &self.weights.final_norm,
9393                    self.rms_eps,
9394                    self.norm_style,
9395                    dst,
9396                );
9397            }
9398            Ok(out)
9399        })();
9400        self.nll_end();
9401        result
9402    }
9403
9404    /// The last layer's hidden row (before the final norm) for every token
9405    /// of `ids`, from a fresh sequence, through the whole-stack prefill (the
9406    /// device chunk graph where there is one) — what a distillation cache of
9407    /// teacher outputs needs, at prefill speed.
9408    pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9409        #[cfg(target_os = "macos")]
9410        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9411        if ids.is_empty() {
9412            return Err("empty id sequence".to_string());
9413        }
9414        self.nll_begin()?;
9415        let result = (|| {
9416            let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9417            self.nll_check_graph("hidden prefill", 0)?;
9418            if hidden.len() != ids.len() * self.hidden_size {
9419                return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
9420            }
9421            Ok(hidden)
9422        })();
9423        self.nll_end();
9424        result
9425    }
9426
9427    /// Forward a full id sequence from a fresh cache and return the
9428    /// logits after the last position (golden-parity harness, bench).
9429    pub fn forward_ids(
9430        &mut self,
9431        ids: &[u32],
9432        task_mask: Option<&TaskMask>,
9433    ) -> Result<Vec<f32>, String> {
9434        #[cfg(target_os = "macos")]
9435        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9436        if ids.is_empty() {
9437            return Err("empty id sequence".to_string());
9438        }
9439        self.clear_sequence_state();
9440        self.check_forward_graph("forward_ids setup", 0)?;
9441        if task_mask.is_none() {
9442            self.o1_begin();
9443        }
9444        let mut hidden = vec![0.0f32; self.hidden_size];
9445        let mut pos = 0usize;
9446        if let Some(b) = &mut self.dsv41 {
9447            let pool = self.pool.clone();
9448            let mut logits = Vec::new();
9449            crate::dsv41::forward_chunk(
9450                &b.0,
9451                &b.1,
9452                &b.2,
9453                &mut b.3,
9454                ids,
9455                0,
9456                pool.as_deref(),
9457                &mut logits,
9458            );
9459            if let Err(err) = self.o1_seal_checked() {
9460                self.clear_sequence_state();
9461                return Err(err);
9462            }
9463            return Ok(logits);
9464        }
9465        // Same routing predicate generation uses. Two reasons it must be
9466        // the same one: (1) a GDN hybrid's recurrent state is GPU-
9467        // resident, and a batched CPU prefill would build it on the host
9468        // only — decode then reads buffers the prefill never wrote;
9469        // (2) bench times THIS function and calls the result "prefill",
9470        // so a different path here reports a number production never
9471        // sees (W2 on 2×5090: 8.7 tok/s reported against 125 real).
9472        // Gemma-4: generation's batched-graph ingest (`g4_batch_ingest`),
9473        // so bench and the parity tools time and score the route run/serve
9474        // take; a refusal leaves the rest to the chunked host prefill.
9475        #[cfg(not(target_os = "macos"))]
9476        if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
9477            let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
9478                Ok(r) => r,
9479                Err(e) => {
9480                    self.clear_sequence_state();
9481                    return Err(e);
9482                }
9483            };
9484            if n > 0 {
9485                hidden.copy_from_slice(&last);
9486                pos = n;
9487            }
9488        }
9489        if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
9490            // prefill-GEMM in chunks; only the last position's hidden is
9491            // needed. (o1-compatible: the batch path attends per position
9492            // through qwen_attention, which carries the collection hook.)
9493            let chunk = self.prefill_chunk();
9494            let hs = self.hidden_size;
9495            while pos < ids.len() {
9496                let end = (pos + chunk).min(ids.len());
9497                let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9498                self.check_forward_graph("forward_ids batched prefill", end - 1)?;
9499                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
9500                pos = end;
9501            }
9502        }
9503        // Same guards as generation's prefill — INCLUDING the graph one.
9504        // The CPU pair walk was intercepting positions that the resident
9505        // token graph would have run itself: on a GDN hybrid over wgpu
9506        // that is 89 ms of host forward against 7 ms of device submit,
9507        // and it made prefill look 12× slower than it is (W2 on an RTX
9508        // 5090, ctx 512: 11.2 tok/s with the walk, 136.6 without).
9509        // CMF_PAIR=0 opts out; a model whose layers live outside
9510        // `weights.layers` has no pair walk to take.
9511        if task_mask.is_none()
9512            && !self.graph_prefill_preferred()
9513            && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9514            && self.pair_supported()
9515        {
9516            while pos + 1 < ids.len() {
9517                let e1 = self.embed_single(ids[pos]);
9518                let e2 = self.embed_single(ids[pos + 1]);
9519                let (_, h2) = self.forward_pair(&e1, &e2, pos);
9520                self.check_forward_graph("forward_ids pair", pos + 1)?;
9521                self.commit_linear_scratch();
9522                hidden = h2;
9523                pos += 2;
9524            }
9525        }
9526        // Resident Embryo graph: the prompt in chunks of one submit each
9527        // (the same device state and logits as the per-position walk).
9528        if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9529            if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9530                self.graph_logits = Some(lg);
9531                hidden = vec![0.0; self.hidden_size];
9532                pos = ids.len();
9533            }
9534        }
9535        while pos < ids.len() {
9536            hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9537            self.check_forward_graph("forward_ids", pos)?;
9538            pos += 1;
9539        }
9540        if let Some(logits) = self.graph_logits.take() {
9541            // Resident stacks already applied final norm and their head in
9542            // the same submit; do not run a second norm/head over the zero
9543            // hidden sentinel returned by forward_layers_span.
9544            if let Err(err) = self.o1_seal_checked() {
9545                self.clear_sequence_state();
9546                return Err(err);
9547            }
9548            return Ok(logits);
9549        }
9550        // Harness contract: after forward_ids the cache is decode-ready —
9551        // under o1 that means sealed (bench measures the seal as part of
9552        // prefill, honestly).
9553        if let Err(err) = self.o1_seal_checked() {
9554            self.clear_sequence_state();
9555            return Err(err);
9556        }
9557        let normed = inference::rms_norm(
9558            &hidden,
9559            &self.weights.final_norm,
9560            self.rms_eps,
9561            self.norm_style,
9562        );
9563        Ok(self.lm_head_forward(&normed))
9564    }
9565
9566    /// Run the V4.1 stack one token at a time and retain logits for every
9567    /// position. This is a diagnostic surface for comparing a converted
9568    /// checkpoint with a tokenwise reference implementation.
9569    #[doc(hidden)]
9570    pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9571        #[cfg(target_os = "macos")]
9572        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9573        if ids.is_empty() {
9574            return Err("empty id sequence".to_string());
9575        }
9576        self.clear_sequence_state();
9577        self.dsv41
9578            .as_ref()
9579            .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9580        self.o1_begin();
9581        let rows = {
9582            let pool = self.pool.clone();
9583            let b = self
9584                .dsv41
9585                .as_mut()
9586                .expect("dsv41 checked above; state cannot change during forward");
9587            let mut rows = Vec::with_capacity(ids.len());
9588            for (position, &id) in ids.iter().enumerate() {
9589                let mut logits = Vec::new();
9590                crate::dsv41::forward_token(
9591                    &b.0,
9592                    &b.1,
9593                    &b.2,
9594                    &mut b.3,
9595                    id,
9596                    position,
9597                    pool.as_deref(),
9598                    &mut logits,
9599                );
9600                rows.push(logits);
9601            }
9602            rows
9603        };
9604        self.o1_seal();
9605        Ok(rows)
9606    }
9607
9608    /// GPTQ calibration of DeepSeek-V4.1 (`cortiq quantize-moe`): the MoE
9609    /// input rows and routed expert ids of layers `lo..hi` for `ids` (a fresh
9610    /// sequence), see [`crate::dsv41::Dsv41MoeCapture`].
9611    pub fn dsv41_moe_capture(
9612        &mut self,
9613        ids: &[u32],
9614        lo: usize,
9615        hi: usize,
9616    ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9617        #[cfg(target_os = "macos")]
9618        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9619        if ids.is_empty() {
9620            return Err("empty id sequence".to_string());
9621        }
9622        self.dsv41
9623            .as_ref()
9624            .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9625        self.clear_sequence_state();
9626        let res = {
9627            let pool = self.pool.clone();
9628            let b = self.dsv41.as_mut().expect("dsv41 checked above");
9629            crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9630        };
9631        self.clear_sequence_state();
9632        res
9633    }
9634
9635    /// Teacher-forced perplexity over a token sequence (phase-C gate:
9636    /// honest quant comparisons instead of prompt vibes).
9637    ///
9638    /// Attention is EXACT even on a model whose layers are flagged for
9639    /// the O(1) kernel — scoring the backbone is the default on purpose
9640    /// (it is the yardstick). `nll_ids_o1` scores the CONVERTED model.
9641    pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9642        let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9643        Ok((nll / cnt.max(1) as f64).exp())
9644    }
9645
9646    /// GPTQ calibration forward: `ids` through layers `0..upto` only, in
9647    /// layer-major chunks of `chunk` positions, with no vocabulary head and
9648    /// no scoring. The Hessian hook (`gptq_capture`) folds every
9649    /// projection's input on the way; the layers above the captured span
9650    /// and the head would only cost time. Falls back to a scored forward
9651    /// where the batched prefill does not apply.
9652    pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9653        if !self.can_prefill_batched() {
9654            return self.nll_ids_from(ids, 0).map(|_| ());
9655        }
9656        self.nll_begin()?;
9657        let result: Result<(), String> = (|| {
9658            let chunk = chunk.max(1);
9659            let mut pos = 0usize;
9660            while pos < ids.len() {
9661                let end = (pos + chunk).min(ids.len());
9662                let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9663                self.nll_check_graph("calibration prefill", pos)?;
9664                pos = end;
9665            }
9666            Ok(())
9667        })();
9668        self.nll_end();
9669        result
9670    }
9671
9672    /// Layer-streamed calibration of one fresh window: layers `from..mid` run
9673    /// on `input` (the hidden state entering layer `from`; `None` = the
9674    /// token embeddings of `ids`) and their output is returned, then layers
9675    /// `mid..upto` run on that output for the capture hooks (their output is
9676    /// dropped). A caller that keeps the returned state per window runs each
9677    /// layer once over the whole conversion instead of re-running the stack
9678    /// below every layer. With `mid == from` the input is returned as is
9679    /// (`None` input: an empty vector).
9680    pub fn calib_window_span(
9681        &mut self,
9682        ids: &[u32],
9683        input: Option<&[f32]>,
9684        from: usize,
9685        mid: usize,
9686        upto: usize,
9687    ) -> Result<Vec<f32>, String> {
9688        if !self.can_prefill_batched() {
9689            return Err("calib_window_span: this architecture has no batched prefill".into());
9690        }
9691        self.nll_begin()?;
9692        let result: Result<Vec<f32>, String> = (|| {
9693            let h = if mid > from {
9694                let inp = match input {
9695                    Some(h) => PrefillIn::Hidden(h),
9696                    None => PrefillIn::Ids(ids),
9697                };
9698                let h = self.prefill_batch_span(inp, 0, None, from, mid);
9699                self.nll_check_graph("calibration span", 0)?;
9700                h
9701            } else {
9702                input.map(<[f32]>::to_vec).unwrap_or_default()
9703            };
9704            if upto > mid {
9705                let inp = if mid > from || input.is_some() {
9706                    PrefillIn::Hidden(&h)
9707                } else {
9708                    PrefillIn::Ids(ids)
9709                };
9710                let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9711                self.nll_check_graph("calibration span", 0)?;
9712            }
9713            Ok(h)
9714        })();
9715        self.nll_end();
9716        result
9717    }
9718
9719    /// GPTQ calibration of a Flash-Next-class MoE (`qwen4_exp`): MoE input
9720    /// rows and router logits of layers `lo..hi` for `ids` (a fresh
9721    /// sequence), per layer `[ids.len()][hidden + n_experts]`.
9722    #[cfg(feature = "gpu")]
9723    pub fn qwen4_capture(
9724        &mut self,
9725        ids: &[u32],
9726        lo: usize,
9727        hi: usize,
9728    ) -> Result<Vec<Vec<f32>>, String> {
9729        let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9730        crate::qwen4_exp::capture_window(
9731            &b.0,
9732            &b.1,
9733            &b.2,
9734            &mut b.3,
9735            ids,
9736            lo,
9737            hi,
9738            &self.inv_freq,
9739            self.pool.as_deref(),
9740        )
9741    }
9742
9743    /// GPTQ calibration capture of an ordinary MoE stack (Qwen3.5/3.6
9744    /// MoE, Clover): `ids` from position 0 through the batched device graph
9745    /// in prefill-sized chunks, the MoE input rows and router logits of
9746    /// layers `lo..hi` copied out after routing. Returns per layer
9747    /// `[ids.len()][hidden + n_experts]`; the sequence state is left fresh.
9748    #[cfg(feature = "gpu")]
9749    pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9750        use crate::gpu_wgpu::clover as cap;
9751        let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9752        let ne = (lo..hi)
9753            .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9754                FfnKind::Moe(m) => Some(m.experts.len()),
9755                _ => None,
9756            })
9757            .ok_or("no MoE layer in the capture range")?;
9758        let chunk = self.prefill_chunk().max(1);
9759        self.clear_sequence_state();
9760        if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9761            return Err("capture unavailable (no wgpu device)".into());
9762        }
9763        let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9764        let res = (|| {
9765            for (ci, part) in ids.chunks(chunk).enumerate() {
9766                let mut hiddens = Vec::with_capacity(part.len() * hs);
9767                for &id in part {
9768                    hiddens.extend_from_slice(&self.embed_single(id));
9769                }
9770                let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9771                match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9772                    crate::gpu::BatchGraphOutcome::Completed => {}
9773                    _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9774                }
9775                let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9776                if written < hi - lo {
9777                    return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9778                }
9779                for (o, r) in out.iter_mut().zip(rows) {
9780                    o.extend_from_slice(&r);
9781                }
9782            }
9783            Ok(())
9784        })();
9785        cap::moe_capture_end();
9786        self.clear_sequence_state();
9787        res.map(|_| out)
9788    }
9789
9790    /// Expert directory triples (gate, up, down) of an ordinary MoE layer,
9791    /// routed and shared.
9792    pub fn moe_expert_dirs(
9793        &self,
9794        li: usize,
9795    ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9796        let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9797            return None;
9798        };
9799        let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9800        let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9801        let shared = match &m.shared {
9802            Some((se, _)) => Some(dirs(se)?),
9803            None => None,
9804        };
9805        Some((routed, shared))
9806    }
9807
9808    /// Expert directory triples of a `qwen4_exp` layer (routed, shared).
9809    pub fn qwen4_expert_dirs(
9810        &self,
9811        li: usize,
9812    ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9813        let b = self.qwen4_exp.as_ref()?;
9814        crate::qwen4_exp::expert_dirs(&b.1, li)
9815    }
9816
9817    /// Teacher-force the installed MTP draft stack and collect each draft
9818    /// FFN's own pre-down activation statistics.
9819    ///
9820    /// This is deliberately separate from [`Self::probe_ffn_mass`]: canonical
9821    /// MTP and MiMo sidecars can have independently compacted FFNs, and MiMo
9822    /// draft layers can differ in width from one another and from the trunk.
9823    /// The returned vector follows the actual draft stack (one entry for a
9824    /// canonical MTP head; one per MiMo draft layer). MiMo layer `k` replays
9825    /// the teacher-forced pairs `(hidden[j], ids[j + k + 1])`.
9826    ///
9827    /// Calibration starts from a fresh sequence and leaves the trunk, draft
9828    /// KVs, history, and device mirrors fresh on both success and error. It
9829    /// is therefore unsuitable for the middle of a conversation whose KV
9830    /// cache must be retained.
9831    pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9832        self.reset_mtp_probe_state();
9833        // The trunk runs as one prefill (the device graph where there is
9834        // one); the draft replays on the host under a CPU scope, where its
9835        // FFN input can be measured. This guard also suppresses gate/oracle
9836        // sparsity experiments for the duration so the statistics and
9837        // admission replay always execute the full dense FFN.
9838        let _exact_calibration = ExactMtpCalibrationGuard::enter();
9839        let result = if ids.len() < 2 {
9840            Err("MTP FFN probe needs at least two token ids".into())
9841        } else if ids
9842            .iter()
9843            .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9844        {
9845            Err("MTP FFN probe received a token id outside the embedding table".into())
9846        } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9847            // A synthetic pipeline can carry both mechanisms, but merging
9848            // unlike draft semantics into one compaction plan is unsafe.
9849            Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9850        } else if self.mimo_mtp.is_some() {
9851            let mut st = self
9852                .mimo_mtp
9853                .take()
9854                .expect("checked MiMo MTP before detaching it");
9855            let result = match self.probe_trunk_rows(ids) {
9856                Ok(trunk) => crate::gpu::cpu_scope(|| {
9857                    self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
9858                }),
9859                Err(e) => Err(e),
9860            };
9861            // The owner is detached during the replay, so the common trunk
9862            // reset cannot clear its KVs. Restore it even on an error.
9863            st.reset();
9864            self.mimo_mtp = Some(st);
9865            result
9866        } else if self.mtp.is_some() {
9867            let mut mtp = self
9868                .mtp
9869                .take()
9870                .expect("checked canonical MTP before detaching it");
9871            let result = match self.probe_trunk_rows(ids) {
9872                Ok(trunk) => crate::gpu::cpu_scope(|| {
9873                    self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
9874                }),
9875                Err(e) => Err(e),
9876            };
9877            mtp.kv.clear();
9878            self.mtp = Some(mtp);
9879            result
9880        } else {
9881            Err("MTP FFN probe: this pipeline has no draft stack".into())
9882        };
9883        // Cover every early/error arm above: no failed calibration can leak
9884        // a partially populated trunk or draft cache into the next request.
9885        self.reset_mtp_probe_state();
9886        result
9887    }
9888
9889    /// Reset every state owner touched by [`Self::probe_mtp_ffn`].
9890    fn reset_mtp_probe_state(&mut self) {
9891        self.clear_sequence_state();
9892        if let Some(mtp) = self.mtp.as_mut() {
9893            mtp.kv.clear();
9894        }
9895        if let Some(mimo) = self.mimo_mtp.as_mut() {
9896            mimo.reset();
9897        }
9898        self.mtp_graph_mode = None;
9899    }
9900
9901    /// The trunk's last hidden row (before the final norm) at every
9902    /// position a teacher-forced draft probe reads (`ids[..len-1]`), from
9903    /// the whole-stack prefill — the device graph where there is one. One
9904    /// position at a time through `forward_layers` on the host this was the
9905    /// probe's whole cost: a 14B's 96k calibration tokens ran for hours.
9906    pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9907        if ids.len() < 2 {
9908            return Err("MTP FFN probe needs at least two token ids".into());
9909        }
9910        let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
9911        self.clear_sequence_state();
9912        Ok(rows)
9913    }
9914
9915    /// Canonical one-layer MTP teacher-forced replay over the trunk rows
9916    /// of [`Self::probe_trunk_rows`]. Its cache advances for every row so
9917    /// later attention inputs match a real draft warm-up rather than a
9918    /// collection of independent one-token measurements. The FFN inputs are
9919    /// measured in batches: nothing after the FFN feeds a later row.
9920    fn mtp_ffn_probe_teacher_forced(
9921        &mut self,
9922        mtp: &mut MtpModule,
9923        ids: &[u32],
9924        trunk: &[f32],
9925    ) -> Result<Vec<MtpFfnProbe>, String> {
9926        let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9927            return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9928        };
9929        let inter = dense.gate_proj.rows();
9930        let (hs, n) = (self.hidden_size, ids.len() - 1);
9931        if trunk.len() != n * hs {
9932            return Err(format!(
9933                "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
9934                trunk.len()
9935            ));
9936        }
9937        let mut mass = vec![0.0f64; inter];
9938        let mut squared_mass = vec![0.0f64; inter];
9939        let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
9940        for position in 0..n {
9941            self.mtp_probe_ffn_input(
9942                mtp,
9943                &trunk[position * hs..(position + 1) * hs],
9944                ids[position + 1],
9945                position,
9946            )?;
9947            inputs.extend_from_slice(&self.ws.p1[..hs]);
9948            if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
9949                let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9950                    unreachable!("checked dense above");
9951                };
9952                accumulate_dense_ffn_activation_mass(
9953                    dense,
9954                    &inputs,
9955                    inputs.len() / hs,
9956                    self.pool.as_deref(),
9957                    &mut mass,
9958                    &mut squared_mass,
9959                )?;
9960                inputs.clear();
9961            }
9962        }
9963        Ok(vec![MtpFfnProbe {
9964            mass,
9965            squared_mass,
9966            observed_positions: n,
9967        }])
9968    }
9969
9970    /// CPU-only MTP block replay up to the FFN's input (the post-attention
9971    /// norm, left in `ws.p1`), where the probe measures. The ordinary
9972    /// `mtp_step_hl` device path keeps these values resident and therefore
9973    /// cannot truthfully provide a per-neuron calibration statistic. The
9974    /// FFN arm itself is not run: its output feeds only this row's logits.
9975    fn mtp_probe_ffn_input(
9976        &mut self,
9977        mtp: &mut MtpModule,
9978        hidden: &[f32],
9979        next_token: u32,
9980        position: usize,
9981    ) -> Result<(), String> {
9982        let e = self.embed_single(next_token);
9983        let mut cat = vec![0.0f32; 2 * self.hidden_size];
9984        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9985        inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9986        inference::rms_norm_into(
9987            hidden,
9988            &mtp.hnorm,
9989            self.rms_eps,
9990            self.norm_style,
9991            cat_h,
9992        );
9993        let mut x = vec![0.0f32; self.hidden_size];
9994        mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9995
9996        let lw = &mtp.layer;
9997        inference::rms_norm_into(
9998            &x,
9999            &lw.input_norm,
10000            self.rms_eps,
10001            self.norm_style,
10002            &mut self.ws.n1,
10003        );
10004        let attn = match &lw.attn {
10005            AttnKind::Full {
10006                wq,
10007                wk,
10008                wv,
10009                wo,
10010                q_norm,
10011                k_norm,
10012                output_gate,
10013                softplus_gate,
10014                bias,
10015            } => {
10016                let mut cfg = self.attn_cfg(position);
10017                cfg.q_norm = q_norm.as_deref();
10018                cfg.k_norm = k_norm.as_deref();
10019                cfg.output_gate = *output_gate;
10020                cfg.softplus_gate = softplus_gate
10021                    .as_ref()
10022                    .map(|(gate, per_head)| (gate, *per_head));
10023                cfg.bias = bias
10024                    .as_ref()
10025                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
10026                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
10027            }
10028            _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
10029        };
10030        for (dst, src) in x.iter_mut().zip(attn) {
10031            *dst += src;
10032        }
10033        inference::rms_norm_into(
10034            &x,
10035            &lw.post_norm,
10036            self.rms_eps,
10037            self.norm_style,
10038            &mut self.ws.p1,
10039        );
10040        Ok(())
10041    }
10042
10043    /// DTG-MA calibration pass (Patent 2): run `ids` through the model
10044    /// (CPU path, per position) and return each layer's per-neuron
10045    /// activation mass Σ|silu(gate)·up| — the statistic the task-guided
10046    /// FFN mask is derived from.
10047    pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
10048        self.clear_sequence_state();
10049        FFN_PROBE.with(|p| {
10050            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
10051        });
10052        crate::gpu::cpu_scope(|| {
10053            for (pos, &id) in ids.iter().enumerate() {
10054                let emb = self.embed_single(id);
10055                let _ = self.forward_layers(&emb, pos, None);
10056            }
10057        });
10058        self.clear_sequence_state();
10059        FFN_PROBE
10060            .with(|p| p.borrow_mut().take())
10061            .unwrap_or_default()
10062    }
10063
10064    /// `probe_ffn_mass` over the BATCHED prefill: same accumulator, one
10065    /// sweep instead of one forward per token. What makes the statistic
10066    /// affordable on a 27B.
10067    pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
10068        if let Err(err) = self.nll_begin() {
10069            // A recorder can be left by a caller that was interrupted before
10070            // this request entered its scoring block.  Consume it even when
10071            // the preflight failure prevents initialization of a new one.
10072            let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
10073            self.nll_end();
10074            return Err(err);
10075        }
10076        FFN_PROBE.with(|p| {
10077            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
10078        });
10079        let result: Result<(), String> = (|| {
10080            for chunk in ids.chunks(256) {
10081                if chunk.len() < 2 {
10082                    continue;
10083                }
10084                self.nll_ids_masked(chunk, 0, None)?;
10085            }
10086            Ok(())
10087        })();
10088        self.nll_end();
10089        let probe = FFN_PROBE
10090            .with(|p| p.borrow_mut().take())
10091            .unwrap_or_default();
10092        match result {
10093            Ok(()) => Ok(probe),
10094            Err(err) => {
10095                drop(probe);
10096                Err(err)
10097            }
10098        }
10099    }
10100
10101    /// Teacher-forced PPL with a task mask active (sparse execution) —
10102    /// the quality gate for a DTG-MA-masked skill. Sequential per
10103    /// position: the batched prefill path is dense-only.
10104    pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
10105        self.nll_begin()?;
10106        let result: Result<f64, String> = (|| {
10107            let mut nll = 0f64;
10108            let mut cnt = 0usize;
10109            let mut hidden = vec![0f32; self.hidden_size];
10110            for (pos, &id) in ids.iter().enumerate() {
10111                if pos > 0 {
10112                    inference::rms_norm_into(
10113                        &hidden,
10114                        &self.weights.final_norm,
10115                        self.rms_eps,
10116                        self.norm_style,
10117                        &mut self.ws.n1,
10118                    );
10119                    let mut logits = self.lm_head_forward(&self.ws.n1);
10120                    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10121                    let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
10122                    let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
10123                    nll -= p.max(1e-300).ln();
10124                    cnt += 1;
10125                    attention::recycle_buf(&mut logits);
10126                }
10127                let emb = self.embed_single(id);
10128                hidden = self.forward_layers(&emb, pos, Some(mask));
10129                self.nll_check_graph("masked serial forward", pos)?;
10130                // Consume a possible graph logits side channel before the
10131                // next row.  Masked scoring normally disables that route,
10132                // but stale channel state must never survive a request.
10133                let _ = self.graph_logits.take();
10134            }
10135            Ok((nll / cnt.max(1) as f64).exp())
10136        })();
10137        self.nll_end();
10138        result
10139    }
10140
10141    /// Teacher-forced NLL sum + scored-token count over positions
10142    /// `start..len-1`, attention EXACT. Positions below `start` still
10143    /// run — they are the context — they are just not scored, so this
10144    /// pairs with `nll_ids_o1(ids, start)` over the very same tokens.
10145    ///
10146    /// Returning (nll, cnt) rather than a ppl is what lets a windowed
10147    /// caller combine windows before the exp, so every scored token
10148    /// weighs the same regardless of how the windows are cut.
10149    /// `nll_ids_from` with a task mask held active at every position.
10150    ///
10151    /// The batched prefill path does not thread masks, so this walks the
10152    /// per-position forward — slower, but it scores the file exactly the
10153    /// way `run --task` will serve it, which is the point of the gate
10154    /// that calls it. With `None` it defers to the fast path.
10155    /// Masked scoring rides the SAME batched sweep as unmasked scoring —
10156    /// the masked-inference fast path: `prefill_batch_masked` lands the
10157    /// per-visit FFN rows on the activations inside the fused arms. The
10158    /// per-position loop below remains only as the no-batch fallback.
10159    pub fn nll_ids_masked(
10160        &mut self,
10161        ids: &[u32],
10162        start: usize,
10163        task_mask: Option<&TaskMask>,
10164    ) -> Result<(f64, usize), String> {
10165        let task_mask = self.drop_open_mask(task_mask);
10166        self.nll_ids_inner(ids, start, task_mask)
10167    }
10168
10169    pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
10170        self.nll_ids_inner(ids, start, None)
10171    }
10172
10173    /// Diagnostic (`CMF_PREFILL_PARITY=split[,k]` on `cortiq ppl`): the same
10174    /// tokens through three ingests, each from a clean sequence — H the host
10175    /// chunked prefill, G the wgpu batch graph in k-row chunks, M the host
10176    /// prefill up to `split` and the batch graph after it, seeded from the
10177    /// host state — and where they part: NLL and top-1 per 256-position bin
10178    /// from each arm's post-stack rows, every GDN layer's recurrent state
10179    /// (the host `linear_state` against the graph's) at `split` and at the
10180    /// end, and every exact-attention layer's K/V rows.
10181    pub fn prefill_parity_probe(
10182        &mut self,
10183        ids: &[u32],
10184        split: usize,
10185        bk: usize,
10186    ) -> Result<String, String> {
10187        use std::fmt::Write as _;
10188        let (n, hs) = (ids.len(), self.hidden_size);
10189        if split == 0 || split >= n || bk == 0 || !self.can_prefill_batched() {
10190            return Err(format!(
10191                "prefill parity: needs 0 < split < {n} tokens, k > 0 and a batched host prefill"
10192            ));
10193        }
10194        // 1 = GDN, 2 = exact attention, 0 = anything else (not compared).
10195        let kinds: Vec<u8> = (0..self.num_layers)
10196            .map(|li| match &self.weights.layers[self.phys_layer(li)].attn {
10197                AttnKind::LinearGdn(_) => 1,
10198                AttnKind::Full { .. } => 2,
10199                _ => 0,
10200            })
10201            .collect();
10202        let kv_id = self.graph_kv_id;
10203        let host_chunk = self.prefill_chunk().max(1);
10204        let host_states = |p: &Self| -> Vec<Vec<f32>> {
10205            (0..p.num_layers)
10206                .map(|li| {
10207                    if kinds[li] == 1 {
10208                        p.kv_cache.layers[li].linear_state.clone()
10209                    } else {
10210                        Vec::new()
10211                    }
10212                })
10213                .collect()
10214        };
10215        let graph_states = |_p: &Self| -> Vec<Vec<f32>> {
10216            (0..kinds.len())
10217                .map(|li| {
10218                    if kinds[li] == 1 {
10219                        crate::gpu::graph_gdn_state_read(kv_id, li).unwrap_or_default()
10220                    } else {
10221                        Vec::new()
10222                    }
10223                })
10224                .collect()
10225        };
10226        // Position-major K/V rows [0, n) of every exact-attention layer.
10227        let host_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
10228            (0..p.num_layers)
10229                .map(|li| {
10230                    let c = &p.kv_cache.layers[li];
10231                    if kinds[li] != 2
10232                        || c.mode != crate::kv_cache::KvMode::F32
10233                        || c.base() != 0
10234                        || c.seq_len != n
10235                    {
10236                        return (Vec::new(), Vec::new());
10237                    }
10238                    let (nkv, hd) = (c.num_kv_heads, c.head_dim);
10239                    let (mut k, mut v) = (vec![0f32; n * nkv * hd], vec![0f32; n * nkv * hd]);
10240                    for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
10241                        for pos in 0..n {
10242                            let d = (pos * nkv + h) * hd;
10243                            k[d..d + hd].copy_from_slice(&kh[pos * hd..(pos + 1) * hd]);
10244                            v[d..d + hd].copy_from_slice(&vh[pos * hd..(pos + 1) * hd]);
10245                        }
10246                    }
10247                    (k, v)
10248                })
10249                .collect()
10250        };
10251        let graph_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
10252            (0..p.num_layers)
10253                .map(|li| {
10254                    let c = &p.kv_cache.layers[li];
10255                    if kinds[li] != 2 {
10256                        return (Vec::new(), Vec::new());
10257                    }
10258                    crate::gpu::graph_kv_read_rows(kv_id, &[(li, 0, n)], c.num_kv_heads, c.head_dim)
10259                        .and_then(|mut v| v.pop())
10260                        .unwrap_or_default()
10261                })
10262                .collect()
10263        };
10264        let host_walk = |p: &mut Self,
10265                         from: usize,
10266                         to: usize,
10267                         rows: &mut [f32],
10268                         at_split: &mut Option<Vec<Vec<f32>>>| {
10269            let mut pos = from;
10270            while pos < to {
10271                let mut end = (pos + host_chunk).min(to);
10272                if pos < split && end > split {
10273                    end = split;
10274                }
10275                let hb = p.prefill_batch(&ids[pos..end], pos);
10276                rows[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10277                pos = end;
10278                if pos == split {
10279                    *at_split = Some(host_states(p));
10280                }
10281            }
10282        };
10283        let graph_walk = |p: &mut Self,
10284                          from: usize,
10285                          to: usize,
10286                          rows: &mut [f32],
10287                          at_split: &mut Option<Vec<Vec<f32>>>|
10288         -> Result<(), String> {
10289            let mut pos = from;
10290            while pos < to {
10291                let mut end = (pos + bk).min(to);
10292                if pos < split && end > split {
10293                    end = split;
10294                }
10295                let k = end - pos;
10296                let mut hid = vec![0f32; k * hs];
10297                for (j, &id) in ids[pos..end].iter().enumerate() {
10298                    hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10299                }
10300                let positions: Vec<usize> = (pos..end).collect();
10301                let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10302                if outcome != crate::gpu::BatchGraphOutcome::Completed {
10303                    return Err(format!(
10304                        "prefill parity: batch graph {outcome:?} at position {pos}"
10305                    ));
10306                }
10307                rows[pos * hs..end * hs].copy_from_slice(&hid);
10308                pos = end;
10309                if pos == split {
10310                    *at_split = Some(graph_states(p));
10311                }
10312            }
10313            Ok(())
10314        };
10315        let score = |p: &Self, rows: &[f32], from: usize| p.parity_score(ids, rows, from);
10316
10317        let t = std::time::Instant::now();
10318        self.clear_sequence_state();
10319        let (mut hh, mut h_split) = (vec![0f32; n * hs], None);
10320        host_walk(self, 0, n, &mut hh, &mut h_split);
10321        let (h_end, h_kv) = (host_states(self), host_kv(self));
10322        let t_h = t.elapsed().as_secs_f64();
10323
10324        let t = std::time::Instant::now();
10325        self.clear_sequence_state();
10326        let (mut hg, mut g_split) = (vec![0f32; n * hs], None);
10327        let g_run = graph_walk(self, 0, n, &mut hg, &mut g_split);
10328        let (g_end, g_kv) = (graph_states(self), graph_kv(self));
10329        let t_g = t.elapsed().as_secs_f64();
10330
10331        let t = std::time::Instant::now();
10332        self.clear_sequence_state();
10333        let (mut hm, mut m_split) = (vec![0f32; n * hs], None);
10334        host_walk(self, 0, split, &mut hm, &mut m_split);
10335        let m_run = graph_walk(self, split, n, &mut hm, &mut None);
10336        let (m_end, m_kv) = (graph_states(self), graph_kv(self));
10337        let t_m = t.elapsed().as_secs_f64();
10338        self.clear_sequence_state();
10339        g_run?;
10340        m_run?;
10341
10342        let (sh, sg, sm) = (
10343            score(self, &hh, 0),
10344            score(self, &hg, 0),
10345            score(self, &hm, split),
10346        );
10347        let mut r = String::new();
10348        let _ = writeln!(
10349            r,
10350            "prefill parity: {n} tokens, split {split}, host chunk {host_chunk}, graph k {bk} \
10351             (host {t_h:.1} s, graph {t_g:.1} s, host+graph {t_m:.1} s)"
10352        );
10353        let _ = writeln!(
10354            r,
10355            "  bin          | NLL host  graph  h+g   | top1 host=graph h+g=graph | hidden rel host:graph h+g:graph"
10356        );
10357        let bin = 256usize;
10358        let mut b0 = 0;
10359        while b0 + 1 < n {
10360            let b1 = (b0 + bin).min(n - 1);
10361            let mean = |s: &[(f64, usize)], off: usize| -> Option<f64> {
10362                let v: Vec<f64> = (b0.max(off)..b1).map(|p| s[p - off].0).collect();
10363                (!v.is_empty()).then(|| v.iter().sum::<f64>() / v.len() as f64)
10364            };
10365            let agree = |a: &[(f64, usize)], ao: usize, b: &[(f64, usize)], bo: usize| {
10366                let ps: Vec<usize> = (b0.max(ao).max(bo)..b1).collect();
10367                if ps.is_empty() {
10368                    return None;
10369                }
10370                let same = ps.iter().filter(|&&p| a[p - ao].1 == b[p - bo].1).count();
10371                Some(100.0 * same as f64 / ps.len() as f64)
10372            };
10373            let hid_rel = |a: &[f32], from: usize| -> Option<f64> {
10374                (b0.max(from)..b1)
10375                    .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &hg[p * hs..(p + 1) * hs]))
10376                    .reduce(f64::max)
10377            };
10378            let f =
10379                |v: Option<f64>, prec: usize| v.map_or("-".to_string(), |x| format!("{x:.prec$}"));
10380            let e = |v: Option<f64>| v.map_or("-".to_string(), |x| format!("{x:.1e}"));
10381            let _ = writeln!(
10382                r,
10383                "  {b0:5}..{b1:<5} | {:>8} {:>6} {:>6} | {:>14} {:>9} | {:>16} {:>9}",
10384                f(mean(&sh, 0), 3),
10385                f(mean(&sg, 0), 3),
10386                f(mean(&sm, split), 3),
10387                f(agree(&sh, 0, &sg, 0), 1),
10388                f(agree(&sm, split, &sg, 0), 1),
10389                e(hid_rel(&hh, 0)),
10390                e(hid_rel(&hm, split)),
10391            );
10392            b0 = b1;
10393        }
10394        // Recurrent state, layer by layer: [conv ring | S] split at the ring.
10395        let ring_len = self
10396            .gdn_cfg
10397            .map(|c| c.conv_dim() * c.conv_kernel.saturating_sub(1))
10398            .unwrap_or(0);
10399        let state_line = |r: &mut String, label: &str, a: &[Vec<f32>], b: &[Vec<f32>]| {
10400            let _ = write!(r, "  GDN {label} (ring/S rel):");
10401            let mut first = None;
10402            for li in (0..kinds.len()).filter(|&li| kinds[li] == 1) {
10403                let (x, y) = (&a[li], &b[li]);
10404                if x.len() != y.len() || x.len() <= ring_len {
10405                    let _ = write!(r, " L{li} len {}/{}", x.len(), y.len());
10406                    continue;
10407                }
10408                let (rr, rs) = (
10409                    rel_l2(&x[..ring_len], &y[..ring_len]),
10410                    rel_l2(&x[ring_len..], &y[ring_len..]),
10411                );
10412                if first.is_none() && rr.max(rs) > 1e-2 {
10413                    first = Some(li);
10414                }
10415                let _ = write!(r, " L{li} {rr:.0e}/{rs:.0e}");
10416            }
10417            let _ = writeln!(
10418                r,
10419                "\n    first GDN layer past 1e-2: {}",
10420                first.map_or("none".to_string(), |l| l.to_string())
10421            );
10422        };
10423        match (&h_split, &g_split) {
10424            (Some(a), Some(b)) => state_line(&mut r, "host vs graph at split", a, b),
10425            _ => {
10426                let _ = writeln!(r, "  GDN at split: not captured");
10427            }
10428        }
10429        if let (Some(a), Some(b)) = (&m_split, &h_split) {
10430            state_line(&mut r, "h+g host part vs host at split", a, b);
10431        }
10432        state_line(&mut r, "host vs graph at end", &h_end, &g_end);
10433        state_line(&mut r, "h+g vs graph at end", &m_end, &g_end);
10434        // Exact-attention K/V: the worst 256-row bin per layer and the first
10435        // bin past 1e-2.
10436        let kv_line = |r: &mut String, label: &str, a: &[(Vec<f32>, Vec<f32>)], from: usize| {
10437            let _ = writeln!(r, "  K/V {label} (worst bin K/V rel, first bin past 1e-2):");
10438            for li in (0..kinds.len()).filter(|&li| kinds[li] == 2) {
10439                let ((ak, av), (gk, gv)) = (&a[li], &g_kv[li]);
10440                if ak.len() != gk.len() || ak.is_empty() {
10441                    let _ = writeln!(r, "    L{li}: rows {}/{}", ak.len(), gk.len());
10442                    continue;
10443                }
10444                let row = ak.len() / n;
10445                let (mut wk, mut wv, mut first) = (0f64, 0f64, None);
10446                let mut b0 = from;
10447                while b0 < n {
10448                    let b1 = (b0 + bin).min(n);
10449                    let (s0, s1) = (b0 * row, b1 * row);
10450                    let (rk, rv) = (
10451                        rel_l2(&ak[s0..s1], &gk[s0..s1]),
10452                        rel_l2(&av[s0..s1], &gv[s0..s1]),
10453                    );
10454                    (wk, wv) = (wk.max(rk), wv.max(rv));
10455                    if first.is_none() && rk.max(rv) > 1e-2 {
10456                        first = Some(b0);
10457                    }
10458                    b0 = b1;
10459                }
10460                let _ = writeln!(
10461                    r,
10462                    "    L{li}: {wk:.1e}/{wv:.1e} first {}",
10463                    first.map_or("none".to_string(), |p| p.to_string())
10464                );
10465            }
10466        };
10467        kv_line(&mut r, "host vs graph", &h_kv, 0);
10468        kv_line(&mut r, "h+g vs graph", &m_kv, 0);
10469        Ok(r)
10470    }
10471
10472    /// (nll, top-1 id) of every position in [from, len − 1) from post-stack
10473    /// rows (`rows` indexed by absolute position), through the scorer's
10474    /// final norm and head.
10475    fn parity_score(&self, ids: &[u32], rows: &[f32], from: usize) -> Vec<(f64, usize)> {
10476        let (n, hs) = (ids.len(), self.hidden_size);
10477        let vrows = self.weights.lm_head.rows();
10478        let vs = self.vocab_size.min(vrows);
10479        let mut out = Vec::with_capacity(n.saturating_sub(from + 1));
10480        let mut pos = from;
10481        while pos + 1 < n {
10482            let end = (pos + 32).min(n - 1);
10483            let sb = end - pos;
10484            let mut normed = vec![0f32; sb * hs];
10485            for j in 0..sb {
10486                let r = inference::rms_norm(
10487                    &rows[(pos + j) * hs..(pos + j + 1) * hs],
10488                    &self.weights.final_norm,
10489                    self.rms_eps,
10490                    self.norm_style,
10491                );
10492                normed[j * hs..(j + 1) * hs].copy_from_slice(&r);
10493            }
10494            let mut logits = vec![0f32; sb * vrows];
10495            self.weights
10496                .lm_head
10497                .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10498            for j in 0..sb {
10499                let lg = &mut logits[j * vrows..j * vrows + vs];
10500                if let Some(mu) = self.logit_multiplier {
10501                    lg.iter_mut().for_each(|v| *v *= mu);
10502                }
10503                if let Some(c) = self.final_softcap {
10504                    softcap_in_place(lg, c, self.pool.as_deref());
10505                }
10506                let (mut max, mut top) = (f32::NEG_INFINITY, 0usize);
10507                for (i, &v) in lg.iter().enumerate() {
10508                    if v > max {
10509                        (max, top) = (v, i);
10510                    }
10511                }
10512                let lse = lg
10513                    .iter()
10514                    .map(|&v| ((v - max) as f64).exp())
10515                    .sum::<f64>()
10516                    .ln()
10517                    + max as f64;
10518                out.push((lse - lg[ids[pos + j + 1] as usize] as f64, top));
10519            }
10520            pos = end;
10521        }
10522        out
10523    }
10524
10525    /// Diagnostic (`CMF_PREFILL_PARITY_O1=k` on `cortiq ppl --o1 …
10526    /// --o1-prefill P`): how much the O(1) seal depends on where its exact
10527    /// prefix ran. Every arm continues on the batch graph from the boundary
10528    /// in k-row chunks:
10529    /// - OH: prefix on the host chunked prefill; seal from the host's
10530    ///   queries and K/V; recurrent state seeded from the host.
10531    /// - OG: prefix on the batch graph (recurrent state stays on the
10532    ///   device); seal from OH's host queries and host K/V.
10533    /// - OD: prefix on the batch graph; seal from OH's queries and the
10534    ///   graph's own K/V rows (read back).
10535    ///
10536    /// OH vs OG isolates the recurrent-state hand-off, OG vs OD the seal's
10537    /// response to the host/graph K/V difference. X is the exact graph.
10538    pub fn o1_seal_parity_probe(
10539        &mut self,
10540        ids: &[u32],
10541        cfg: crate::nystrom::O1Cfg,
10542        requested: usize,
10543        bk: usize,
10544    ) -> Result<String, String> {
10545        use std::fmt::Write as _;
10546        let (n, hs) = (ids.len(), self.hidden_size);
10547        self.set_o1(Some(cfg));
10548        let split = self
10549            .o1_effective_boundary(requested)
10550            .ok_or("o1 parity: no O(1) configuration")?;
10551        if split + 1 >= n || bk == 0 || !self.can_prefill_batched() {
10552            return Err(format!(
10553                "o1 parity: needs the boundary {split} inside {n} tokens, k > 0 and a batched host prefill"
10554            ));
10555        }
10556        let o1_layers: Vec<usize> = (0..self.num_layers)
10557            .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
10558            .collect();
10559        let graph_rows = |p: &mut Self, from: usize, to: usize, rows: &mut [f32]| {
10560            let mut pos = from;
10561            while pos < to {
10562                let end = (pos + bk).min(to);
10563                let k = end - pos;
10564                let mut hid = vec![0f32; k * hs];
10565                for (j, &id) in ids[pos..end].iter().enumerate() {
10566                    hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10567                }
10568                let positions: Vec<usize> = (pos..end).collect();
10569                let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10570                if outcome != crate::gpu::BatchGraphOutcome::Completed {
10571                    return Err(format!(
10572                        "o1 parity: batch graph {outcome:?} at position {pos}"
10573                    ));
10574                }
10575                rows[pos * hs..end * hs].copy_from_slice(&hid);
10576                pos = end;
10577            }
10578            Ok(())
10579        };
10580        // Seal every O(1) layer from the given rows; `trace` = (q, k, v) per
10581        // O(1) layer, position-major.
10582        let seal_from = |p: &mut Self, trace: &[(Vec<f32>, Vec<f32>, Vec<f32>)]| {
10583            for (&li, (q, k, v)) in o1_layers.iter().zip(trace) {
10584                let c = &mut p.kv_cache.layers[li];
10585                let (row, qrow) = (k.len() / split, q.len() / split);
10586                for pos in 0..split {
10587                    c.append(
10588                        &k[pos * row..(pos + 1) * row],
10589                        &v[pos * row..(pos + 1) * row],
10590                        &[],
10591                    );
10592                    c.o1_push_q(&q[pos * qrow..(pos + 1) * qrow]);
10593                }
10594            }
10595            p.o1_seal_checked()
10596        };
10597        // Greedy continuation (`CMF_PARITY_GREEDY` tokens, default 40) from
10598        // the last prompt row, one batch-graph row per step.
10599        let steps: usize = std::env::var("CMF_PARITY_GREEDY")
10600            .ok()
10601            .and_then(|v| v.parse().ok())
10602            .unwrap_or(40);
10603        // `CMF_PARITY_DECODE=token`: the steps through `decode_step_logits`
10604        // (the token graph generation decodes on) instead of the batch graph.
10605        let token_decode = std::env::var("CMF_PARITY_DECODE").as_deref() == Ok("token");
10606        let greedy = |p: &mut Self, rows: &[f32]| -> Result<Vec<u32>, String> {
10607            let argmax = |lg: &[f32], vs: usize| {
10608                (0..vs.min(lg.len())).fold(0, |b, i| if lg[i] > lg[b] { i } else { b }) as u32
10609            };
10610            let normed = inference::rms_norm(
10611                &rows[(n - 1) * hs..n * hs],
10612                &p.weights.final_norm,
10613                p.rms_eps,
10614                p.norm_style,
10615            );
10616            let mut out = vec![argmax(&p.lm_head_forward(&normed), p.vocab_size)];
10617            for s in 1..steps {
10618                let (tok, at) = (out[s - 1], n + s - 1);
10619                let lg = if token_decode {
10620                    p.decode_step_logits(tok, at)
10621                } else {
10622                    let mut hid = p.embed_single(tok);
10623                    let outcome = p.try_batch_graph_wgpu(&mut hid, &[at], 1, None);
10624                    if outcome != crate::gpu::BatchGraphOutcome::Completed {
10625                        return Err(format!("o1 parity: greedy step {s} {outcome:?}"));
10626                    }
10627                    let normed =
10628                        inference::rms_norm(&hid, &p.weights.final_norm, p.rms_eps, p.norm_style);
10629                    p.lm_head_forward(&normed)
10630                };
10631                out.push(argmax(&lg, p.vocab_size));
10632            }
10633            Ok(out)
10634        };
10635
10636        // OW first, in a fresh process as `run` starts: the host prefix
10637        // exactly as generation runs it — boundary at `split`, the
10638        // pipeline's prefill chunk, the layers sealing after the chunk that
10639        // reaches the boundary (`CMF_O1_WALK_BOUNDARY=1`: that chunk walks
10640        // its rows one at a time, each layer sealing after the boundary row).
10641        self.clear_sequence_state();
10642        self.o1_begin_with_prefix(Some(requested));
10643        let walk_chunk = self.prefill_chunk().max(1);
10644        let walk_from = (split - 1) / walk_chunk * walk_chunk;
10645        let mut ow = vec![0f32; n * hs];
10646        let mut pos = 0;
10647        while pos < split {
10648            let end = (pos + walk_chunk).min(split);
10649            let hb = self.prefill_batch(&ids[pos..end], pos);
10650            ow[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10651            pos = end;
10652        }
10653        self.o1_seal_checked()?;
10654        let ow_run = graph_rows(self, split, n, &mut ow);
10655        let ow_text = ow_run.clone().and_then(|_| greedy(self, &ow));
10656
10657        // OH. Collect one row past the boundary's natural barrier so the
10658        // host walk does not seal inside the last chunk, copy the trace,
10659        // then seal at `split` exactly.
10660        let t = std::time::Instant::now();
10661        self.clear_sequence_state();
10662        self.o1_begin_with_prefix(Some(split + 1));
10663        let mut oh = vec![0f32; n * hs];
10664        let mut pos = 0;
10665        while pos < split {
10666            let end = (pos + 128).min(split);
10667            let hb = self.prefill_batch(&ids[pos..end], pos);
10668            oh[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10669            pos = end;
10670        }
10671        let mut host = Vec::with_capacity(o1_layers.len());
10672        for &li in &o1_layers {
10673            let c = &self.kv_cache.layers[li];
10674            let q = c
10675                .o1_q_trace()
10676                .ok_or("o1 parity: host walk sealed early")?
10677                .to_vec();
10678            let (nkv, hd) = (c.num_kv_heads, c.head_dim);
10679            let (mut k, mut v) = (vec![0f32; split * nkv * hd], vec![0f32; split * nkv * hd]);
10680            for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
10681                for p in 0..split {
10682                    let d = (p * nkv + h) * hd;
10683                    k[d..d + hd].copy_from_slice(&kh[p * hd..(p + 1) * hd]);
10684                    v[d..d + hd].copy_from_slice(&vh[p * hd..(p + 1) * hd]);
10685                }
10686            }
10687            host.push((q, k, v));
10688        }
10689        for &li in &o1_layers {
10690            self.kv_cache.layers[li].o1_set_boundary(split);
10691        }
10692        self.o1_seal_checked()?;
10693        let oh_run = graph_rows(self, split, n, &mut oh);
10694        let t_oh = t.elapsed().as_secs_f64();
10695        let oh_text = oh_run.clone().and_then(|_| greedy(self, &oh));
10696
10697        // OG: graph prefix, host seal inputs. `CMF_PARITY_PERTURB=eps`
10698        // scales every host K/V value by (1 ± eps), signs from a fixed
10699        // xorshift stream: the seal's sensitivity to rounding-size noise.
10700        self.clear_sequence_state();
10701        self.o1_begin_with_prefix(Some(requested));
10702        let mut og = vec![0f32; n * hs];
10703        graph_rows(self, 0, split, &mut og)?;
10704        let perturb: f32 = std::env::var("CMF_PARITY_PERTURB")
10705            .ok()
10706            .and_then(|v| v.parse().ok())
10707            .unwrap_or(0.0);
10708        if perturb > 0.0 {
10709            let mut seed = 0x9e37_79b9_7f4a_7c15u64;
10710            let noisy: Vec<_> = host
10711                .iter()
10712                .map(|(q, k, v)| {
10713                    let mut jitter = |x: &f32| {
10714                        seed ^= seed << 13;
10715                        seed ^= seed >> 7;
10716                        seed ^= seed << 17;
10717                        x * if seed & 1 == 0 {
10718                            1.0 + perturb
10719                        } else {
10720                            1.0 - perturb
10721                        }
10722                    };
10723                    let k: Vec<f32> = k.iter().map(&mut jitter).collect();
10724                    let v: Vec<f32> = v.iter().map(&mut jitter).collect();
10725                    (q.clone(), k, v)
10726                })
10727                .collect();
10728            seal_from(self, &noisy)?;
10729        } else {
10730            seal_from(self, &host)?;
10731        }
10732        let og_run = graph_rows(self, split, n, &mut og);
10733        let og_text = og_run.clone().and_then(|_| greedy(self, &og));
10734
10735        // OD: graph prefix, the graph's own K/V with the host queries.
10736        self.clear_sequence_state();
10737        self.o1_begin_with_prefix(Some(requested));
10738        let mut od = vec![0f32; n * hs];
10739        graph_rows(self, 0, split, &mut od)?;
10740        let mut dev = Vec::with_capacity(o1_layers.len());
10741        let mut kv_rel = Vec::with_capacity(o1_layers.len());
10742        for (&li, (q, hk, hv)) in o1_layers.iter().zip(&host) {
10743            let c = &self.kv_cache.layers[li];
10744            let (k, v) = crate::gpu::graph_kv_read_rows(
10745                self.graph_kv_id,
10746                &[(li, 0, split)],
10747                c.num_kv_heads,
10748                c.head_dim,
10749            )
10750            .and_then(|mut rows| rows.pop())
10751            .ok_or_else(|| format!("o1 parity: no graph K/V rows for layer {li}"))?;
10752            kv_rel.push((li, rel_l2(hk, &k), rel_l2(hv, &v)));
10753            dev.push((q.clone(), k, v));
10754        }
10755        seal_from(self, &dev)?;
10756        let od_run = graph_rows(self, split, n, &mut od);
10757        let od_text = od_run.clone().and_then(|_| greedy(self, &od));
10758
10759        // X: exact batch graph.
10760        self.clear_sequence_state();
10761        self.set_o1(None);
10762        let mut xx = vec![0f32; n * hs];
10763        let x_run = graph_rows(self, 0, n, &mut xx);
10764        let x_text = x_run.clone().and_then(|_| greedy(self, &xx));
10765        self.clear_sequence_state();
10766        oh_run?;
10767        ow_run?;
10768        og_run?;
10769        od_run?;
10770        x_run?;
10771
10772        let sx = self.parity_score(ids, &xx, split);
10773        let arms = [
10774            ("OH", self.parity_score(ids, &oh, split)),
10775            ("OG", self.parity_score(ids, &og, split)),
10776            ("OD", self.parity_score(ids, &od, split)),
10777            ("OW", self.parity_score(ids, &ow, split)),
10778        ];
10779        let mut r = String::new();
10780        let _ = writeln!(
10781            r,
10782            "o1 seal parity: {n} tokens, boundary {split}, graph k {bk} (OH {t_oh:.1} s), \
10783             OW chunk {walk_chunk}, boundary chunk {walk_from}..{split}"
10784        );
10785        let _ = write!(r, "  seal K/V rel host vs graph:");
10786        for (li, rk, rv) in &kv_rel {
10787            let _ = write!(r, " L{li} {rk:.0e}/{rv:.0e}");
10788        }
10789        // Post-stack prefix rows against the exact graph: the batched rows,
10790        // then the rows OW walks one at a time.
10791        let worst = |a: &[f32], p0: usize, p1: usize| {
10792            (p0..p1)
10793                .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &xx[p * hs..(p + 1) * hs]))
10794                .fold(0f64, f64::max)
10795        };
10796        let _ = writeln!(
10797            r,
10798            "\n  prefix rows rel vs exact graph, [0,{walk_from}) / [{walk_from},{split}): \
10799             OH {:.1e} / {:.1e}, OW {:.1e} / {:.1e}",
10800            worst(&oh, 0, walk_from),
10801            worst(&oh, walk_from, split),
10802            worst(&ow, 0, walk_from),
10803            worst(&ow, walk_from, split),
10804        );
10805        let _ = writeln!(
10806            r,
10807            "  bin          | NLL exact    OH     OG     OD     OW | top1=exact  OH    OG    OD    OW"
10808        );
10809        let mean = |s: &[(f64, usize)], b0: usize, b1: usize| {
10810            (b0..b1).map(|p| s[p - split].0).sum::<f64>() / (b1 - b0).max(1) as f64
10811        };
10812        let agree = |s: &[(f64, usize)], b0: usize, b1: usize| {
10813            let same = (b0..b1)
10814                .filter(|&p| s[p - split].1 == sx[p - split].1)
10815                .count();
10816            100.0 * same as f64 / (b1 - b0).max(1) as f64
10817        };
10818        let mut b0 = split;
10819        let mut total = [0f64; 5];
10820        while b0 + 1 < n {
10821            let b1 = ((b0 / 256 + 1) * 256).min(n - 1);
10822            let _ = writeln!(
10823                r,
10824                "  {b0:5}..{b1:<5} | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | {:>12.1} {:>5.1} {:>5.1} {:>5.1}",
10825                mean(&sx, b0, b1),
10826                mean(&arms[0].1, b0, b1),
10827                mean(&arms[1].1, b0, b1),
10828                mean(&arms[2].1, b0, b1),
10829                mean(&arms[3].1, b0, b1),
10830                agree(&arms[0].1, b0, b1),
10831                agree(&arms[1].1, b0, b1),
10832                agree(&arms[2].1, b0, b1),
10833                agree(&arms[3].1, b0, b1),
10834            );
10835            b0 = b1;
10836        }
10837        for (i, s) in std::iter::once(&sx)
10838            .chain(arms.iter().map(|a| &a.1))
10839            .enumerate()
10840        {
10841            total[i] = s.iter().map(|x| x.0).sum::<f64>() / s.len().max(1) as f64;
10842        }
10843        let _ = writeln!(
10844            r,
10845            "  all          | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | ppl ratio vs exact OH ×{:.3} OG ×{:.3} OD ×{:.3} OW ×{:.3}",
10846            total[0],
10847            total[1],
10848            total[2],
10849            total[3],
10850            total[4],
10851            (total[1] - total[0]).exp(),
10852            (total[2] - total[0]).exp(),
10853            (total[3] - total[0]).exp(),
10854            (total[4] - total[0]).exp(),
10855        );
10856        for (name, text) in [
10857            ("X ", x_text),
10858            ("OH", oh_text),
10859            ("OG", og_text),
10860            ("OD", od_text),
10861            ("OW", ow_text),
10862        ] {
10863            let s = text.map_or_else(|e| e, |t| self.tokenizer.decode(&t));
10864            let _ = writeln!(r, "  greedy {name}: {:?}", s);
10865        }
10866        Ok(r)
10867    }
10868
10869    fn nll_ids_inner(
10870        &mut self,
10871        ids: &[u32],
10872        start: usize,
10873        task_mask: Option<&TaskMask>,
10874    ) -> Result<(f64, usize), String> {
10875        self.nll_begin()?;
10876        // Block diffusion on Apple silicon: the Metal executor's causal path.
10877        #[cfg(target_os = "macos")]
10878        if task_mask.is_none() && self.is_block_diffusion() {
10879            if let Some(r) = self.clover_metal_nll(ids, start) {
10880                self.nll_end();
10881                return r;
10882            }
10883        }
10884        let result: Result<(f64, usize), String> = (|| {
10885            let mut nll = 0f64;
10886            let mut cnt = 0usize;
10887            // An unmasked quality run with the resident wgpu graph must score
10888            // the same stateful path used by generation.  The layer-major
10889            // GEMM prefill below is a valid CPU/GEMM oracle, but it seeds
10890            // neither the graph's device GDN state nor its device KV mirrors;
10891            // using it here would silently score a different execution.  Keep
10892            // masked scoring on the exact per-position path as before, and
10893            // let the serial arm below drive the graph-aware scorer.
10894            // Only native Metal has a fused graph lm_head contract.  Vulkan
10895            // and other graph backends may expose hidden state without the
10896            // optional logits side channel; preserve their established CPU
10897            // norm/head fallback instead of turning that valid route into a
10898            // hard missing-logits error.
10899            let (graph_quality, fused_head_quality) = nll_graph_policy(
10900                task_mask.is_none(),
10901                self.graph_prefill_preferred(),
10902                crate::gpu::q1_force(),
10903            );
10904            self.graph_head_required = fused_head_quality;
10905            self.graph_want_logits = fused_head_quality;
10906            #[cfg(target_os = "macos")]
10907            if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
10908                match self.nll_batch_metal(ids, start) {
10909                    MetalBatchNllOutcome::Completed(nll, count) => {
10910                        return Ok((nll, count));
10911                    }
10912                    MetalBatchNllOutcome::Declined => {}
10913                    MetalBatchNllOutcome::Failed(err) => return Err(err),
10914                }
10915            }
10916            // CMF_NLL_SERIAL=1 scores position by position through the
10917            // decode path (the wgpu token graph where it admits the model)
10918            // — the perplexity gate of a decode kernel, which the batched
10919            // prefill arm below never runs.
10920            let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
10921            if self.can_prefill_batched() && !graph_quality && !force_serial {
10922                // prefill-GEMM: layer-major position chunks, lm_head batched
10923                // (254MB lm_head read once per chunk, not per position).
10924                // The layer chunk is large (grouping positions by MoE experts
10925                // wins with size), lm_head in sub-blocks (logit buffer
10926                // 32×vocab ≈ 32MB instead of 128×).
10927                // CMF_NLL_CHUNK: positions per prefill step (default 128).
10928                // A device scorer pays one round trip per projection per
10929                // step, so a 512-position step quarters them.
10930                let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
10931                    .ok()
10932                    .and_then(|v| v.parse().ok())
10933                    .filter(|&c: &usize| c > 0)
10934                    .unwrap_or(128);
10935                const LM_SUB: usize = 32;
10936                let n = ids.len().saturating_sub(1);
10937                let hs = self.hidden_size;
10938                let rows = self.weights.lm_head.rows();
10939                let mut pos = 0usize;
10940                let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
10941                while pos < n {
10942                    let end = (pos + chunk_len).min(n);
10943                    let bsz = end - pos;
10944                    let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
10945                    self.nll_check_graph("batched prefill", pos)?;
10946                    if state_trace && end % 256 == 0 {
10947                        self.trace_recurrent_state(end);
10948                    }
10949                    let mut k0 = 0usize;
10950                    while k0 < bsz {
10951                        let k1 = (k0 + LM_SUB).min(bsz);
10952                        let sb = k1 - k0;
10953                        // Sub-block entirely below the scored range: the KV
10954                        // it just built is all this pass needed from it.
10955                        if pos + k1 <= start {
10956                            k0 = k1;
10957                            continue;
10958                        }
10959                        let mut normed = vec![0.0f32; sb * hs];
10960                        for k in 0..sb {
10961                            let r = inference::rms_norm(
10962                                &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
10963                                &self.weights.final_norm,
10964                                self.rms_eps,
10965                                self.norm_style,
10966                            );
10967                            normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
10968                        }
10969                        let mut logits = vec![0.0f32; sb * rows];
10970                        self.weights
10971                            .lm_head
10972                            .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10973                        for k in 0..sb {
10974                            if pos + k0 + k < start {
10975                                continue;
10976                            }
10977                            self.nll_check_graph("batched score row", pos + k0 + k)?;
10978                            let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
10979                            if let Some(mu) = self.logit_multiplier {
10980                                for v in lg.iter_mut() {
10981                                    *v *= mu;
10982                                }
10983                            }
10984                            // Gemma-class final-logit soft-capping: the
10985                            // decode paths apply it; scoring must too, or
10986                            // the uncapped softmax misprices every token.
10987                            if let Some(c) = self.final_softcap {
10988                                softcap_in_place(lg, c, self.pool.as_deref());
10989                            }
10990                            // Cortiq Embryo hierarchical head: same correction
10991                            // the decode path applies (lm_head_forward).
10992                            if let Some(cm) = self.head_clusters.clone() {
10993                                self.hierarchical_head_logprobs(
10994                                    &normed[k * hs..(k + 1) * hs],
10995                                    &cm,
10996                                    lg,
10997                                );
10998                            }
10999                            let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
11000                            let target = ids[pos + k0 + k + 1] as usize;
11001                            let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11002                            let lse: f64 = lg
11003                                .iter()
11004                                .map(|&v| ((v - max) as f64).exp())
11005                                .sum::<f64>()
11006                                .ln()
11007                                + max as f64;
11008                            nll += lse - lg[target] as f64;
11009                            cnt += 1;
11010                            if std::env::var("CMF_PPL_TRACE").is_ok() {
11011                                let top = lg
11012                                    .iter()
11013                                    .enumerate()
11014                                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11015                                    .map(|(i, _)| i)
11016                                    .unwrap_or(0);
11017                                eprintln!(
11018                                    "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
11019                                    pos + k0 + k,
11020                                    target,
11021                                    lse - lg[target] as f64,
11022                                    top,
11023                                    lg[target],
11024                                    lg[top]
11025                                );
11026                            }
11027                        }
11028                        k0 = k1;
11029                    }
11030                    pos = end;
11031                }
11032                return Ok((nll, cnt));
11033            }
11034            for pos in 0..ids.len().saturating_sub(1) {
11035                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
11036                self.nll_check_graph("serial forward", pos)?;
11037                // Architectures whose head lives inside their own stack return
11038                // the logits out of band and a zero hidden — DeepSeek-V4 folds
11039                // its hyper-connection copies between the last layer and the
11040                // norm, so it cannot hand back a vector this loop could use.
11041                // Scoring the zeros gave a perplexity of exactly the vocabulary
11042                // size, which is a uniform distribution reported as a
11043                // measurement. `generate` already reads this channel.
11044                let out_of_band = self.graph_logits.take();
11045                if self.graph_head_required && out_of_band.is_none() {
11046                    METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
11047                    return Err(format!(
11048                        "fused Metal graph head did not complete at NLL position {pos}"
11049                    ));
11050                }
11051                if pos < start {
11052                    continue;
11053                }
11054                let logits = match out_of_band {
11055                    Some(lg) => lg,
11056                    None => {
11057                        let normed = inference::rms_norm(
11058                            &hidden,
11059                            &self.weights.final_norm,
11060                            self.rms_eps,
11061                            self.norm_style,
11062                        );
11063                        // lm_head_forward applies the final-logit softcap itself
11064                        // — capping again here double-squashed gemma-class
11065                        // logits (tanh∘tanh) and reported a flattered ppl.
11066                        self.lm_head_forward(&normed)
11067                    }
11068                };
11069                let target = ids[pos + 1] as usize;
11070                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11071                let lse: f64 = logits
11072                    .iter()
11073                    .map(|&v| ((v - max) as f64).exp())
11074                    .sum::<f64>()
11075                    .ln()
11076                    + max as f64;
11077                let tok_nll = lse - logits[target] as f64;
11078                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11079                    let top = logits
11080                        .iter()
11081                        .enumerate()
11082                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11083                        .map(|(i, _)| i)
11084                        .unwrap_or(0);
11085                    eprintln!(
11086                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11087                        logits[target], logits[top]
11088                    );
11089                }
11090                nll += tok_nll;
11091                cnt += 1;
11092            }
11093            Ok((nll, cnt))
11094        })();
11095        self.nll_end();
11096        result
11097    }
11098
11099    /// Score one post-layer hidden with the same final norm/head path used by
11100    /// decode. Keeping this in one helper is important for the production
11101    /// batch scorer: its rows stop before the final norm, just like the
11102    /// per-position O(1) path below.
11103    fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
11104        let normed = inference::rms_norm(
11105            hidden,
11106            &self.weights.final_norm,
11107            self.rms_eps,
11108            self.norm_style,
11109        );
11110        // lm_head_forward applies the final-logit softcap itself — capping
11111        // again here double-squashed gemma-class logits in earlier scorers.
11112        let mut logits = self.lm_head_forward(&normed);
11113        let target = target as usize;
11114        let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11115        let lse: f64 = logits
11116            .iter()
11117            .map(|&v| ((v - max) as f64).exp())
11118            .sum::<f64>()
11119            .ln()
11120            + max as f64;
11121        let tok_nll = lse - logits[target] as f64;
11122        if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11123            let top = logits
11124                .iter()
11125                .enumerate()
11126                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11127                .map(|(i, _)| i)
11128                .unwrap_or(0);
11129            eprintln!(
11130                "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11131                logits[target], logits[top]
11132            );
11133        }
11134        attention::recycle_buf(&mut logits);
11135        tok_nll
11136    }
11137
11138    /// `CMF_STATE_TRACE`: per-layer magnitude of the recurrent record at
11139    /// position `pos` — the whole `linear_state` (vmf: S then the conv
11140    /// ring; GDN: conv ring then S), its recurrent S part alone, the
11141    /// bounded ring, and the last per-position KV row count.  The tool
11142    /// that separated the ~4k perplexity cliff of the 500-step exports
11143    /// (a state that keeps climbing past the trained window) from a
11144    /// runtime boundary; one line per layer, `STATE pos=… layer=…`.
11145    fn trace_recurrent_state(&self, pos: usize) {
11146        let stats = |v: &[f32]| -> (f64, f64) {
11147            if v.is_empty() {
11148                return (0.0, 0.0);
11149            }
11150            let (mut ss, mut mx) = (0f64, 0f64);
11151            for &x in v {
11152                ss += (x as f64) * (x as f64);
11153                mx = mx.max((x as f64).abs());
11154            }
11155            ((ss / v.len() as f64).sqrt(), mx)
11156        };
11157        for (li, l) in self.kv_cache.layers.iter().enumerate() {
11158            let lw = &self.weights.layers[self.phys_layer(li)];
11159            let (kind, s_len) = match &lw.attn {
11160                AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
11161                AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
11162                AttnKind::Bounded(_) => ("bounded", 0),
11163                AttnKind::Full { .. } => ("full", 0),
11164                _ => ("other", 0),
11165            };
11166            let (rms, max) = stats(&l.linear_state);
11167            let s_part = if kind == "vmf" {
11168                &l.linear_state[..s_len.min(l.linear_state.len())]
11169            } else if kind == "gdn" {
11170                let ring = l
11171                    .linear_state
11172                    .len()
11173                    .saturating_sub(s_len)
11174                    .min(l.linear_state.len());
11175                &l.linear_state[ring..]
11176            } else {
11177                &l.linear_state[..0]
11178            };
11179            let (s_rms, s_max) = stats(s_part);
11180            let (ring_rms, ring_len) = match &l.bounded {
11181                Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
11182                None => (0.0, 0),
11183            };
11184            eprintln!(
11185                "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
11186                 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
11187                l.linear_state.len(),
11188                l.seq_len
11189            );
11190        }
11191    }
11192
11193    /// Teacher-forced NLL of the CONVERTED model: the O(1) Nyström path
11194    /// is ACTIVE over the scored positions. Returns `Ok((nll sum, scored
11195    /// count))` over `prefill..len-1` and surfaces a post-mutation batch
11196    /// failure instead of returning a partial score.
11197    ///
11198    /// Runtime discipline, deliberately NOT the matrix probe's: the
11199    /// requested prefix plus any required deferred lead-in run the exact
11200    /// prompt pass — that pass is what freezes the landmarks and M — and
11201    /// every post-seal scored position goes through `NystromState::step()`,
11202    /// the same code decode runs.
11203    /// So the landmarks are PREFILL-frozen (what ships), not
11204    /// full-sequence oracles (what the published probe measured). When the
11205    /// requested prefix is shorter than the bounded transition, rows in the
11206    /// exact lead-in are still scored so the shifted target range is stable.
11207    ///
11208    /// Pair with `nll_ids_from(ids, prefill)` for the exact baseline
11209    /// over the identical token set — that ratio is the honest one.
11210    pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
11211        // This scorer consumes host hiddens, so never request the optional
11212        // token-graph lm_head side channel. `nll_begin` also consumes a
11213        // prior graph failure and clears only the cancel bit that failure
11214        // raised, leaving a caller-owned cancellation observable.
11215        self.nll_begin()?;
11216        let requested_prefix = (prefill > 0).then_some(prefill);
11217        self.o1_begin_with_prefix(requested_prefix);
11218        let n = ids.len().saturating_sub(1);
11219        let requested_start = prefill.min(n);
11220        // The exact prefix must reach the deferred boundary before a
11221        // collecting layer can convert. Rows between the requested start and
11222        // that boundary remain part of the public NLL range and are scored
11223        // from the same hidden pass below.
11224        let exact_end = if self.o1_active() {
11225            match requested_prefix {
11226                Some(requested) => self.o1_effective_boundary(requested),
11227                None => self
11228                    .o1_cfg
11229                    .as_ref()
11230                    .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
11231            }
11232            .unwrap_or(requested_start)
11233            .min(n)
11234        } else {
11235            requested_start
11236        };
11237        let mut nll = 0f64;
11238        let mut cnt = 0usize;
11239
11240        // Exact prompt pass over ids[..exact_end]: the seal consumes its
11241        // q/k/v. Rows at or after requested_start are scored here when the
11242        // bounded lead-in is longer than the caller's requested prefix.
11243        let mut pos = 0usize;
11244        if self.can_prefill_batched() {
11245            const CHUNK: usize = 128;
11246            while pos < exact_end {
11247                let end = (pos + CHUNK).min(exact_end);
11248                let hiddens = self.prefill_batch(&ids[pos..end], pos);
11249                if self
11250                    .graph_failed
11251                    .swap(false, std::sync::atomic::Ordering::Relaxed)
11252                {
11253                    self.cancel
11254                        .store(false, std::sync::atomic::Ordering::Relaxed);
11255                    self.nll_end();
11256                    return Err("GPU graph failed during O(1) NLL prefix".into());
11257                }
11258                for row in 0..end - pos {
11259                    let score_pos = pos + row;
11260                    if score_pos >= requested_start && score_pos < n {
11261                        nll += self.nll_from_hidden(
11262                            &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
11263                            ids[score_pos + 1],
11264                            score_pos,
11265                        );
11266                        cnt += 1;
11267                    }
11268                }
11269                pos = end;
11270            }
11271        } else {
11272            while pos < exact_end {
11273                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11274                if self
11275                    .graph_failed
11276                    .swap(false, std::sync::atomic::Ordering::Relaxed)
11277                {
11278                    self.cancel
11279                        .store(false, std::sync::atomic::Ordering::Relaxed);
11280                    self.nll_end();
11281                    return Err("GPU graph failed during O(1) NLL prefix".into());
11282                }
11283                if pos >= requested_start && pos < n {
11284                    nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11285                    cnt += 1;
11286                }
11287                pos += 1;
11288            }
11289        }
11290        self.o1_seal_checked().map_err(|err| {
11291            self.nll_end();
11292            err
11293        })?;
11294
11295        // Reuse the production whole-token batch graph for the post-seal
11296        // suffix when the caller explicitly enabled both routes. This is a
11297        // teacher-forced scorer, so every row is ids[pos] and its target is
11298        // ids[pos + 1]; no speculative tail or rollback state is involved.
11299        // A first Declined is safe to handle with the established serial O(1)
11300        // path. Once a chunk completes, however, the device recurrent state
11301        // owns the sequence and a later decline must be terminal rather than
11302        // falling back to stale CPU state.
11303        let batch_k = std::env::var("CMF_BATCH_K")
11304            .ok()
11305            .and_then(|v| v.parse::<usize>().ok())
11306            .unwrap_or(0);
11307        let batch_admitted = batch_k > 0
11308            && self.can_prefill_batched()
11309            && self.o1_active()
11310            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
11311            && (0..self.num_layers).all(|li| {
11312                let cache = &self.kv_cache.layers[self.phys_layer(li)];
11313                cache.o1.is_none() || cache.o1_views().is_some()
11314            });
11315        if std::env::var("CMF_GRAPH_PROF").is_ok() {
11316            eprintln!(
11317                "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
11318                batch_admitted,
11319                batch_k,
11320                n.saturating_sub(exact_end),
11321            );
11322        }
11323        let mut batch_completed = false;
11324        if batch_admitted && exact_end < n {
11325            let hs = self.hidden_size;
11326            let mut batch_pos = exact_end;
11327            while batch_pos < n {
11328                let end = (batch_pos + batch_k).min(n);
11329                let bk = end - batch_pos;
11330                let mut hiddens = vec![0.0f32; bk * hs];
11331                for (row, &id) in ids[batch_pos..end].iter().enumerate() {
11332                    hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
11333                }
11334                let positions: Vec<usize> = (batch_pos..end).collect();
11335                let t_batch = std::time::Instant::now();
11336                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
11337                if std::env::var("CMF_GRAPH_PROF").is_ok() {
11338                    let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
11339                    eprintln!(
11340                        "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
11341                        batch_pos,
11342                        end.saturating_sub(1),
11343                        bk as f64 / (ms / 1000.0),
11344                    );
11345                }
11346                if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
11347                    self.nll_end();
11348                    return Err(err);
11349                }
11350                match outcome {
11351                    crate::gpu::BatchGraphOutcome::Completed => {
11352                        batch_completed = true;
11353                        for row in 0..bk {
11354                            nll += self.nll_from_hidden(
11355                                &hiddens[row * hs..(row + 1) * hs],
11356                                ids[batch_pos + row + 1],
11357                                batch_pos + row,
11358                            );
11359                            cnt += 1;
11360                        }
11361                        batch_pos = end;
11362                    }
11363                    crate::gpu::BatchGraphOutcome::Declined => {
11364                        if batch_completed {
11365                            self.nll_end();
11366                            return Err(format!(
11367                                "O(1) NLL batch declined after completed chunk at position {batch_pos}"
11368                            ));
11369                        }
11370                        break;
11371                    }
11372                    crate::gpu::BatchGraphOutcome::Failed => {
11373                        self.nll_end();
11374                        return Err(format!(
11375                            "O(1) NLL batch graph failed after admission at position {batch_pos}"
11376                        ));
11377                    }
11378                }
11379            }
11380            if batch_completed && cnt == n.saturating_sub(requested_start) {
11381                self.nll_end();
11382                return Ok((nll, cnt));
11383            }
11384        }
11385
11386        // Serial O(1) fallback/reference. It is intentionally retained when
11387        // batch admission declines before mutation; callers must label this
11388        // CMF_BATCH_K=0/per-position path separately from the production
11389        // whole-token batch route.
11390        for pos in exact_end..n {
11391            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11392            if self
11393                .graph_failed
11394                .swap(false, std::sync::atomic::Ordering::Relaxed)
11395            {
11396                self.cancel
11397                    .store(false, std::sync::atomic::Ordering::Relaxed);
11398                self.nll_end();
11399                return Err(format!(
11400                    "GPU graph failed during O(1) NLL serial scoring at position {pos}"
11401                ));
11402            }
11403            nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11404            cnt += 1;
11405        }
11406        self.nll_end();
11407        Ok((nll, cnt))
11408    }
11409
11410    /// Teacher-forced calibration data (B1): for each position, whether the
11411    /// argmax equals the actual next token, and the top-1 softmax prob
11412    /// (top-1 probability) under EACH temperature in `temps` — all from ONE forward
11413    /// pass (argmax/correctness are temperature-invariant; only p_max
11414    /// reshapes). Feeds `cortiq calibrate` (reliability/ECE + temperature
11415    /// fit): is the model's confidence a true property, or does it need a
11416    /// measured scaling?
11417    pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
11418        self.clear_sequence_state();
11419        let n = ids.len().saturating_sub(1);
11420        let mut correct = Vec::with_capacity(n);
11421        let mut pmax = Vec::with_capacity(n);
11422        for pos in 0..n {
11423            let emb = self.embed_single(ids[pos]);
11424            let hidden = self.forward_layers(&emb, pos, None);
11425            let logits = if let Some(logits) = self.graph_logits.take() {
11426                logits
11427            } else {
11428                let normed = inference::rms_norm(
11429                    &hidden,
11430                    &self.weights.final_norm,
11431                    self.rms_eps,
11432                    self.norm_style,
11433                );
11434                // lm_head_forward applies the final-logit softcap itself —
11435                // capping again here double-squashed gemma-class logits
11436                // (tanh∘tanh) and reported a flattered ppl.
11437                self.lm_head_forward(&normed)
11438            };
11439            let target = ids[pos + 1] as usize;
11440            let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
11441            for (i, &v) in logits.iter().enumerate() {
11442                if v > mval {
11443                    mval = v;
11444                    amax = i;
11445                }
11446            }
11447            correct.push(amax == target);
11448            let row: Vec<f32> = temps
11449                .iter()
11450                .map(|&t| {
11451                    let tt = t.max(1e-3);
11452                    let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
11453                    1.0 / s.max(1e-12) // numerator at the max is exp(0)=1
11454                })
11455                .collect();
11456            pmax.push(row);
11457        }
11458        self.clear_sequence_state();
11459        (correct, pmax)
11460    }
11461
11462    /// Teacher-forced PPL with the dynamic router driving per-window
11463    /// skill switches (VMF experiment №2 measurement). Sequential (φ
11464    /// must update per token), returns (ppl, switch_count). The router
11465    /// must be enabled (`enable_dynamic_routing`); else this equals
11466    /// plain `ppl_ids`. The active skill when scoring token t shapes the
11467    /// logits for t+1 — on-policy over the held-out text itself.
11468    pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
11469        if self.dyn_router.is_none() {
11470            return Ok((self.ppl_ids(ids)?, 0));
11471        }
11472        self.nll_begin()?;
11473        let saved_active = self.dyn_active;
11474        let mut router = self
11475            .dyn_router
11476            .take()
11477            .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
11478        router.reset();
11479        self.dyn_phi_seen = 0;
11480        let _ = self.set_active_skill(None);
11481
11482        let result: Result<(f64, usize), String> = (|| {
11483            let mut nll = 0f64;
11484            let mut cnt = 0usize;
11485            for pos in 0..ids.len().saturating_sub(1) {
11486                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11487                self.nll_check_graph("dynamic serial forward", pos)?;
11488                let out_of_band = self.graph_logits.take();
11489                let mut logits = match out_of_band {
11490                    Some(lg) => lg,
11491                    None => {
11492                        let normed = inference::rms_norm(
11493                            &hidden,
11494                            &self.weights.final_norm,
11495                            self.rms_eps,
11496                            self.norm_style,
11497                        );
11498                        // lm_head_forward applies the final-logit softcap itself —
11499                        // capping again here double-squashed gemma-class logits
11500                        // and reported a flattered ppl.
11501                        self.lm_head_forward(&normed)
11502                    }
11503                };
11504                let target = ids[pos + 1] as usize;
11505                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11506                let lse: f64 = logits
11507                    .iter()
11508                    .map(|&v| ((v - max) as f64).exp())
11509                    .sum::<f64>()
11510                    .ln()
11511                    + max as f64;
11512                let tok_nll = lse - logits[target] as f64;
11513                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11514                    let top = logits
11515                        .iter()
11516                        .enumerate()
11517                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11518                        .map(|(i, _)| i)
11519                        .unwrap_or(0);
11520                    eprintln!(
11521                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11522                        logits[target], logits[top]
11523                    );
11524                }
11525                nll += tok_nll;
11526                cnt += 1;
11527                attention::recycle_buf(&mut logits);
11528                // Route on the evolving phi (drives the NEXT token's skill).
11529                let phi = self.dyn_phi_ema.clone();
11530                if let Some(new_active) = router.step(&phi, pos) {
11531                    let _ = self.set_active_skill(new_active);
11532                }
11533            }
11534            Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
11535        })();
11536
11537        // Restore the detached router and the active overlay on both success
11538        // and failure. The scoring state is cleared independently below.
11539        let _ = self.set_active_skill(saved_active);
11540        self.dyn_router = Some(router);
11541        self.nll_end();
11542        result
11543    }
11544
11545    /// Routing probe φ (spec §9): mean-pooled hidden after `layer`.
11546    pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
11547        self.clear_sequence_state();
11548        let mut acc = vec![0f32; self.hidden_size];
11549        for (pos, &id) in ids.iter().enumerate() {
11550            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11551            for (a, v) in acc.iter_mut().zip(&h) {
11552                *a += v;
11553            }
11554        }
11555        let n = ids.len().max(1) as f32;
11556        for a in acc.iter_mut() {
11557            *a /= n;
11558        }
11559        self.clear_sequence_state();
11560        acc
11561    }
11562
11563    /// Router-v2 φ probe (spec §9.4, `phi.pool = "span_mean"`): the hidden
11564    /// AFTER `layer` — the same per-position walk and the same quantity as
11565    /// [`Self::probe_phi`] — averaged over the positions in `span` only
11566    /// (the user text between the template's prefix and suffix ids), NOT
11567    /// unit-normalized (the decision normalizes). The walk stops at
11568    /// `span.end`: causality makes the later positions irrelevant, so the
11569    /// result is bit-identical to probing `ids[..span.end]`. An empty span
11570    /// gives the zero vector, which the decision treats as degenerate.
11571    ///
11572    /// Every sequence state is reset before and after — the host KV/ring/
11573    /// recurrent state, the reuse keys (`kv_history`, `kv_prefix`) and the
11574    /// device graph's sequence — so run it on a pipeline that does not
11575    /// also serve a conversation (its prefix reuse would be lost).
11576    pub fn probe_phi_span(
11577        &mut self,
11578        ids: &[u32],
11579        layer: usize,
11580        span: std::ops::Range<usize>,
11581    ) -> Vec<f32> {
11582        #[cfg(target_os = "macos")]
11583        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11584        let end = span.end.min(ids.len());
11585        let start = span.start.min(end);
11586        let reset = |p: &mut Self| p.clear_sequence_state();
11587        reset(self);
11588        let mut acc = vec![0f32; self.hidden_size];
11589        for (pos, &id) in ids[..end].iter().enumerate() {
11590            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11591            if pos >= start {
11592                for (a, v) in acc.iter_mut().zip(&h) {
11593                    *a += v;
11594                }
11595            }
11596        }
11597        let n = end - start;
11598        if n > 0 {
11599            let n = n as f32;
11600            for a in acc.iter_mut() {
11601                *a /= n;
11602            }
11603        }
11604        reset(self);
11605        acc
11606    }
11607
11608    /// One decode step of the current sequence: forward `token` at
11609    /// `position` (the cache holds positions `[0, position)`, e.g. after
11610    /// [`Self::forward_ids`]) and return the next-token logits — the same
11611    /// forward and head the generation loop runs (resident-graph logits
11612    /// when the graph ran, final norm + lm_head otherwise). The logit-dump
11613    /// tools drive greedy decoding with it so every position is observable.
11614    pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
11615        #[cfg(target_os = "macos")]
11616        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11617        self.graph_logits = None;
11618        let hidden = self.forward_layers(&self.embed_single(token), position, None);
11619        if let Some(logits) = self.graph_logits.take() {
11620            return logits;
11621        }
11622        inference::rms_norm_into(
11623            &hidden,
11624            &self.weights.final_norm,
11625            self.rms_eps,
11626            self.norm_style,
11627            &mut self.ws.n1,
11628        );
11629        self.lm_head_forward(&self.ws.n1)
11630    }
11631
11632    /// Layer-major batched prefill (prefill-GEMM): full-attention —
11633    /// per-position with the existing operators (KV grows naturally,
11634    /// causality preserved), GDN projections / FFN / MoE — batched
11635    /// (a weight row is read from DRAM once per chunk, not per
11636    /// position). Returns the hidden of all positions [b × hidden].
11637    fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
11638        self.prefill_batch_masked(ids, start_pos, None)
11639    }
11640
11641    /// `prefill_batch` with a task mask honored on the dense-FFN panels
11642    /// (the masked-inference fast path: full fused compute, mask lands on
11643    /// the activations). The whole-chunk GPU graph is skipped for masked
11644    /// layers by the callers' arms; the per-GEMM device paths stay in
11645    /// play because the zeroing happens on the host between them.
11646    fn prefill_batch_masked(
11647        &mut self,
11648        ids: &[u32],
11649        start_pos: usize,
11650        task_mask: Option<&TaskMask>,
11651    ) -> Vec<f32> {
11652        self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
11653    }
11654
11655    /// One prompt chunk through the whole stack, post-stack rows out (no
11656    /// final norm) — the ingest generation uses, shared by scoring and
11657    /// `forward_ids` so they measure the same execution: the batched wgpu
11658    /// graph's device prefix plus the host's batched walk for the rest when
11659    /// `batch_prefix_prefill` holds and the graph admits the chunk, else
11660    /// the host's chunked prefill. Err only when a graph that had mutated
11661    /// device state failed.
11662    fn prefill_rows(
11663        &mut self,
11664        ids: &[u32],
11665        pos: usize,
11666        task_mask: Option<&TaskMask>,
11667    ) -> Result<Vec<f32>, String> {
11668        self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
11669    }
11670
11671    fn prefill_input_rows(
11672        &mut self,
11673        input: PrefillIn<'_>,
11674        pos: usize,
11675        task_mask: Option<&TaskMask>,
11676    ) -> Result<Vec<f32>, String> {
11677        self.mimo_moe_prepare();
11678        let hs = self.hidden_size;
11679        let bk = match input {
11680            PrefillIn::Ids(ids) => ids.len(),
11681            PrefillIn::Hidden(rows) => rows.len() / hs,
11682        };
11683        #[cfg(not(target_os = "macos"))]
11684        if task_mask.is_none()
11685            && !self.o1_active()
11686            && bk > 1
11687            && (self.batch_prefix_prefill()
11688                // An embedded prompt (vision rows) on the graph route takes
11689                // the batched graph like an id prompt does.
11690                || (matches!(input, PrefillIn::Hidden(_)) && self.generation_batch_k() > 0)
11691                || (self.verify_exact_moe
11692                    && crate::gpu::enabled_here()
11693                    && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
11694        {
11695            let mut hiddens = match input {
11696                PrefillIn::Hidden(rows) => rows.to_vec(),
11697                PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
11698            };
11699            let positions: Vec<usize> = (pos..pos + bk).collect();
11700            let mut run = 0usize;
11701            match self.try_batch_graph_wgpu_prefix(
11702                &mut hiddens,
11703                &positions,
11704                bk,
11705                None,
11706                Some(&mut run),
11707            ) {
11708                crate::gpu::BatchGraphOutcome::Completed => {
11709                    let out = if run < self.num_layers {
11710                        self.prefill_batch_span(
11711                            PrefillIn::Hidden(&hiddens),
11712                            pos,
11713                            None,
11714                            run,
11715                            self.num_layers,
11716                        )
11717                    } else {
11718                        hiddens
11719                    };
11720                    return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11721                        Err("MiMo attention graph failed after admission".into())
11722                    } else {
11723                        Ok(out)
11724                    };
11725                }
11726                crate::gpu::BatchGraphOutcome::Failed => {
11727                    return Err("batched prefix prefill failed after admission".into());
11728                }
11729                crate::gpu::BatchGraphOutcome::Declined => {
11730                    // Rows an earlier chunk left on the device only.
11731                    #[cfg(feature = "gpu")]
11732                    self.pull_lagging_host_kv(0, self.num_layers, pos);
11733                }
11734            }
11735        }
11736        let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
11737        if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11738            Err("batch tail graph failed after admission".into())
11739        } else {
11740            Ok(out)
11741        }
11742    }
11743
11744    /// The layer-major batched walk over a layer span [from..upto_excl):
11745    /// the whole prefill machinery (chunk graph, batched attends, GEMM
11746    /// panels) for a PARTIAL stack — the network split's prefill rides
11747    /// the same canon as the local one. Input is token ids (embeds
11748    /// itself, coordinator side) or ready boundary hiddens (worker side).
11749    fn prefill_batch_span(
11750        &mut self,
11751        input: PrefillIn<'_>,
11752        start_pos: usize,
11753        task_mask: Option<&TaskMask>,
11754        from: usize,
11755        upto_excl: usize,
11756    ) -> Vec<f32> {
11757        let hs = self.hidden_size;
11758        let b = match input {
11759            PrefillIn::Ids(ids) => ids.len(),
11760            PrefillIn::Hidden(hb) => hb.len() / hs,
11761        };
11762        let upto_excl = upto_excl.min(self.num_layers);
11763        // The CPU embed is deferred: when the chunk graph takes the run
11764        // from layer 0 it gathers the embeddings on the device instead.
11765        // A hidden input is ready by definition.
11766        let mut h: Vec<f32>;
11767        let mut h_ready;
11768        match input {
11769            PrefillIn::Ids(_) => {
11770                h = vec![0.0; b * hs];
11771                h_ready = false;
11772            }
11773            PrefillIn::Hidden(hb) => {
11774                h = hb.to_vec();
11775                h_ready = true;
11776            }
11777        }
11778        let fill_h = |h: &mut Vec<f32>, me: &Self| {
11779            if let PrefillIn::Ids(ids) = input {
11780                for (bi, &id) in ids.iter().enumerate() {
11781                    let e = me.embed_single(id);
11782                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
11783                }
11784                if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11785                    if let Ok(t) = tp.parse::<usize>() {
11786                        if t >= start_pos && t < start_pos + ids.len() {
11787                            let bi = t - start_pos;
11788                            let row = &h[bi * hs..(bi + 1) * hs];
11789                            let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11790                            eprintln!(
11791                                "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
11792                                ids[bi],
11793                                row[0],
11794                                row[1],
11795                                ids.len(),
11796                                &ids[..ids.len().min(8)]
11797                            );
11798                        }
11799                    }
11800                }
11801            }
11802        };
11803        let (_nkv, _hd, _rd, eps) = (
11804            self.num_kv_heads,
11805            self.head_dim,
11806            self.rotary_dim,
11807            self.rms_eps,
11808        );
11809        let pool = self.pool.clone();
11810        let norm_style = self.norm_style;
11811        self.mimo_moe_prepare();
11812        let automatic_gpu_prefix = self.automatic_gpu_prefix();
11813
11814        #[cfg(target_os = "macos")]
11815        let mut chunk_skip_until = 0usize;
11816        // SenseNova-Loop: the chunk rows' mHC streams.
11817        let mut mhc_rs: Vec<f32> = Vec::new();
11818        for li in from..upto_excl {
11819            let _capacity_tail = automatic_gpu_prefix
11820                .filter(|&prefix| {
11821                    li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
11822                })
11823                .map(|_| crate::gpu::enter_cpu_scope());
11824            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU
11825            // Spark-X2.5: the layer's int8 matrix-unit GEMMs take the
11826            // `zi_mm` tile, bit-identical to the 64x64 kernel and about
11827            // three times its rate (`gpu_wgpu::zi_gemm_route`).
11828            #[cfg(feature = "gpu")]
11829            let _fast_gemm = self
11830                .proj_gate_sigmoid
11831                .then(crate::gpu::enter_prefill_fast_gemm);
11832            // GPU chunk graph (default-on under CMF_GPU=1): a run of
11833            // consecutive eligible layers for the whole chunk in ONE
11834            // Metal submission — norm, QKV, RoPE with fused mirror
11835            // append, causal attend, O, FFN, hidden device-resident
11836            // across the run. Any refusal falls through to the CPU path.
11837            #[cfg(target_os = "macos")]
11838            if task_mask.is_none() {
11839                if li < chunk_skip_until {
11840                    continue;
11841                }
11842                // Device-side embedding needs a q8_row embedding matrix;
11843                // with any other layout the CPU fills `h` first and the
11844                // graph starts from a ready hidden (refusing the whole
11845                // run over the embedding alone kept q4t models — the
11846                // whole Nanbeige/Bonsai class — on the CPU prefill).
11847                if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
11848                    fill_h(&mut h, self);
11849                    h_ready = true;
11850                }
11851                let ids_for_embed = match input {
11852                    PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
11853                    PrefillIn::Hidden(_) => None,
11854                };
11855                let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
11856                if end > li {
11857                    h_ready = true;
11858                    chunk_skip_until = end;
11859                    // Looped Transformer: the graph stopped at a loop
11860                    // boundary — apply final norm before the next iteration.
11861                    if self.is_loop_end(end - 1) && end < self.num_layers {
11862                        for bi in 0..b {
11863                            let normed = inference::rms_norm(
11864                                &h[bi * hs..(bi + 1) * hs],
11865                                &self.weights.final_norm,
11866                                eps,
11867                                norm_style,
11868                            );
11869                            h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11870                        }
11871                    }
11872                    continue;
11873                }
11874            }
11875            if !h_ready {
11876                fill_h(&mut h, self);
11877                h_ready = true;
11878            }
11879            if task_mask.is_none() && self.verify_exact_moe {
11880                let positions: Vec<_> = (start_pos..start_pos + b).collect();
11881                match self.mimo_graph_layer_rows(li, &mut h, &positions) {
11882                    crate::gpu::BatchGraphOutcome::Completed => continue,
11883                    crate::gpu::BatchGraphOutcome::Failed => return h,
11884                    crate::gpu::BatchGraphOutcome::Declined => {}
11885                }
11886            }
11887            #[cfg(feature = "gpu")]
11888            self.pull_lagging_host_kv(li, li + 1, start_pos);
11889            // SenseNova-Loop mHC: every row's streams route into `h`; the
11890            // attention below adds onto that branch, so its output is the
11891            // difference from the copy kept here.
11892            let mhc_attn = self.mhc_layer(li).then(|| {
11893                let m = self.mhc.as_deref().expect("mhc layer");
11894                let p = self.phys_layer(li);
11895                let n = m.s.streams * hs;
11896                mhc_rs.resize(b * n, 0.0);
11897                let coefs: Vec<_> = (0..b)
11898                    .map(|bi| {
11899                        let r = &mut mhc_rs[bi * n..(bi + 1) * n];
11900                        if m.enters(p) {
11901                            m.enter(r, &h[bi * hs..(bi + 1) * hs]);
11902                        }
11903                        m.route(p, false, r, &mut h[bi * hs..(bi + 1) * hs])
11904                    })
11905                    .collect();
11906                (h.clone(), coefs)
11907            });
11908            let lw = &self.weights.layers[self.phys_layer(li)];
11909            let t_attn = std::time::Instant::now();
11910            // ── attention ──
11911            match &lw.attn {
11912                AttnKind::Kda(w) => {
11913                    // Projections batched, recurrence sequential.
11914                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
11915                    let mut normed = vec![0.0f32; b * hs];
11916                    for bi in 0..b {
11917                        inference::rms_norm_into(
11918                            &h[bi * hs..(bi + 1) * hs],
11919                            &lw.input_norm,
11920                            eps,
11921                            norm_style,
11922                            &mut normed[bi * hs..(bi + 1) * hs],
11923                        );
11924                    }
11925                    let attn = crate::linear_core::kda_forward_batch(
11926                        &normed,
11927                        b,
11928                        w,
11929                        &cfg,
11930                        &mut self.kv_cache.layers[li].linear_state,
11931                        pool.as_deref(),
11932                    );
11933                    for (dst, &a) in h.iter_mut().zip(&attn) {
11934                        *dst += a;
11935                    }
11936                }
11937                AttnKind::LinearGdn(w) => {
11938                    // Projections batched, recurrence sequential.
11939                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
11940                    let mut normed = vec![0.0f32; b * hs];
11941                    for bi in 0..b {
11942                        let r = inference::rms_norm(
11943                            &h[bi * hs..(bi + 1) * hs],
11944                            &lw.input_norm,
11945                            eps,
11946                            norm_style,
11947                        );
11948                        normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11949                    }
11950                    let attn = crate::linear_core::gdn_forward_batch(
11951                        &normed,
11952                        b,
11953                        w,
11954                        &cfg,
11955                        &mut self.kv_cache.layers[li].linear_state,
11956                        pool.as_deref(),
11957                    );
11958                    for (dst, &a) in h.iter_mut().zip(&attn) {
11959                        *dst += a;
11960                    }
11961                }
11962                AttnKind::ShortConv(w) => {
11963                    // Projections batched over the chunk; the conv walks the
11964                    // contiguous positions in order (same ring as decode).
11965                    let cfg = self
11966                        .short_conv_cfg
11967                        .expect("short-conv layer without short_conv_cfg");
11968                    let mut normed = vec![0.0f32; b * hs];
11969                    for bi in 0..b {
11970                        inference::rms_norm_into(
11971                            &h[bi * hs..(bi + 1) * hs],
11972                            &lw.input_norm,
11973                            eps,
11974                            norm_style,
11975                            &mut normed[bi * hs..(bi + 1) * hs],
11976                        );
11977                    }
11978                    let mut attn = short_conv_forward_batch(
11979                        &normed,
11980                        b,
11981                        w,
11982                        &cfg,
11983                        &mut self.kv_cache.layers[li].linear_state,
11984                        pool.as_deref(),
11985                    );
11986                    // LFM3 sandwich: the mixer output is normalized before
11987                    // the residual, as on the decode path.
11988                    if let Some(wn) = &lw.attn_out_norm {
11989                        for bi in 0..b {
11990                            inference::rms_norm_into(
11991                                &attn[bi * hs..(bi + 1) * hs],
11992                                wn,
11993                                eps,
11994                                norm_style,
11995                                &mut normed[bi * hs..(bi + 1) * hs],
11996                            );
11997                        }
11998                        attn.copy_from_slice(&normed);
11999                    }
12000                    for (dst, &a) in h.iter_mut().zip(&attn) {
12001                        *dst += a;
12002                    }
12003                }
12004                AttnKind::Mla(w) => {
12005                    // Per-position prefill (correctness first; latent
12006                    // batching is a later optimization).
12007                    let inv_freq_l = self.layer_inv_freq(li);
12008                    let rs = self.layer_rope_scale(li);
12009                    let mut normed = vec![0.0f32; hs];
12010                    for bi in 0..b {
12011                        inference::rms_norm_into(
12012                            &h[bi * hs..(bi + 1) * hs],
12013                            &lw.input_norm,
12014                            eps,
12015                            norm_style,
12016                            &mut normed,
12017                        );
12018                        let ao = mla_attention(
12019                            w,
12020                            &normed,
12021                            &mut self.kv_cache.layers[li],
12022                            start_pos + bi,
12023                            &inv_freq_l,
12024                            rs,
12025                            eps,
12026                            pool.as_deref(),
12027                        );
12028                        for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
12029                            *dst += a;
12030                        }
12031                    }
12032                }
12033                AttnKind::Full {
12034                    wq,
12035                    wk,
12036                    wv,
12037                    wo,
12038                    q_norm,
12039                    k_norm,
12040                    output_gate,
12041                    softplus_gate,
12042                    bias,
12043                } => {
12044                    // Chunk-GEMM QKV/O; per-position causal attention
12045                    // inside (roadmap §3 P0 — full-attention prefill no
12046                    // longer re-reads the projection weights b times).
12047                    let mut normed = vec![0.0f32; b * hs];
12048                    for bi in 0..b {
12049                        inference::rms_norm_into(
12050                            &h[bi * hs..(bi + 1) * hs],
12051                            &lw.input_norm,
12052                            eps,
12053                            norm_style,
12054                            &mut normed[bi * hs..(bi + 1) * hs],
12055                        );
12056                    }
12057                    let inv_freq_l = self.layer_inv_freq(li);
12058                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
12059                    let cfg = QwenAttnCfg {
12060                        mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
12061                        num_heads: self.layer_num_heads(li),
12062                        num_kv_heads: nkv_l,
12063                        head_dim: hd_l,
12064                        hidden_size: hs,
12065                        position: start_pos,
12066                        inv_freq: &inv_freq_l,
12067                        rotary_dim: rd_l,
12068                        scale: self.attn_scale,
12069                        softcap: self.attn_softcap,
12070                        window: self.layer_window(li),
12071                        v_norm: self.attn_v_norm,
12072                        qk_norm_after_rope: self.qk_norm_after_rope,
12073                        gate_sigmoid: self.proj_gate_sigmoid,
12074                        q_norm: q_norm.as_deref(),
12075                        k_norm: k_norm.as_deref(),
12076                        output_gate: *output_gate,
12077                        softplus_gate: softplus_gate
12078                            .as_ref()
12079                            .map(|(gate, per_head)| (gate, *per_head)),
12080                        rope_scale: self.layer_rope_scale(li),
12081                        bias: bias
12082                            .as_ref()
12083                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12084                        rms_eps: eps,
12085                        norm_style,
12086                        pool: pool.as_deref(),
12087                        v_head_dim: self.layer_v_dim(li),
12088                    };
12089                    #[cfg(feature = "gpu")]
12090                    let _mirror = self
12091                        .prefill_mirror_target(li)
12092                        .map(crate::gpu::enter_prefill_mirror);
12093                    let mut attn = attention::qwen_attention_batch(
12094                        &normed,
12095                        b,
12096                        wq,
12097                        wk,
12098                        wv,
12099                        wo,
12100                        &mut self.kv_cache.layers[li],
12101                        &cfg,
12102                    );
12103                    if let Some(w) = &lw.attn_out_norm {
12104                        for bi in 0..b {
12105                            inference::rms_norm_into(
12106                                &attn[bi * hs..(bi + 1) * hs],
12107                                w,
12108                                eps,
12109                                norm_style,
12110                                &mut normed[bi * hs..(bi + 1) * hs],
12111                            );
12112                        }
12113                        attn.copy_from_slice(&normed);
12114                    }
12115                    for (dst, &a) in h.iter_mut().zip(&attn) {
12116                        *dst += a;
12117                    }
12118                }
12119                AttnKind::Bounded(w) => {
12120                    // Chunk-GEMM projections, the bounded operator per
12121                    // position over ring + chunk — never a growing KV.
12122                    let mut normed = vec![0.0f32; b * hs];
12123                    for bi in 0..b {
12124                        inference::rms_norm_into(
12125                            &h[bi * hs..(bi + 1) * hs],
12126                            &lw.input_norm,
12127                            eps,
12128                            norm_style,
12129                            &mut normed[bi * hs..(bi + 1) * hs],
12130                        );
12131                    }
12132                    let rope = self
12133                        .bounded_rope
12134                        .clone()
12135                        .expect("bounded layer without an installed rotation table");
12136                    let cfg = crate::bounded::BoundedAttnCfg {
12137                        num_heads: self.num_heads,
12138                        num_kv_heads: self.num_kv_heads,
12139                        head_dim: self.head_dim,
12140                        hidden_size: hs,
12141                        scale: self.attn_scale,
12142                        rope: &rope,
12143                        pool: pool.as_deref(),
12144                    };
12145                    let mut attn = crate::bounded::bounded_attention_batch(
12146                        &normed,
12147                        b,
12148                        w,
12149                        &mut self.kv_cache.layers[li],
12150                        &cfg,
12151                    );
12152                    if let Some(wn) = &lw.attn_out_norm {
12153                        for bi in 0..b {
12154                            inference::rms_norm_into(
12155                                &attn[bi * hs..(bi + 1) * hs],
12156                                wn,
12157                                eps,
12158                                norm_style,
12159                                &mut normed[bi * hs..(bi + 1) * hs],
12160                            );
12161                        }
12162                        attn.copy_from_slice(&normed);
12163                    }
12164                    for (dst, &a) in h.iter_mut().zip(&attn) {
12165                        *dst += a;
12166                    }
12167                    attention::recycle_buf(&mut attn);
12168                }
12169                AttnKind::Linear(w) => {
12170                    for bi in 0..b {
12171                        let normed = inference::rms_norm(
12172                            &h[bi * hs..(bi + 1) * hs],
12173                            &lw.input_norm,
12174                            eps,
12175                            norm_style,
12176                        );
12177                        vmf_phase_forward(
12178                            &normed,
12179                            w,
12180                            &self.vmf_cfg.expect("linear layer without vmf_cfg"),
12181                            &mut self.kv_cache.layers[li].linear_state,
12182                            pool.as_deref(),
12183                        )
12184                        .iter()
12185                        .enumerate()
12186                        .for_each(|(i, &a)| h[bi * hs + i] += a);
12187                    }
12188                }
12189            }
12190
12191            // mHC: fold the attention output into the streams, route the
12192            // FFN's branch.
12193            let mhc_ffn = mhc_attn.map(|(branch, coefs)| {
12194                let m = self.mhc.as_deref().expect("mhc layer");
12195                let p = self.phys_layer(li);
12196                let n = m.s.streams * hs;
12197                let mut y = vec![0f32; hs];
12198                (0..b)
12199                    .map(|bi| {
12200                        for (e, v) in y.iter_mut().enumerate() {
12201                            *v = h[bi * hs + e] - branch[bi * hs + e];
12202                        }
12203                        let r = &mut mhc_rs[bi * n..(bi + 1) * n];
12204                        m.merge(r, &y, &coefs[bi]);
12205                        m.route(p, true, r, &mut h[bi * hs..(bi + 1) * hs])
12206                    })
12207                    .collect::<Vec<_>>()
12208            });
12209            // ── FFN batched ──
12210            let lw = &self.weights.layers[self.phys_layer(li)];
12211            let mut post = vec![0.0f32; b * hs];
12212            for bi in 0..b {
12213                let r =
12214                    inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
12215                post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
12216            }
12217            // A restrictive per-visit FFN row lands on the activations
12218            // inside the dense arm; an all-open row costs nothing.
12219            let mask_row = task_mask
12220                .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
12221                .and_then(|m| m.ffn_masks.get(li))
12222                .map(|v| v.as_slice());
12223            let attn_ns = t_attn.elapsed().as_nanos() as u64;
12224            let t_ffn = std::time::Instant::now();
12225            let mut ffn = match &lw.ffn {
12226                FfnKind::Dense(d) if !d.segs.is_empty() => {
12227                    tube_ffn(d, &post, b, pool.as_deref(), mask_row)
12228                }
12229                FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
12230                FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
12231                    moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
12232                }
12233                FfnKind::Moe(m) if self.verify_exact_moe => {
12234                    moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
12235                }
12236                // Keep prompt expert panels off the projection arena and
12237                // use their routes to prime the model-wide bank.
12238                FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
12239                    let before = m.stats.borrow().clone();
12240                    let out = crate::gpu::cpu_scope(|| {
12241                        moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
12242                    });
12243                    self.mimo_moe.prime(li, m, &before);
12244                    out
12245                }
12246                FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
12247                // Dual-branch layers run per position (the expert branch
12248                // reads the raw residual — nothing to batch yet).
12249                FfnKind::DenseMoe(dm) => {
12250                    let mut out = vec![0.0f32; b * hs];
12251                    for bi in 0..b {
12252                        let r = dense_moe_ffn(
12253                            dm,
12254                            &post[bi * hs..(bi + 1) * hs],
12255                            &h[bi * hs..(bi + 1) * hs],
12256                            eps,
12257                            norm_style,
12258                            pool.as_deref(),
12259                        );
12260                        out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
12261                    }
12262                    out
12263                }
12264            };
12265            if prefill_prof_on() {
12266                PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
12267                PREFILL_SPLIT[1].fetch_add(
12268                    t_ffn.elapsed().as_nanos() as u64,
12269                    std::sync::atomic::Ordering::Relaxed,
12270                );
12271            }
12272            if let Some(w) = &lw.ffn_out_norm {
12273                for bi in 0..b {
12274                    inference::rms_norm_into(
12275                        &ffn[bi * hs..(bi + 1) * hs],
12276                        w,
12277                        eps,
12278                        norm_style,
12279                        &mut post[bi * hs..(bi + 1) * hs],
12280                    );
12281                }
12282                ffn.copy_from_slice(&post);
12283            }
12284            if let Some(coefs) = mhc_ffn {
12285                // mHC: fold the FFN output in; the range's last layer hands
12286                // the streams' mean on as the hidden.
12287                let m = self.mhc.as_deref().expect("mhc layer");
12288                let p = self.phys_layer(li);
12289                let n = m.s.streams * hs;
12290                for bi in 0..b {
12291                    let r = &mut mhc_rs[bi * n..(bi + 1) * n];
12292                    m.merge(r, &ffn[bi * hs..(bi + 1) * hs], &coefs[bi]);
12293                    if m.leaves(p) {
12294                        m.leave(r, &mut h[bi * hs..(bi + 1) * hs]);
12295                    }
12296                }
12297            } else {
12298                for (dst, &f) in h.iter_mut().zip(&ffn) {
12299                    *dst += f;
12300                }
12301            }
12302            if let Some(sc) = lw.layer_scale {
12303                for v in h.iter_mut() {
12304                    *v *= sc;
12305                }
12306            }
12307            // CMF_LAYER_DUMP: every position's hidden after layer li.
12308            if self.layer_dump.is_some() {
12309                for bi in 0..b {
12310                    self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
12311                }
12312            }
12313            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
12314                if let Ok(t) = tp.parse::<usize>() {
12315                    if t >= start_pos && t < start_pos + b {
12316                        let bi = t - start_pos;
12317                        let row = &h[bi * hs..(bi + 1) * hs];
12318                        let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
12319                        eprintln!(
12320                            "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
12321                            row[0], row[1]
12322                        );
12323                    }
12324                }
12325            }
12326            // CMF_DEBUG_LAYERS=1: per-layer hidden-state health of the
12327            // LAST prompt position — the knife for "which layer type
12328            // breaks first" on a new architecture.
12329            if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
12330                let row = &h[(b - 1) * hs..b * hs];
12331                let rms =
12332                    (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
12333                let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
12334                eprintln!(
12335                    "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
12336                    match &self.weights.layers[self.phys_layer(li)].attn {
12337                        AttnKind::LinearGdn(_) => "gdn",
12338                        AttnKind::Linear(_) => "vmf",
12339                        AttnKind::ShortConv(_) => "conv",
12340                        _ => "attn",
12341                    },
12342                    match &lw.ffn {
12343                        FfnKind::Moe(_) => "moe",
12344                        FfnKind::Dense(_) => "dense",
12345                        FfnKind::DenseMoe(_) => "dense+moe",
12346                    },
12347                );
12348            }
12349            // Looped Transformer: apply final norm at the end of each loop iteration.
12350            if self.is_loop_end(li) && li + 1 < self.num_layers {
12351                for bi in 0..b {
12352                    let normed = inference::rms_norm(
12353                        &h[bi * hs..(bi + 1) * hs],
12354                        &self.weights.final_norm,
12355                        eps,
12356                        norm_style,
12357                    );
12358                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
12359                }
12360            }
12361            if std::env::var("CMF_TRACE_H").is_ok() {
12362                let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
12363                let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
12364                eprintln!(
12365                    "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
12366                    lw.layer_scale
12367                );
12368            }
12369        }
12370        crate::gpu::set_layer(-1); // lm_head/final ops outside layer-split
12371        if prefill_prof_on() {
12372            let ms = |a: &std::sync::atomic::AtomicU64| {
12373                a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
12374            };
12375            let sp = &attention::ATTN_SPLIT;
12376            let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
12377            eprintln!(
12378                "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
12379                 host gate {:.1}], host loop {:.1}, attend {:.1} \
12380                 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
12381                 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
12382                ms(&PREFILL_SPLIT[0]),
12383                ms(&sp[0]),
12384                qkv_enq,
12385                qkv_wait,
12386                ms(&sp[6]),
12387                ms(&sp[1]),
12388                ms(&sp[2]),
12389                ms(&sp[4]),
12390                ms(&sp[5]),
12391                ca_wait,
12392                ms(&sp[3]),
12393                ms(&PREFILL_SPLIT[1]),
12394            );
12395            let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
12396            eprintln!(
12397                "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
12398                 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
12399                 fallbacks (cumulative)",
12400                attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
12401            );
12402        }
12403        // A batched span owns a complete set of positions. Publish any
12404        // collecting→sealed transition only after every layer has finished;
12405        // callers that cross into serial/device work must see the new epoch
12406        // before this function returns.
12407        self.o1_progress();
12408        // Every layer of the chunk has appended and attended its rows.
12409        self.swa_trim_tails();
12410        h
12411    }
12412
12413    /// Embed a single token.
12414    fn embed_single(&self, id: u32) -> Vec<f32> {
12415        let mut out = vec![0.0f32; self.hidden_size];
12416        if (id as usize) < self.weights.embed_tokens.rows() {
12417            self.weights.embed_tokens.row_f32(id as usize, &mut out);
12418        }
12419        if self.embed_multiplier != 1.0 {
12420            for v in out.iter_mut() {
12421                *v *= self.embed_multiplier;
12422            }
12423        }
12424        // DeepSeek-V4's hash layers route by TOKEN ID, so the id has to
12425        // reach the forward. It rides in slot 0 (the forward re-reads the
12426        // real embedding itself from the table).
12427        if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
12428            let mut v = vec![0.0f32; self.hidden_size.max(1)];
12429            v[0] = id as f32;
12430            return v;
12431        }
12432        // Gemma-3n: the per-layer-embedding half needs the token ID, so
12433        // it rides appended to the embedding; the g3n forward splits it.
12434        // Gemma-4 E-series PLE: same contract, its own table.
12435        if let Some(b) = &self.g4e {
12436            return b.0.extend_embedding(id, &out, self.pool.as_deref());
12437        }
12438        if let Some(b) = &self.g3n {
12439            return b.0.extend_embedding(id, &out, self.pool.as_deref());
12440        }
12441        out
12442    }
12443
12444    /// A run of consecutive prefill layers on the GPU for the whole
12445    /// chunk (default-on under CMF_GPU=1; CMF_GPU_CHUNK=0 disables).
12446    /// Eligibility per layer: q8_row weights, plain full attention
12447    /// (no output gate), F32 KV, no o1/masks/gemma extras. Returns the
12448    /// first layer index NOT processed (== `li0` when the run is empty).
12449    #[cfg(target_os = "macos")]
12450    fn chunk_run_gpu(
12451        &mut self,
12452        li0: usize,
12453        h: &mut [f32],
12454        b: usize,
12455        pos0: usize,
12456        embed_ids: Option<&[u32]>,
12457        cap: usize,
12458    ) -> usize {
12459        // (The old streaming attend needed a depth bound at ~1k; the
12460        // GEMM attention scales like the CPU path and lifted it.)
12461        // CMF_GPU_CHUNK=0 disables the graph.
12462        // Gemma-4 (`metal_gemma4`) brings its own kernels for the global
12463        // layers, V norm, attention scale and sandwich (`ChunkGemma4`).
12464        // `CMF_METAL_GEMMA4_CHUNK=0` keeps its prompt on the host walk.
12465        let gemma4 =
12466            self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
12467        if !crate::gpu::enabled_here()
12468            || std::env::var("CMF_GPU_CHUNK")
12469                .map(|v| v == "0")
12470                .unwrap_or(false)
12471            || b < self.chunk_min_rows
12472            // MRoPE rows rotate on the host (`attention::MropeView`).
12473            || self.prompt_mrope.is_some()
12474            // Sliding windows with per-layer RoPE ride the chunk graph
12475            // (`metal_graph_swa`: causal_softmax_win, per-layer tables).
12476            || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
12477            || (self.global_attn.is_some() && !gemma4)
12478            // Chunk attention has no value-less learned-sink column. q1
12479            // decode can carry it through ordinary `gqa_attend`, but prompt
12480            // chunks must stay on the exact host attention path.
12481            || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
12482            // Per-layer KV heads and narrow V remain outside this chunk
12483            // graph (the q1 sliding-native door checks the same geometry).
12484            || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
12485            // Collection owns the exact Q trace and boundary conversion;
12486            // this chunk graph appends dense KV without feeding that trace.
12487            || self.o1_active()
12488            || (self.attn_v_norm && !gemma4)
12489            || !self.metal_rope_scale_ok()
12490            || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
12491        {
12492            return li0;
12493        }
12494        let Some(model) = self.model.clone() else {
12495            return li0;
12496        };
12497        let (nh, nkv, hd, hs) = (
12498            self.num_heads,
12499            self.num_kv_heads,
12500            self.head_dim,
12501            self.hidden_size,
12502        );
12503        // Collect the longest run of consecutive eligible layers.
12504        // Looped Transformer: stop at the loop boundary so the CPU can
12505        // apply loop_final_norm between iterations.
12506        let loop_end = if self.loop_final_norm {
12507            ((li0 / self.physical_layers) + 1) * self.physical_layers
12508        } else {
12509            self.num_layers
12510        };
12511        let run_end = self.num_layers.min(loop_end).min(cap);
12512        // Each layer's own RoPE table (Spark-X2.5: sliding and full layers
12513        // differ); the global one for every model without sliding layers.
12514        // Declared before `layers`, which borrows it (and drops a routing
12515        // closure on MoE layers).
12516        let tables: Vec<std::sync::Arc<Vec<f32>>> =
12517            (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
12518        let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
12519        let mut stored_at: Vec<usize> = Vec::new();
12520        for li in li0..run_end {
12521            if self.mhc_layer(li) {
12522                break;
12523            }
12524            let lw = &self.weights.layers[self.phys_layer(li)];
12525            if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
12526                && !gemma4
12527            {
12528                break;
12529            }
12530            let AttnKind::Full {
12531                wq,
12532                wk,
12533                wv,
12534                wo,
12535                q_norm,
12536                k_norm,
12537                output_gate: false,
12538                softplus_gate,
12539                bias,
12540            } = &lw.attn
12541            else {
12542                break;
12543            };
12544            // Spark-X2.5's per-head sigmoid g_proj gate, from f32 rows.
12545            let head_gate = match softplus_gate {
12546                None => None,
12547                Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
12548                    Some((d, r, c)) if r == nh && c == hs => Some(d),
12549                    _ => break,
12550                },
12551                Some(_) => break,
12552            };
12553            // The FFN: a dense SiLU/GELU trio, or routed q4tp experts
12554            // (Mellum2.1: softmax router, no shared expert) whose first
12555            // trio stands in for the shape checks.
12556            let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
12557                FfnKind::Dense(d)
12558                    if (matches!(d.act, Act::Silu | Act::Gelu)
12559                        || (gemma4 && d.act == Act::GeluTanh))
12560                        && d.segs.is_empty() =>
12561                {
12562                    (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
12563                }
12564                FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
12565                    Some((router, experts, q8)) => {
12566                        let e0 = &m.experts[0];
12567                        let route = Box::new(move |lg: &[f32]| {
12568                            // `moe_ffn_batch`'s routing and weights, row by row.
12569                            let (idx, p, wsum) = moe_route(lg, m, None);
12570                            let mut st = m.stats.borrow_mut();
12571                            if st.len() < m.experts.len() {
12572                                st.resize(m.experts.len(), 0);
12573                            }
12574                            for &e in &idx {
12575                                st[e] += 1;
12576                            }
12577                            let w = idx.iter().map(|&e| p[e] / wsum).collect();
12578                            (idx, w)
12579                        });
12580                        (
12581                            &e0.gate_proj,
12582                            &e0.up_proj,
12583                            &e0.down_proj,
12584                            Act::Silu,
12585                            Some(crate::gpu_metal::ChunkMoe {
12586                                router,
12587                                experts,
12588                                q8,
12589                                route,
12590                            }),
12591                        )
12592                    }
12593                    None => break,
12594                },
12595                _ => break,
12596            };
12597            // q8_row (row_scale populated), q8_2f (plus its input field),
12598            // or q4_tiled / q4tp (row_scale empty — their scales are in the
12599            // payload). Mixing across the seven projections of one layer is
12600            // fine; the encoder branches per weight on the tensor's dtype.
12601            // Anything else refuses.
12602            fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
12603                t.q8_row_parts()
12604                    .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
12605                    .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12606                    .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12607            }
12608            fn cf(t: &QTensor) -> &[f32] {
12609                t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
12610            }
12611            let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
12612            let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
12613            // q8_2f projections ride the chunk graph on the MoE layers it
12614            // was measured on (Mellum2.1) and on the dense exact-GELU FFN
12615            // (Spark-X2.6-4B q8_2f on the M4: the gate/up inputs staged as
12616            // x·col, the down field applied after the activation —
12617            // `ChunkLayer::ffn_col`). A dense SiLU q8_2f stack keeps the host
12618            // walk: its down GEMM fuses the activation into the X-tile load
12619            // and has no input field (0.8.14 refused every q8_2f weight here).
12620            // Gemma-4 reads every q8_2f weight through the blob GEMM with its
12621            // own field (`ChunkGemma4`), so its dense q8_2f stack rides.
12622            if moe.is_none()
12623                && !gemma4
12624                && ffn_act != Act::Gelu
12625                && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
12626            {
12627                break;
12628            }
12629            let parts = (
12630                cw(wq),
12631                cw(wk),
12632                cw(wv),
12633                cw(wo),
12634                cw(gate_t),
12635                cw(up_t),
12636                cw(down_t),
12637            );
12638            let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
12639            else {
12640                break;
12641            };
12642            let layer = &self.kv_cache.layers[li];
12643            if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
12644                break;
12645            }
12646            // A trimmed tail holds every row the chunk's first query reads.
12647            debug_assert!(
12648                layer.base() == 0
12649                    || self
12650                        .layer_window(li)
12651                        .is_some_and(|w| layer.head_len(0) + 1 >= w)
12652            );
12653            stored_at.push(layer.head_len(0));
12654            // Per-layer geometry for Gemma-4 (512-wide global layers over
12655            // one KV head); every other model keeps the pipeline-wide one.
12656            let (nkv, hd) = if gemma4 {
12657                let (a, bb, _) = self.layer_geom(li);
12658                (a, bb)
12659            } else {
12660                (nkv, hd)
12661            };
12662            layers.push(crate::gpu_metal::ChunkLayer {
12663                model: &model,
12664                kv_id: self.graph_kv_id,
12665                layer: li,
12666                wq: pq,
12667                wk: pk,
12668                wv: pv,
12669                wo: po,
12670                gate: pg,
12671                up: pu,
12672                down: pd,
12673                input_norm: &lw.input_norm,
12674                post_norm: &lw.post_norm,
12675                bias: bias
12676                    .as_ref()
12677                    .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
12678                q_norm: q_norm.as_deref(),
12679                k_norm: k_norm.as_deref(),
12680                inv_freq: &tables[li - li0],
12681                rope_scale: self.layer_rope_scale(li),
12682                rd: self.layer_geom(li).2,
12683                nh,
12684                nkv,
12685                hd,
12686                hs,
12687                inter: gate_t.rows(),
12688                gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
12689                late_qk_norm: self.qk_norm_after_rope,
12690                eps: self.rms_eps as f32,
12691                window: self.layer_window(li),
12692                head_gate,
12693                act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
12694                g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
12695                    v_norm: self.attn_v_norm,
12696                    scale: self.attn_scale,
12697                    attn_out_norm: lw.attn_out_norm.as_deref(),
12698                    ffn_out_norm: lw.ffn_out_norm.as_deref(),
12699                    layer_scale: lw.layer_scale,
12700                }),
12701                col,
12702                // The plain dense branch alone reads it: routed experts and
12703                // Gemma-4 carry their own fields.
12704                ffn_col: if moe.is_none() && !gemma4 {
12705                    ffn_col
12706                } else {
12707                    [&[][..]; 3]
12708                },
12709                moe,
12710            });
12711        }
12712        if layers.is_empty() {
12713            return li0;
12714        }
12715        // KV row width per layer (uniform but for Gemma-4).
12716        let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
12717        let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
12718            .iter()
12719            .zip(&rows_kv)
12720            .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
12721            .collect();
12722        let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
12723        for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
12724            let li = layers[i].layer;
12725            let layer = &self.kv_cache.layers[li];
12726            let nkv = rows_kv[i].0;
12727            io.push(crate::gpu_metal::ChunkIo {
12728                cpu_stored: stored_at[i],
12729                cpu_gen: layer.generation(),
12730                cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
12731                cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
12732                out_k: ok,
12733                out_v: ov,
12734                imp: oi,
12735            });
12736        }
12737        let n_run = layers.len();
12738        let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
12739        // Device-side embedding when the run starts the model and the
12740        // embedding matrix is q8_row-mapped.
12741        let ep = embed_ids.and_then(|ids| {
12742            self.weights
12743                .embed_tokens
12744                .q8_row_parts()
12745                .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
12746                    idx,
12747                    rows,
12748                    row_scale: rs,
12749                    ids,
12750                    mult: self.embed_multiplier,
12751                })
12752        });
12753        if embed_ids.is_some() && ep.is_none() {
12754            return li0;
12755        }
12756        if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
12757            return li0;
12758        }
12759        drop(io);
12760        drop(layers);
12761        // CPU caches stay the owners of record: append the chunk rows
12762        // and bank the importance masses per layer.
12763        for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
12764            let li = li0 + i;
12765            let row = rows_kv[i].1;
12766            let layer = &mut self.kv_cache.layers[li];
12767            for bi in 0..b {
12768                layer.append(
12769                    &ok[bi * row..(bi + 1) * row],
12770                    &ov[bi * row..(bi + 1) * row],
12771                    &[],
12772                );
12773            }
12774            layer.accumulate_imp(oi);
12775        }
12776        last
12777    }
12778
12779    /// Is layer `li` a sliding-window (local-RoPE) layer? Gemma-3:
12780    /// every `pattern`-th layer is global, the rest are local.
12781    fn layer_is_local(&self, li: usize) -> bool {
12782        if let Some(layers) = &self.sliding_layers {
12783            return layers.get(li).copied().unwrap_or(false);
12784        }
12785        match self.swa {
12786            Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
12787            None => false,
12788        }
12789    }
12790
12791    /// The RoPE table for layer `li` (local layers may have their own;
12792    /// Gemma-4 global layers use the proportional padded table).
12793    fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
12794        if self.layer_is_local(li) {
12795            if let Some(f) = &self.inv_freq_local {
12796                return f.clone();
12797            }
12798        } else if let Some(f) = &self.inv_freq_global {
12799            return f.clone();
12800        }
12801        self.inv_freq.clone()
12802    }
12803
12804    /// The attend window for layer `li` (None = full context).
12805    fn layer_window(&self, li: usize) -> Option<usize> {
12806        self.swa
12807            .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
12808    }
12809
12810    /// Drop every sliding layer's rows its window can no longer read
12811    /// (`LayerKvCache::trim_window`). Call only at a safe point — after a
12812    /// complete position / pair / chunk walk, never between a layer's
12813    /// appends and its attend. One comparison per layer below the trigger.
12814    ///
12815    /// Off with an MTP head: its verify oracles and rollbacks snapshot and
12816    /// compare per-layer row counts as positions (`CMF_METAL_VERIFY_CHECK`,
12817    /// the MTP caches), which a trimmed tail would break.
12818    fn swa_trim_tails(&mut self) {
12819        let Some((slack, align)) = self.swa_trim else {
12820            return;
12821        };
12822        if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
12823            return;
12824        }
12825        for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
12826            if let Some(w) = self.layer_window(li) {
12827                self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
12828            }
12829        }
12830    }
12831
12832    fn layer_num_heads(&self, li: usize) -> usize {
12833        self.attention_heads_per_layer
12834            .as_ref()
12835            .and_then(|v| v.get(li).copied())
12836            .unwrap_or(self.num_heads)
12837    }
12838
12839    fn layer_rope_scale(&self, li: usize) -> f32 {
12840        if self.layer_is_local(li) {
12841            self.rope_scale_local
12842        } else {
12843            self.rope_scale
12844        }
12845    }
12846
12847    /// Attention geometry of layer `li`: (num_kv_heads, head_dim,
12848    /// rotary_dim). Gemma-4 global layers override all three.
12849    fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
12850        if !self.layer_is_local(li) {
12851            if let Some((ghd, gkv)) = self.global_attn {
12852                return (gkv, ghd, ghd);
12853            }
12854        }
12855        let rd = if self.layer_is_local(li) {
12856            self.rotary_dim_local.unwrap_or(self.rotary_dim)
12857        } else {
12858            self.rotary_dim
12859        };
12860        // A layer without positional encoding rotates nothing.
12861        let rd = if self
12862            .no_rope_layers
12863            .as_ref()
12864            .and_then(|v| v.get(li))
12865            .copied()
12866            == Some(true)
12867        {
12868            0
12869        } else {
12870            rd
12871        };
12872        (self.layer_num_kv_heads(li), self.head_dim, rd)
12873    }
12874
12875    /// KV heads of layer `li` (virtual index): the per-layer count when the
12876    /// model has one (MiMo-V2), else the uniform `num_kv_heads`.
12877    fn layer_num_kv_heads(&self, li: usize) -> usize {
12878        self.kv_heads_per_layer
12879            .as_ref()
12880            .and_then(|v| v.get(self.phys_layer(li)).copied())
12881            .unwrap_or(self.num_kv_heads)
12882    }
12883
12884    /// V head width of layer `li` (≤ its head_dim).
12885    fn layer_v_dim(&self, li: usize) -> usize {
12886        let (_, hd, _) = self.layer_geom(li);
12887        self.v_head_dim.unwrap_or(hd).min(hd)
12888    }
12889
12890    /// Install a per-layer KV geometry: KV heads per PHYSICAL layer and/or
12891    /// a V head width narrower than `head_dim` (MiMo-V2). Validates it and
12892    /// reshapes the caches of every layer whose KV head count differs from
12893    /// `num_kv_heads`. The loader and the tests share this one path, so a
12894    /// hand-built pipeline cannot hold a geometry the loader would refuse.
12895    /// Call before the first forward (it drops cached rows of reshaped
12896    /// layers). Refuses combinations whose paths would read it wrong:
12897    /// Gemma-4 global layers and MLA carry their own geometry.
12898    pub fn set_attn_geometry(
12899        &mut self,
12900        kv_heads_per_layer: Option<Vec<usize>>,
12901        v_head_dim: Option<usize>,
12902    ) -> Result<(), String> {
12903        if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
12904            if self.global_attn.is_some() {
12905                return Err(
12906                    "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
12907                     attention geometry"
12908                        .into(),
12909                );
12910            }
12911            if self
12912                .weights
12913                .layers
12914                .iter()
12915                .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
12916            {
12917                return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
12918            }
12919        }
12920        if let Some(vd) = v_head_dim {
12921            if vd == 0 || vd > self.head_dim {
12922                return Err(format!(
12923                    "v_head_dim {vd} must be in 1..={} (head_dim)",
12924                    self.head_dim
12925                ));
12926            }
12927        }
12928        if let Some(v) = &kv_heads_per_layer {
12929            if v.len() != self.physical_layers {
12930                return Err(format!(
12931                    "kv_heads_per_layer has {} entries, expected {} layers",
12932                    v.len(),
12933                    self.physical_layers
12934                ));
12935            }
12936            for (li, &nkv) in v.iter().enumerate() {
12937                let is_attn = matches!(
12938                    self.weights.layers.get(li).map(|lw| &lw.attn),
12939                    Some(AttnKind::Full { .. }) | None
12940                );
12941                if !is_attn {
12942                    continue;
12943                }
12944                let nh = self
12945                    .attention_heads_per_layer
12946                    .as_ref()
12947                    .and_then(|h| h.get(li).copied())
12948                    .unwrap_or(self.num_heads);
12949                if nkv == 0 || nh % nkv != 0 {
12950                    return Err(format!(
12951                        "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
12952                    ));
12953                }
12954            }
12955        }
12956        self.kv_heads_per_layer = kv_heads_per_layer;
12957        self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
12958        if self.kv_heads_per_layer.is_some() {
12959            for li in 0..self.kv_cache.layers.len() {
12960                let full = matches!(
12961                    self.weights
12962                        .layers
12963                        .get(self.phys_layer(li))
12964                        .map(|lw| &lw.attn),
12965                    Some(AttnKind::Full { .. })
12966                );
12967                let nkv = self.layer_num_kv_heads(li);
12968                let cache = &self.kv_cache.layers[li];
12969                if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
12970                    let sinks = cache.sinks.clone();
12971                    self.kv_cache.layers[li] =
12972                        crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
12973                    self.kv_cache.layers[li].sinks = sinks;
12974                }
12975            }
12976        }
12977        Ok(())
12978    }
12979
12980    /// Attach learned attention-sink logits (one per Q head) to PHYSICAL
12981    /// layer `phys` — every virtual layer that runs it. The loader calls
12982    /// this for each `model.layers.N.self_attn.sinks` tensor.
12983    pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
12984        let Some(lw) = self.weights.layers.get(phys) else {
12985            return Err(format!("sinks for layer {phys}: no such layer"));
12986        };
12987        if !matches!(lw.attn, AttnKind::Full { .. }) {
12988            return Err(format!(
12989                "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
12990            ));
12991        }
12992        let nh = self
12993            .attention_heads_per_layer
12994            .as_ref()
12995            .and_then(|h| h.get(phys).copied())
12996            .unwrap_or(self.num_heads);
12997        if sinks.len() != nh {
12998            return Err(format!(
12999                "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
13000                sinks.len()
13001            ));
13002        }
13003        if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
13004            return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
13005        }
13006        for li in 0..self.kv_cache.layers.len() {
13007            if self.phys_layer(li) == phys {
13008                self.kv_cache.layers[li].sinks = Some(sinks.clone());
13009            }
13010        }
13011        Ok(())
13012    }
13013
13014    /// Why the GPU attention graphs cannot serve this model, if they
13015    /// cannot: the wgpu whole-token and batched graphs, the greedy
13016    /// multi-burst, the q1 attention dropin and the Metal block/chunk/rows
13017    /// graphs all assume ONE (num_kv_heads, head_dim) geometry, V heads as
13018    /// wide as K, a single RoPE table, full-context attention and a plain
13019    /// softmax. A model outside that contract runs on the CPU layer walk
13020    /// (and the per-op GPU matvecs) until a graph learns it — never on a
13021    /// graph that would read it wrong. None = no attention-level reason
13022    /// (the graph builders still check weights and layer kinds).
13023    pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
13024        if self.kv_heads_per_layer.is_some() {
13025            return Some("per-layer KV head counts");
13026        }
13027        if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
13028            return Some("V heads narrower than Q/K heads");
13029        }
13030        if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
13031            return Some("learned attention sinks");
13032        }
13033        if self.swa.is_some() || self.sliding_layers.is_some() {
13034            return Some("sliding-window layers");
13035        }
13036        None
13037    }
13038
13039    /// Can the Metal kernels apply this model's RoPE amplitude? Every
13040    /// rotation kernel (`attn_rope_qkn[_b]`, `chunk_rope_kv`) multiplies the
13041    /// rotated pair by the layer's `rope_scale` (YaRN `attention_factor`,
13042    /// Mellum2.1's global layers) exactly where the CPU's
13043    /// `rope_rotate_scaled` does. With the Q/K norm AFTER RoPE the kernels'
13044    /// shared sum of squares would be the unscaled one, so that pairing
13045    /// (no known model) declines.
13046    #[cfg(target_os = "macos")]
13047    fn metal_rope_scale_ok(&self) -> bool {
13048        !(self.qk_norm_after_rope
13049            && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
13050    }
13051
13052    /// Can the Metal block graph carry this model's sliding-window layers?
13053    /// Both of its attention forms take a layer's own window, RoPE table
13054    /// and rotary width: the device attend (`AttnDeviceParams::window`, the
13055    /// layer's `inv_freq`/`rd`) and the sandwich's host attend (exactly the
13056    /// CPU path's attention). True when the sliding window is the model's
13057    /// only attention-level decline (Spark-X2.5). Learned, value-less
13058    /// sinks may accompany that geometry: q1's ordinary device GQA
13059    /// includes the sink in its softmax denominator and disables split-K.
13060    /// Per-layer KV heads, narrow V and Gemma-4 global geometry still
13061    /// decline; a YaRN amplitude per layer rides along
13062    /// (`metal_rope_scale_ok`).
13063    ///
13064    /// Per-layer Q heads (Laguna) and capped scores (Gemma-2) decline here
13065    /// too, with V norm (Gemma-4): the chunk prefill's own gate checks
13066    /// neither of the first two, and before this door opened its `swa`
13067    /// refusal kept every sliding model with them off the chunk graph.
13068    #[cfg(target_os = "macos")]
13069    fn metal_graph_swa(&self) -> bool {
13070        (self.swa.is_some() || self.sliding_layers.is_some())
13071            && self.attention_heads_per_layer.is_none()
13072            && !self.attn_v_norm
13073            && self.attn_softcap == 0.0
13074            && self.kv_heads_per_layer.is_none()
13075            && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
13076            && self.global_attn.is_none()
13077            && self.inv_freq_global.is_none()
13078            && self.metal_rope_scale_ok()
13079            && !(0..self.num_layers).any(|li| {
13080                self.layer_is_local(li)
13081                    && self.inv_freq_local.is_none()
13082                    && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13083            })
13084    }
13085
13086    /// Can the Metal graphs carry this Gemma-4 model (`gpu_metal::gemma4`)?
13087    /// Its sliding layers (window, local RoPE table, 256-wide heads)
13088    /// device-attend like Spark-X2.5's; the 512-wide global layers (one KV
13089    /// head, proportional RoPE in the zero-padded table) take the 16-slot
13090    /// kernels; the V norm, sandwich norms, layer scalar and tanh-GELU come
13091    /// from the Gemma-4 library. Dense FFNs only (no Gemma-4 MoE measured).
13092    /// `CMF_METAL_GEMMA4=0` declines it as before (CPU attention).
13093    #[cfg(target_os = "macos")]
13094    fn metal_gemma4(&self) -> bool {
13095        crate::gpu_metal::gemma4::enabled()
13096            && (self.global_attn.is_some() || self.attn_v_norm)
13097            && self.attention_heads_per_layer.is_none()
13098            && self.kv_heads_per_layer.is_none()
13099            && self.v_head_dim.is_none()
13100            && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
13101            && self.attn_softcap == 0.0
13102            && !self.qk_norm_after_rope
13103            && self.metal_rope_scale_ok()
13104            && self
13105                .weights
13106                .layers
13107                .iter()
13108                .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
13109            && !(0..self.num_layers).any(|li| {
13110                self.layer_is_local(li)
13111                    && self.inv_freq_local.is_none()
13112                    && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13113            })
13114    }
13115
13116    /// Why the WGPU graphs (whole-token, batched prefill, greedy burst)
13117    /// cannot run this model's attention, if they cannot. Per-layer KV
13118    /// heads, V narrower than K, learned sinks and sliding windows ride
13119    /// their per-layer geometry (`GraphAttnGeom`, the ATTEND_X kernels);
13120    /// what that geometry does not express keeps the decline, by name.
13121    /// None for every model with one attention geometry.
13122    pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
13123        // Keep the ordinary (uniform) fast arms off YaRN until they carry a
13124        // scale field too. A non-uniform geometry gets a `GraphAttnGeom` and
13125        // is handled below by the generic graph kernels.
13126        let geometry_reason = self.graph_attn_decline_reason();
13127        let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
13128        if has_scaled_rope && geometry_reason.is_none() {
13129            return Some("scaled RoPE without per-layer geometry");
13130        }
13131        // For Q/K norm after partial RoPE, the scale changes only the rotary
13132        // slice's RMS denominator. The generic graph deliberately declines
13133        // that uncommon combination until it carries a separate rotary sum;
13134        // Mellum has q/k norm before RoPE, so it remains on this path.
13135        if has_scaled_rope && self.qk_norm_after_rope {
13136            return Some("scaled RoPE with post-RoPE Q/K norm");
13137        }
13138        geometry_reason?;
13139        // Gemma-4: global layers 512 wide over 256-wide sliding ones
13140        // (`GraphAttnGeom::hd`), the scale-less V norm (`v_norm`), and the
13141        // sandwich norms + layer scalar (`GraphLayer`) ride the TOKEN graph.
13142        // The batch graph has none of the three and refuses them itself
13143        // (`batch_graph_gemma_decline`).
13144        if self.attention_heads_per_layer.is_some() {
13145            return Some("per-layer Q head counts with per-layer geometry");
13146        }
13147        // Sandwich norms are measured on Gemma-4 (the humanizer port) and
13148        // LFM3, both with plain x̂·w weights, against HF transformers;
13149        // Gemma-2/3's (1+w) norms keep the per-op path until they are
13150        // measured the same way, and a layer scale stays Gemma-4's.
13151        let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
13152        let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
13153        if !gemma4
13154            && self.weights.layers.iter().any(|lw| {
13155                lw.layer_scale.is_some()
13156                    || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some()))
13157            })
13158        {
13159            return Some("sandwich norms / layer scale with per-layer geometry");
13160        }
13161        // `GraphAttnGeom::rope_scale` carries YaRN's post-rotation
13162        // attention-amplitude factor to the WGPU RoPE kernel. It is not an
13163        // angular position scale, so it must not be folded into `invf`.
13164        if self.weights.layers.iter().any(|lw| {
13165            matches!(
13166                &lw.attn,
13167                AttnKind::Full {
13168                    output_gate: true,
13169                    ..
13170                }
13171            )
13172        }) && self.v_head_dim.is_some()
13173        {
13174            return Some("gated attention with V narrower than K");
13175        }
13176        if (0..self.num_layers).any(|li| {
13177            self.layer_is_local(li)
13178                && self.inv_freq_local.is_none()
13179                && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13180        }) {
13181            return Some("local rotary width without a local RoPE table");
13182        }
13183        None
13184    }
13185
13186    /// What the wgpu BATCH graph (and the attention-only singleton graphs)
13187    /// cannot run of Gemma-4, which the token graph does run: per-layer
13188    /// head widths, the V norm, sandwich norms and the layer scalar.
13189    fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
13190        // Gemma-4 rides `gemma_batch` (per-layer head width, V norm,
13191        // sandwich norms, layer scalar) unless CMF_G4_BATCH=0.
13192        if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
13193            return None;
13194        }
13195        if self.global_attn.is_some() {
13196            return Some("per-layer head width (Gemma-4 global layers)");
13197        }
13198        if self.attn_v_norm {
13199            return Some("V norm");
13200        }
13201        if self.weights.layers.iter().any(|lw| {
13202            lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
13203        }) {
13204            return Some("sandwich norms / layer scale");
13205        }
13206        None
13207    }
13208
13209    /// The wgpu graphs' attention geometry for layer `li` (virtual index):
13210    /// Some only for a model whose layers do not share one (MiMo-V2) — KV
13211    /// heads, V width, rotary width and RoPE table, window and sinks of
13212    /// THIS layer, exactly what the CPU attention reads for it.
13213    fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
13214        self.graph_attn_decline_reason()?;
13215        let (nkv, hd, rd) = self.layer_geom(li);
13216        let invf: &[f32] = if self.layer_is_local(li) {
13217            match &self.inv_freq_local {
13218                Some(f) => f.as_slice(),
13219                None => self.inv_freq.as_slice(),
13220            }
13221        } else {
13222            match &self.inv_freq_global {
13223                Some(f) => f.as_slice(),
13224                None => self.inv_freq.as_slice(),
13225            }
13226        };
13227        Some(crate::gpu::GraphAttnGeom {
13228            nkv,
13229            hd,
13230            dv: self.layer_v_dim(li),
13231            rd,
13232            invf,
13233            rope_scale: self.layer_rope_scale(li),
13234            window: self.layer_window(li),
13235            sink: self.kv_cache.layers[li].sinks.as_deref(),
13236            v_norm: self.attn_v_norm,
13237        })
13238    }
13239
13240    /// The decode graph's device K/V mirror that layer `li`'s batched
13241    /// prefill appends its chunk to and attends against (wgpu), instead
13242    /// of uploading the layer's whole K/V prefix every chunk and the whole
13243    /// cache again at the first decode token. Only where the token graph
13244    /// will read that very mirror: the decode graph on, not refused, and
13245    /// this layer's geometry what it requests (`graph_attn_geom`: KV heads,
13246    /// V as wide as K, the window as a ring, no sink).
13247    ///
13248    /// Spark-X2.5 only (`proj_gate_sigmoid`), the one architecture whose
13249    /// output was measured identical on it. `CMF_PREFILL_MIRROR=0` keeps
13250    /// the host upload (A/B).
13251    #[cfg(feature = "gpu")]
13252    fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
13253        if !self.proj_gate_sigmoid
13254            || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
13255            || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
13256            || self.graph_refused()
13257            || self.o1_active()
13258            || self.attn_softcap > 0.0
13259            || self.wgpu_graph_attn_decline().is_some()
13260        {
13261            return None;
13262        }
13263        let g = self.graph_attn_geom(li)?;
13264        let (nkv, hd, _) = self.layer_geom(li);
13265        // The token graph passes the call-wide head width (layer 0's).
13266        if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
13267            return None;
13268        }
13269        Some(crate::gpu::PrefillMirror {
13270            kv_id: self.graph_kv_id,
13271            layer: li,
13272            limit: self.kv_cache.max_seq_len,
13273        })
13274    }
13275
13276    /// Bring the host KV cache of every Full-attention layer in
13277    /// `[from, upto)` up to `position` rows from the wgpu mirrors, where a
13278    /// device graph advanced a layer that the host is about to run: a
13279    /// device prefix that shrank since the prompt (or a batched prefill
13280    /// prefix longer than the decode one). A layer whose mirror does not
13281    /// hold the missing rows is left alone. Rows a sliding layer's ring
13282    /// no longer holds come back as zeros — outside every window that
13283    /// will read them.
13284    #[cfg(feature = "gpu")]
13285    fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
13286        let kv_id = self.graph_kv_id;
13287        for li in from..upto.min(self.num_layers) {
13288            if !matches!(
13289                self.weights.layers[self.phys_layer(li)].attn,
13290                AttnKind::Full { .. }
13291            ) {
13292                continue;
13293            }
13294            // Absolute: a trimmed sliding tail holds positions base.. only.
13295            let host = self.kv_cache.layers[li].pos_len();
13296            if host >= position {
13297                continue;
13298            }
13299            let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
13300                continue;
13301            };
13302            let to = dev.min(position);
13303            if to <= host {
13304                continue;
13305            }
13306            let (nkv, hd) = {
13307                let c = &self.kv_cache.layers[li];
13308                (c.num_kv_heads, c.head_dim)
13309            };
13310            let Some((k, v, first_valid)) =
13311                crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
13312            else {
13313                continue;
13314            };
13315            // A sliding layer only ever reads its last `window` rows; a
13316            // full-context layer needs every row it did not have.
13317            let need_from = match self.layer_window(li) {
13318                Some(w) => host.max((position + 1).saturating_sub(w)),
13319                None => host,
13320            };
13321            if first_valid > need_from {
13322                tracing::warn!(
13323                    "layer {li}: device KV rows {host}..{to} no longer resident \
13324                     (from {first_valid}); host attention will miss them"
13325                );
13326            }
13327            let row = nkv * hd;
13328            let cache = &mut self.kv_cache.layers[li];
13329            for p in 0..to - host {
13330                cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
13331            }
13332        }
13333    }
13334
13335    /// Log (once per graph site and pipeline) that `site` declined for
13336    /// `reason`. The lines are kept so a caller or a test can read them.
13337    fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
13338        let mut seen = self.graph_declines.borrow_mut();
13339        if !seen.iter().any(|&(s, r)| s == site && r == reason) {
13340            tracing::warn!("{site} declined: {reason} (CPU attention path)");
13341            seen.push((site, reason));
13342        }
13343    }
13344
13345    /// The GPU-graph declines this pipeline has logged so far, as the
13346    /// logged lines.
13347    pub fn graph_declines(&self) -> Vec<String> {
13348        self.graph_declines
13349            .borrow()
13350            .iter()
13351            .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
13352            .collect()
13353    }
13354
13355    /// Does layer `li` have the plain attention geometry the historical
13356    /// head-masked f32 path (`multi_head_attention`) assumes — pipeline-wide
13357    /// KV heads / head_dim / RoPE table, full context, no sink, V as wide
13358    /// as K? Anything else runs the dense `qwen_attention` instead.
13359    /// `CMF_LAYER_DUMP` writer (see `Pipeline::layer_dump`): one position's
13360    /// hidden after layer `li` as raw little-endian f32 into
13361    /// `<dir>/p{pos:06}_l{li:02}.f32`. A failed write is reported once and
13362    /// never stops the forward — the dump is a diagnostic.
13363    fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
13364        let Some(dir) = &self.layer_dump else {
13365            return;
13366        };
13367        let mut bytes = Vec::with_capacity(row.len() * 4);
13368        for v in row {
13369            bytes.extend_from_slice(&v.to_le_bytes());
13370        }
13371        let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
13372        if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
13373            use std::sync::atomic::{AtomicBool, Ordering};
13374            static SAID: AtomicBool = AtomicBool::new(false);
13375            if !SAID.swap(true, Ordering::Relaxed) {
13376                tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
13377            }
13378        }
13379    }
13380
13381    /// Decide the MiMo-V2 expert placement once (`crate::mimo_moe`). Any
13382    /// other model turns the slot off on the first call.
13383    fn mimo_moe_prepare(&mut self) {
13384        if !self.mimo_moe.is_undecided() {
13385            return;
13386        }
13387        let slot = {
13388            let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
13389                .filter_map(
13390                    |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
13391                        FfnKind::Moe(m) => Some((li, m)),
13392                        _ => None,
13393                    },
13394                )
13395                .collect();
13396            // One bank lives on one device: an in-process multi-GPU split
13397            // keeps the whole-layer path.
13398            if layers.is_empty()
13399                || self.physical_layers != self.num_layers
13400                || self.gpu_plan.is_some()
13401            {
13402                crate::mimo_moe::Slot::Off
13403            } else {
13404                // Whether a whole-token graph could run this model's layers
13405                // (then a whole-layer prefix is one submit, not per-layer
13406                // fences).
13407                let graph_prefix =
13408                    self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
13409                crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
13410            }
13411        };
13412        self.mimo_moe = slot;
13413    }
13414
13415    #[cfg(test)]
13416    pub(crate) fn test_graph_kv_id(&self) -> u64 {
13417        self.graph_kv_id
13418    }
13419
13420    /// Dynamic MiMo layer: one device attention graph, followed by a bank
13421    /// frame. Both decode and short verification use this same attention
13422    /// path and absolute layer key; the host KV may intentionally lag.
13423    pub(crate) fn mimo_graph_layer_rows(
13424        &mut self,
13425        li: usize,
13426        h: &mut [f32],
13427        positions: &[usize],
13428    ) -> crate::gpu::BatchGraphOutcome {
13429        use crate::gpu::BatchGraphOutcome as Out;
13430        let b = positions.len();
13431        if !(1..=4).contains(&b)
13432            || h.len() != b * self.hidden_size
13433            || !self.mimo_moe.is_dynamic(li, true)
13434            || !crate::gpu::enabled_here()
13435            || !crate::gpu::wgpu_active()
13436            || self.o1_active()
13437            || self.physical_layers != self.num_layers
13438            // The pair-fusion diagnostic (and an explicit graph-off run)
13439            // rewinds only host KV. A hidden singleton attention graph here
13440            // would leave device mirrors ahead of the next host position.
13441            || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
13442            || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
13443            || self.wgpu_graph_attn_decline().is_some()
13444        {
13445            return Out::Declined;
13446        }
13447        let attn_started = std::time::Instant::now();
13448        let outcome = {
13449            let lw = &self.weights.layers[li];
13450            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13451                return Out::Declined;
13452            }
13453            let FfnKind::Moe(m) = &lw.ffn else {
13454                return Out::Declined;
13455            };
13456            let AttnKind::Full {
13457                wq,
13458                wk,
13459                wv,
13460                wo,
13461                q_norm,
13462                k_norm,
13463                output_gate,
13464                softplus_gate,
13465                bias,
13466            } = &lw.attn
13467            else {
13468                return Out::Declined;
13469            };
13470            if *output_gate || softplus_gate.is_some() {
13471                return Out::Declined;
13472            }
13473            let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
13474                m.experts
13475                    .first()?
13476                    .gate_proj
13477                    .mapped_q4tp()
13478                    .map(|(m, _)| m.clone())
13479            }) else {
13480                return Out::Declined;
13481            };
13482            fn gw<'a>(
13483                t: &'a QTensor,
13484                owner: &std::sync::Arc<cortiq_core::CmfModel>,
13485            ) -> Option<crate::gpu::GraphW<'a>> {
13486                if let Some((m, idx, kind, rs)) = t.graph_weight() {
13487                    if m.uid() != owner.uid() || t.has_prism_contract() {
13488                        return None;
13489                    }
13490                    return Some(crate::gpu::GraphW {
13491                        idx,
13492                        kind,
13493                        row_scale: rs,
13494                        data: &[],
13495                        prism: crate::gpu::GraphPrismOp::None,
13496                        affine: false,
13497                    });
13498                }
13499                t.as_f32().map(|data| crate::gpu::GraphW {
13500                    idx: 0,
13501                    kind: 4,
13502                    row_scale: &[],
13503                    data,
13504                    prism: crate::gpu::GraphPrismOp::None,
13505                    affine: false,
13506                })
13507            }
13508            let (Some(q), Some(k), Some(v), Some(o)) = (
13509                gw(wq, &model),
13510                gw(wk, &model),
13511                gw(wv, &model),
13512                gw(wo, &model),
13513            ) else {
13514                return Out::Declined;
13515            };
13516            let layer = crate::gpu::GraphLayer {
13517                mhc: None,
13518                input_norm: &lw.input_norm,
13519                post_norm: &lw.post_norm,
13520                attn_out_norm: None,
13521                ffn_out_norm: None,
13522                layer_scale: None,
13523                ffn: crate::gpu::GraphFfn::AttentionOnly,
13524                attn: crate::gpu::GraphAttn::Full {
13525                    wq: q,
13526                    wk: k,
13527                    wv: v,
13528                    wo: o,
13529                    q_norm: q_norm.as_deref(),
13530                    k_norm: k_norm.as_deref(),
13531                    late_qk_norm: self.qk_norm_after_rope,
13532                    bias: bias
13533                        .as_ref()
13534                        .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
13535                    output_gate: false,
13536                    cpu_k: self.kv_cache.layers[li].k_heads(),
13537                    cpu_v: self.kv_cache.layers[li].v_heads(),
13538                    cpu_base: self.kv_cache.layers[li].base(),
13539                    geom: self.graph_attn_geom(li),
13540                    head_gate: None,
13541                },
13542            };
13543            let (nkv, hd, rd) = self.layer_geom(li);
13544            crate::gpu::forward_batch_graph_at(
13545                &model,
13546                self.graph_kv_id,
13547                li,
13548                &[layer],
13549                &self.inv_freq,
13550                h,
13551                self.layer_num_heads(li),
13552                nkv,
13553                hd,
13554                rd,
13555                self.hidden_size,
13556                1,
13557                positions,
13558                self.kv_cache.max_seq_len,
13559                self.norm_style == cortiq_core::NormStyle::Gemma,
13560                self.rms_eps as f32,
13561                self.attn_scale,
13562                b,
13563                &[],
13564                self.o1_epoch,
13565                None,
13566                None,
13567            )
13568        };
13569        match outcome {
13570            Out::Completed => {}
13571            Out::Declined => return Out::Declined,
13572            Out::Failed => {
13573                self.graph_failed
13574                    .store(true, std::sync::atomic::Ordering::Relaxed);
13575                return Out::Failed;
13576            }
13577        }
13578        let attn_ns = attn_started.elapsed().as_nanos() as u64;
13579        let hs = self.hidden_size;
13580        let lw = &self.weights.layers[li];
13581        let FfnKind::Moe(m) = &lw.ffn else {
13582            unreachable!()
13583        };
13584        let mut post = vec![0.0; h.len()];
13585        for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
13586            inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
13587        }
13588        let mut ffn = if b == 1 {
13589            moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
13590        } else {
13591            moe_ffn_banked_rows(
13592                &mut self.mimo_moe,
13593                li,
13594                m,
13595                &post,
13596                b,
13597                hs,
13598                self.pool.as_deref(),
13599            )
13600        };
13601        for (x, &f) in h.iter_mut().zip(&ffn) {
13602            *x += f;
13603        }
13604        attention::recycle_buf(&mut ffn);
13605        if self.layer_dump.is_some() {
13606            for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
13607                self.dump_layer_row(pos, li, row);
13608            }
13609        }
13610        crate::mimo_moe::note_attention_graph(b, attn_ns);
13611        Out::Completed
13612    }
13613
13614    fn layer_attn_plain(&self, li: usize) -> bool {
13615        self.kv_heads_per_layer.is_none()
13616            && self.v_head_dim.is_none()
13617            && self.global_attn.is_none()
13618            && self.layer_window(li).is_none()
13619            && self.kv_cache.layers[li].sinks.is_none()
13620    }
13621
13622    /// Forward one position through all layers (hybrid dispatch).
13623    fn forward_layers(
13624        &mut self,
13625        hidden: &[f32],
13626        position: usize,
13627        task_mask: Option<&TaskMask>,
13628    ) -> Vec<f32> {
13629        let out = self.forward_layers_upto(hidden, position, task_mask, None);
13630        self.o1_progress();
13631        self.swa_trim_tails();
13632        out
13633    }
13634
13635    // ── Network pipeline-split building blocks (coordinator/worker) ──
13636    // A remote worker owns layers [from ..= upto] and their KV; the
13637    // coordinator owns the rest plus embed / final norm / head. Attention
13638    // causality is per-layer, so a whole prompt's boundary hiddens ship
13639    // as one batch and decode ships one vector per token.
13640
13641    /// Embed one token id (embed multiplier applied).
13642    pub fn embed_id(&self, id: u32) -> Vec<f32> {
13643        self.embed_single(id)
13644    }
13645
13646    /// Refuse the archs/modes whose forward cannot be cut at a layer
13647    /// boundary. Loud by design: a split that silently changed the math
13648    /// would be a chimera.
13649    pub fn split_supported(&self) -> Result<(), String> {
13650        if self.dsv4.is_some() {
13651            return Err(
13652                "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
13653            );
13654        }
13655        if self.dsv41.is_some() {
13656            return Err(
13657                "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
13658                    .into(),
13659            );
13660        }
13661        if self.qwen4_exp.is_some() {
13662            return Err(
13663                "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
13664            );
13665        }
13666        if self.per_token_stack() {
13667            return Err(
13668                "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
13669            );
13670        }
13671        Ok(())
13672    }
13673
13674    /// Forward `hidden` through layers [from ..= upto] at `position`,
13675    /// appending those layers' KV/state. Both split sides call this
13676    /// over their own range; a task mask applies to the span's own
13677    /// layers (each side masks what it runs).
13678    pub fn forward_span(
13679        &mut self,
13680        hidden: &[f32],
13681        position: usize,
13682        from: usize,
13683        upto: usize,
13684        task_mask: Option<&TaskMask>,
13685    ) -> Result<Vec<f32>, String> {
13686        self.split_supported()?;
13687        if from > upto || upto >= self.num_layers {
13688            return Err(format!(
13689                "forward_span: layer range {from}..={upto} outside 0..{}",
13690                self.num_layers
13691            ));
13692        }
13693        if hidden.len() != self.hidden_size {
13694            return Err(format!(
13695                "forward_span: hidden len {} ≠ hidden_size {}",
13696                hidden.len(),
13697                self.hidden_size
13698            ));
13699        }
13700        let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
13701        self.o1_progress();
13702        self.swa_trim_tails();
13703        if self
13704            .graph_failed
13705            .swap(false, std::sync::atomic::Ordering::Relaxed)
13706        {
13707            self.cancel
13708                .store(false, std::sync::atomic::Ordering::Relaxed);
13709            self.clear_sequence_state();
13710            return Err("forward_span: deferred O(1) transition failed".into());
13711        }
13712        Ok(out)
13713    }
13714
13715    /// Final norm + lm_head over a boundary hidden (the final-logit
13716    /// softcap is applied by lm_head_forward itself).
13717    pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
13718        let normed = inference::rms_norm(
13719            hidden,
13720            &self.weights.final_norm,
13721            self.rms_eps,
13722            self.norm_style,
13723        );
13724        self.lm_head_forward(&normed)
13725    }
13726
13727    /// Sample the next token with this pipeline's sampler state.
13728    pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
13729        sampler::sample_with_scratch(
13730            logits,
13731            &self.sampler_config,
13732            past_tokens,
13733            &mut self.rng,
13734            &mut self.sampler_scratch,
13735        )
13736    }
13737
13738    /// Fresh sequence: clear KV, reuse history and device mirrors.
13739    pub fn reset_session(&mut self) {
13740        self.clear_sequence_state();
13741    }
13742
13743    /// Batched span prefill from token ids (coordinator side): embed +
13744    /// layers [0 ..= upto]; returns the boundary hiddens of ALL positions
13745    /// (ids.len() × hidden). Rides the same layer-major machinery as the
13746    /// local prefill; falls back to the per-position walk under
13747    /// CMF_PREFILL=seq.
13748    pub fn prefill_span_ids(
13749        &mut self,
13750        ids: &[u32],
13751        start_pos: usize,
13752        upto: usize,
13753        task_mask: Option<&TaskMask>,
13754    ) -> Result<Vec<f32>, String> {
13755        self.split_supported()?;
13756        if upto >= self.num_layers {
13757            return Err(format!(
13758                "prefill_span_ids: upto {upto} outside 0..{}",
13759                self.num_layers
13760            ));
13761        }
13762        // Same predicate as the whole-stack prefill: a span whose GDN
13763        // state lives on the device must walk positions through the
13764        // graph, not through the batched CPU span.
13765        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13766            let out =
13767                self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
13768            self.check_o1_progress_failure("prefill_span_ids")?;
13769            Ok(out)
13770        } else {
13771            let hs = self.hidden_size;
13772            let mut out = Vec::with_capacity(ids.len() * hs);
13773            for (i, &id) in ids.iter().enumerate() {
13774                let emb = self.embed_id(id);
13775                out.extend_from_slice(&self.forward_span(
13776                    &emb,
13777                    start_pos + i,
13778                    0,
13779                    upto,
13780                    task_mask,
13781                )?);
13782            }
13783            Ok(out)
13784        }
13785    }
13786
13787    /// Batched span prefill from boundary hiddens (worker side): layers
13788    /// [from ..= upto] for every position in the batch; returns the batch.
13789    pub fn prefill_span_hidden(
13790        &mut self,
13791        hidden: &[f32],
13792        start_pos: usize,
13793        from: usize,
13794        upto: usize,
13795        task_mask: Option<&TaskMask>,
13796    ) -> Result<Vec<f32>, String> {
13797        self.split_supported()?;
13798        let hs = self.hidden_size;
13799        if hidden.is_empty() || hidden.len() % hs != 0 {
13800            return Err(format!(
13801                "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
13802                hidden.len()
13803            ));
13804        }
13805        if from > upto || upto >= self.num_layers {
13806            return Err(format!(
13807                "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
13808                self.num_layers
13809            ));
13810        }
13811        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13812            let out = self.prefill_batch_span(
13813                PrefillIn::Hidden(hidden),
13814                start_pos,
13815                task_mask,
13816                from,
13817                upto + 1,
13818            );
13819            self.check_o1_progress_failure("prefill_span_hidden")?;
13820            Ok(out)
13821        } else {
13822            let b = hidden.len() / hs;
13823            let mut out = Vec::with_capacity(hidden.len());
13824            for i in 0..b {
13825                let h = self.forward_span(
13826                    &hidden[i * hs..(i + 1) * hs],
13827                    start_pos + i,
13828                    from,
13829                    upto,
13830                    task_mask,
13831                )?;
13832                out.extend_from_slice(&h);
13833            }
13834            Ok(out)
13835        }
13836    }
13837
13838    /// Build the whole-token wgpu graph for a pure-attention q1 model (every
13839    /// layer Full q1 + dense q1 FFN, no gate/bias). Returns the post-stack
13840    /// hidden (caller does final norm + lm_head), or None to fall back.
13841    fn try_token_graph_wgpu(
13842        &self,
13843        hidden: &[f32],
13844        position: usize,
13845        logits_out: &mut Vec<f32>,
13846        layers_run: &mut usize,
13847    ) -> Option<Result<Vec<f32>, ()>> {
13848        self.try_token_graph_wgpu_steps(
13849            hidden,
13850            position,
13851            logits_out,
13852            1,
13853            None,
13854            Some(layers_run),
13855            0,
13856            self.num_layers,
13857        )
13858    }
13859
13860    /// The span twin (network split): the graph covers [from..upto_excl)
13861    /// — one submit per SEGMENT per token. lm_head folds in only when
13862    /// the span reaches the last layer.
13863    fn try_token_graph_wgpu_span(
13864        &self,
13865        hidden: &[f32],
13866        position: usize,
13867        logits_out: &mut Vec<f32>,
13868        from: usize,
13869        upto_excl: usize,
13870        layers_run: &mut usize,
13871    ) -> Option<Result<Vec<f32>, ()>> {
13872        self.try_token_graph_wgpu_steps(
13873            hidden,
13874            position,
13875            logits_out,
13876            1,
13877            None,
13878            Some(layers_run),
13879            from,
13880            upto_excl,
13881        )
13882    }
13883
13884    /// The pooled tail's device lm_head: final-norm + head as one
13885    /// layer-less graph pass, so decode stops paying the double-digit
13886    /// millisecond host matvec every token. `None` keeps the host head.
13887    #[cfg(feature = "gpu")]
13888    fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
13889        if !crate::minicpm_tail::minimax_pool_on()
13890            || !crate::gpu::enabled_here()
13891            || self.head_clusters.is_some()
13892            || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
13893            || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
13894        {
13895            return None;
13896        }
13897        let (model, i, kind, rs) = self
13898            .weights
13899            .lm_head
13900            .graph_weight()
13901            .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
13902        let name = &model.tensors[i].name;
13903        let prism = if crate::prism::is_inverse_embedding(&model, name) {
13904            crate::gpu::GraphPrismOp::InverseEmbedding
13905        } else if crate::prism::is_forward_weight(&model, name) {
13906            crate::gpu::GraphPrismOp::Forward
13907        } else {
13908            crate::gpu::GraphPrismOp::None
13909        };
13910        let gw = crate::gpu::GraphW {
13911            idx: i,
13912            kind,
13913            row_scale: rs,
13914            data: &[],
13915            prism,
13916            affine: crate::prism::is_affine_target(&model, name),
13917        };
13918        let rows = self.weights.lm_head.rows();
13919        let mut h = hidden.to_vec();
13920        let mut lg = Vec::new();
13921        let mut layers_run = 0usize;
13922        let outcome = crate::gpu::forward_token_graph(
13923            &model,
13924            self.graph_kv_id,
13925            &[],
13926            &[],
13927            self.o1_epoch,
13928            &self.inv_freq,
13929            &mut h,
13930            self.num_heads,
13931            self.num_kv_heads,
13932            self.head_dim,
13933            self.attn_scale,
13934            self.head_dim,
13935            self.hidden_size,
13936            self.intermediate_size,
13937            0,
13938            self.kv_cache.max_seq_len,
13939            self.norm_style == cortiq_core::NormStyle::Gemma,
13940            self.rms_eps as f32,
13941            Some((&gw, rows)),
13942            &self.weights.final_norm,
13943            &mut lg,
13944            &[],
13945            1,
13946            None,
13947            None,
13948            Some(&mut layers_run),
13949            0,
13950            false,
13951        );
13952        if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
13953            return None;
13954        }
13955        // The same post-steps the host head applies — keep sampling
13956        // identical whichever arm produced the logits.
13957        lg.resize(self.vocab_size, 0.0);
13958        if let Some(m) = self.logit_multiplier {
13959            for l in lg.iter_mut() {
13960                *l *= m;
13961            }
13962        }
13963        if let Some(c) = self.final_softcap {
13964            softcap_in_place(&mut lg, c, self.pool.as_deref());
13965        }
13966        Some(lg)
13967    }
13968
13969    /// Record one token-layer's winners into the route-frequency profile and
13970    /// write it out every 16 tokens when `CMF_M3_PROFILE_SAVE=<path>` is set.
13971    fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
13972        if winners.is_empty() {
13973            return;
13974        }
13975        if self.m3_n_exp == 0 {
13976            self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
13977            self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
13978        }
13979        for &e in winners {
13980            let e = e as usize;
13981            if e < self.m3_n_exp && li < self.num_layers {
13982                self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
13983            }
13984        }
13985        if li + 1 == self.num_layers {
13986            self.m3_tokens += 1;
13987            if self.m3_tokens % 16 == 0 {
13988                if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
13989                    let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
13990                    let _ = std::fs::write(&path, bytes);
13991                }
13992            }
13993        }
13994    }
13995
13996    /// Admit the most frequent (layer, expert) pairs of a saved profile into
13997    /// the bank before the first token: `CMF_M3_PROFILE=<path>` names the
13998    /// file, `CMF_M3_PREWARM_N` (default 300) bounds how many experts go in.
13999    /// The bank's own eviction stays in charge; prewarm only chooses order.
14000    fn m3_prewarm(&mut self) {
14001        let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
14002            return;
14003        };
14004        let Ok(bytes) = std::fs::read(&path) else {
14005            tracing::warn!("m3 prewarm: cannot read profile {path}");
14006            return;
14007        };
14008        let counts: Vec<u32> = bytes
14009            .chunks_exact(4)
14010            .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
14011            .collect();
14012        let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
14013        if counts.len() != self.num_layers * n_exp {
14014            tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
14015            return;
14016        }
14017        let limit: usize = std::env::var("CMF_M3_PREWARM_N")
14018            .ok()
14019            .and_then(|v| v.parse().ok())
14020            .unwrap_or(300);
14021        let mut ranked: Vec<(u32, usize, usize)> = counts
14022            .iter()
14023            .enumerate()
14024            .filter(|(_, c)| **c > 0)
14025            .map(|(i, &c)| (c, i / n_exp, i % n_exp))
14026            .collect();
14027        ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
14028        ranked.truncate(limit);
14029        let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
14030        for (_, li, e) in ranked {
14031            per_layer[li].push(e);
14032        }
14033        let mut admitted = 0usize;
14034        for (li, ids) in per_layer.iter().enumerate() {
14035            if ids.is_empty() {
14036                continue;
14037            }
14038            let lw = &self.weights.layers[self.phys_layer(li)];
14039            let FfnKind::Moe(m) = &lw.ffn else {
14040                continue;
14041            };
14042            // Arming installs the layer's layout; nothing predicted yet.
14043            let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
14044            if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
14045                admitted += ids.len();
14046            }
14047        }
14048        tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
14049    }
14050
14051    /// One MiniMax tail layer through the pooled token-graph arm: attention,
14052    /// the pre-FFN norm, device routing and the descriptor-indexed expert
14053    /// bank in ONE submit (one sync instead of host attention + a MoE
14054    /// frame), with cold experts finished on the host against the read-back
14055    /// FFN input and admitted for the next token.
14056    ///
14057    /// `Ok(true)` — the layer is done (hidden replaced). `Ok(false)` —
14058    /// decline before any device mutation; the ordinary host path owns this
14059    /// position. `Err(())` — the device failed after admission; the caller
14060    /// must stop the sequence exactly like a failed span run.
14061    #[cfg(feature = "gpu")]
14062    fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
14063        if self.o1_active() || self.attn_softcap > 0.0 {
14064            return Ok(false);
14065        }
14066        if !self.m3_prewarmed {
14067            self.m3_prewarmed = true;
14068            self.m3_prewarm();
14069        }
14070        let frame = {
14071            let lw = &self.weights.layers[self.phys_layer(li)];
14072            let FfnKind::Moe(m) = &lw.ffn else {
14073                return Ok(false);
14074            };
14075            if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
14076                return Ok(false);
14077            }
14078            let t_arm = std::time::Instant::now();
14079            if self.m3_winners.len() != self.num_layers {
14080                self.m3_winners = vec![Vec::new(); self.num_layers];
14081            }
14082            let pred = std::mem::take(&mut self.m3_winners[li]);
14083            let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
14084            if armed.is_none() {
14085                self.m3_winners[li] = pred; // declined before anything ran
14086            }
14087            M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14088            match armed {
14089                Some(frame) => frame,
14090                // First-call refusal (geometry/bank): the host layer still
14091                // runs; nothing was mutated.
14092                None => return Ok(false),
14093            }
14094        };
14095        crate::gpu_wgpu::pooled_frame_enter(frame);
14096        // The pooled call must not fold lm_head: its cold experts have to
14097        // land before any final norm. The decode loop re-arms this flag for
14098        // the next position anyway.
14099        let want_logits = self.graph_want_logits;
14100        self.graph_want_logits = false;
14101        let mut lg = Vec::new();
14102        let mut gl = 0usize;
14103        let t_span = std::time::Instant::now();
14104        let span = crate::gpu::gpu_scope(|| {
14105            self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
14106        });
14107        self.graph_want_logits = want_logits;
14108        M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14109        let hh = match span {
14110            Some(Ok(hh)) => hh,
14111            Some(Err(())) => return Err(()),
14112            None => {
14113                // Declined before submission (seed/resolve/refusal): the
14114                // device mirror has not moved, so the host path is exact.
14115                return Ok(false);
14116            }
14117        };
14118        if gl != 1 {
14119            // A single-layer run that reports no layer mutated nothing we
14120            // may build on; anything else is a contract break.
14121            return if gl == 0 { Ok(false) } else { Err(()) };
14122        }
14123        *h = hh;
14124        let winners = crate::gpu_wgpu::pooled_frame_take_winners();
14125        self.m3_note_route(li, &winners);
14126        if self.m3_winners.len() != self.num_layers {
14127            self.m3_winners = vec![Vec::new(); self.num_layers];
14128        }
14129        self.m3_winners[li] = winners;
14130        let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
14131        // A non-finite parsed weight would poison the residual for the rest
14132        // of the sequence — drop such entries loudly instead.
14133        cold.retain(|&(_, w)| {
14134            if w.is_finite() {
14135                true
14136            } else {
14137                tracing::error!("pooled tail: non-finite cold weight from expert route");
14138                false
14139            }
14140        });
14141        if cold.is_empty() {
14142            return Ok(true);
14143        }
14144        let x = crate::gpu_wgpu::pooled_frame_take_input();
14145        if x.len() != self.hidden_size {
14146            return Err(());
14147        }
14148        // Cold experts: on the device first (admit, then one prepared frame
14149        // over exactly those slots — one submit for the layer). The host
14150        // computes them only when the device declines, and then they are
14151        // admitted for the next token. `CMF_M3_COLD_CPU=1` forces the host
14152        // arm for an A/B measurement.
14153        let lw = &self.weights.layers[self.phys_layer(li)];
14154        let FfnKind::Moe(m) = &lw.ffn else {
14155            return Err(());
14156        };
14157        let t_cold = std::time::Instant::now();
14158        let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
14159        let mut out = crate::attention::take_buf(self.hidden_size);
14160        let on_device = !force_host
14161            && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
14162        if !on_device {
14163            crate::attention::recycle_buf(&mut out);
14164            let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
14165                .iter()
14166                .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
14167                .collect::<Result<_, ()>>()?;
14168            out = crate::gpu::cpu_scope(|| {
14169                crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
14170            });
14171            let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
14172            let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
14173        }
14174        M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14175        M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
14176        if out.len() != h.len() {
14177            crate::attention::recycle_buf(&mut out);
14178            return Err(());
14179        }
14180        for (dst, src) in h.iter_mut().zip(out.iter()) {
14181            *dst += src;
14182        }
14183        crate::attention::recycle_buf(&mut out);
14184        if on_device {
14185            M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
14186        }
14187        Ok(true)
14188    }
14189
14190    #[cfg(not(feature = "gpu"))]
14191    fn m3_pooled_layer(
14192        &mut self,
14193        _li: usize,
14194        _position: usize,
14195        _h: &mut Vec<f32>,
14196    ) -> Result<bool, ()> {
14197        Ok(false)
14198    }
14199
14200    /// Greedy burst: forward `t_next` and let the device pick + re-embed
14201    /// the next k−1 tokens — k frames, ONE submit, k ids back. The ZML
14202    /// trade, on wgpu. None ⇒ caller keeps the per-token path.
14203    fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
14204        if self.o1_active() || self.attn_softcap > 0.0 {
14205            return None;
14206        }
14207        // The burst builds the whole-token graph; attention the graph's
14208        // per-layer geometry cannot express keeps the per-token path.
14209        if let Some(reason) = self.wgpu_graph_attn_decline() {
14210            self.note_graph_decline("wgpu multi-burst", reason);
14211            return None;
14212        }
14213        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
14214        if !graph_on || self.graph_refused() {
14215            // Same memo as the decode site: this path builds the very
14216            // same graph, so a model it cannot build for must not be
14217            // walked again here either. Missing this guard was worth
14218            // 2.5x on an Adreno — 0.361 tok/s against 0.905 — because
14219            // the burst retried per token what decode had already given
14220            // up on.
14221            return None;
14222        }
14223        let emb = self.embed_single(t_next);
14224        let mut lg = Vec::new();
14225        let mut ids = Vec::new();
14226        match self.try_token_graph_wgpu_steps(
14227            &emb,
14228            position,
14229            &mut lg,
14230            k,
14231            Some(&mut ids),
14232            None,
14233            0,
14234            self.num_layers,
14235        ) {
14236            Some(Ok(_)) => {}
14237            Some(Err(())) => {
14238                // Preserve the backend's post-admission failure through the
14239                // Option-based burst API.  The decode caller consumes this
14240                // flag and clears the sequence instead of falling through
14241                // to a stale CPU recurrent state.
14242                self.graph_failed
14243                    .store(true, std::sync::atomic::Ordering::Relaxed);
14244                return None;
14245            }
14246            None => return None,
14247        }
14248        (ids.len() == k).then_some(ids)
14249    }
14250
14251    /// Multi-step greedy: k whole frames in ONE submit, argmax and re-embed
14252    /// on the device. `ids_out` receives the k winner ids; the hidden/logits
14253    /// outputs are NOT produced in that mode.
14254    fn try_token_graph_wgpu_steps(
14255        &self,
14256        hidden: &[f32],
14257        position: usize,
14258        logits_out: &mut Vec<f32>,
14259        steps: usize,
14260        ids_out: Option<&mut Vec<u32>>,
14261        layers_run: Option<&mut usize>,
14262        from: usize,
14263        upto_excl: usize,
14264    ) -> Option<Result<Vec<f32>, ()>> {
14265        // SenseNova-Loop: MRoPE prompt rows rotate on the host; the graph's
14266        // mHC kernels take two streams only, and a graph must not enter an
14267        // mHC range midway (its streams would be the host's).
14268        if self.prompt_mrope.is_some()
14269            || self.mhc.as_deref().is_some_and(|m| {
14270                from < self.num_layers && m.on(self.phys_layer(from)) && !m.enters(self.phys_layer(from))
14271            })
14272            || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
14273            || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
14274                && (from..upto_excl.min(self.num_layers)).any(|li| self.mhc_layer(li))
14275        {
14276            return None;
14277        }
14278        // The bank has already reserved its VRAM. Never build a second
14279        // all-expert arena across bank-owned layers (including bursts).
14280        let upto_excl = match self.mimo_moe.graph_prefix_end() {
14281            Some(end) if end < upto_excl => {
14282                if steps != 1 || layers_run.is_none() || from >= end {
14283                    return None;
14284                }
14285                end
14286            }
14287            _ => upto_excl,
14288        };
14289        // O(1) Nyström decode runs off the sealed state, not the KV cache the
14290        // graph mirrors — never take the graph while o1 is active.
14291        let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
14292        if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
14293            // Softcapped scores have no graph kernel yet — CPU owns them.
14294            // o1 rides the graph only behind CMF_O1_GPU=1 while the port
14295            // proves itself; without it the CPU path owns o1 as before.
14296            return None;
14297        }
14298        // Per-layer KV heads, narrow V, sinks and sliding windows ride
14299        // `GraphAttn::Full::geom` (the ATTEND_X kernels). Anything that
14300        // geometry cannot express declines here, by name — before the
14301        // per-layer gate existed a sliding/sink model ran the graph as
14302        // full-context attention, fluent and wrong. The caller memoizes
14303        // the refusal.
14304        if let Some(reason) = self.wgpu_graph_attn_decline() {
14305            self.note_graph_decline("wgpu token graph", reason);
14306            return None;
14307        }
14308        // Per-layer sealed o1 state for the graph. During prefill the
14309        // state is still Collecting -> views are None -> the graph
14310        // refuses below and the CPU prefill records the q trace and
14311        // seals, exactly as the o1 design requires.
14312        let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
14313            .map(|li| {
14314                if !o1_gpu {
14315                    return None;
14316                }
14317                self.kv_cache.layers[self.phys_layer(li)].o1_views()
14318            })
14319            .collect();
14320        if self.o1_active() && o1_gpu {
14321            // Any o1 layer not sealed (or degenerate exact-only) keeps the
14322            // whole token on the CPU: half-graph forwards would desync.
14323            let want: usize = (from..upto_excl)
14324                .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
14325                .count();
14326            let have = o1_views.iter().filter(|v| v.is_some()).count();
14327            if want == 0 || have != want {
14328                // The silent twin of the gpu-side o1 gates, found the
14329                // same way: a 15x decode drop with an empty log. Views
14330                // stay None until the layer's state SEALS, so `have`
14331                // lagging `want` early in a run is the o1 design working
14332                // — but it must say so, or the next reader spends a
14333                // night proving the kernels innocent.
14334                // On CHANGE, not once: the first decline is the legal
14335                // unsealed prefill, and a once-print buries the state
14336                // that matters — what the count reads AFTER the seal.
14337                use std::sync::atomic::{AtomicUsize, Ordering};
14338                static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
14339                let code = have * 1000 + want;
14340                if LAST.swap(code, Ordering::Relaxed) != code {
14341                    tracing::warn!(
14342                        "o1 graph: {have} of {want} layers sealed — per-op until all seal"
14343                    );
14344                }
14345                return None;
14346            }
14347        }
14348        let nh = self.num_heads;
14349        let (nkv, hd, rd) = self.layer_geom(0);
14350        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14351        let mut layers = Vec::with_capacity(upto_excl - from);
14352        let mut model = None;
14353        let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
14354        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14355            // `blr` rides as GRAPH_KIND_BLR (two stage dispatches).
14356            if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
14357                let name = &m.tensors[i].name;
14358                let prism = if crate::prism::is_inverse_embedding(m, name) {
14359                    crate::gpu::GraphPrismOp::InverseEmbedding
14360                } else if crate::prism::is_forward_weight(m, name) {
14361                    crate::gpu::GraphPrismOp::Forward
14362                } else {
14363                    crate::gpu::GraphPrismOp::None
14364                };
14365                return Some(crate::gpu::GraphW {
14366                    idx: i,
14367                    kind,
14368                    row_scale: rs,
14369                    data: &[],
14370                    prism,
14371                    affine: crate::prism::is_affine_target(m, name),
14372                });
14373            }
14374            // Small unquantized projections (GDN in_proj_a/b) stay f32.
14375            match t.as_f32() {
14376                Some(d) => Some(crate::gpu::GraphW {
14377                    idx: 0,
14378                    kind: 4,
14379                    row_scale: &[],
14380                    data: d,
14381                    prism: crate::gpu::GraphPrismOp::None,
14382                    affine: false,
14383                }),
14384                None => {
14385                    if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14386                        eprintln!("batch graph: weight has no graph/f32 representation");
14387                    }
14388                    None
14389                }
14390            }
14391        }
14392        for li in from..upto_excl {
14393            let lw = &self.weights.layers[self.phys_layer(li)];
14394            if dbg {
14395                let ak = match &lw.attn {
14396                    AttnKind::Mla(_) => "Mla".into(),
14397                    AttnKind::Full {
14398                        output_gate, bias, ..
14399                    } => format!("Full gate={output_gate} bias={}", bias.is_some()),
14400                    AttnKind::LinearGdn(_) => "LinearGdn".into(),
14401                    AttnKind::Kda(_) => "Kda".into(),
14402                    AttnKind::Linear(_) => "Linear".into(),
14403                    AttnKind::ShortConv(_) => "ShortConv".into(),
14404                    AttnKind::Bounded(_) => "Bounded".into(),
14405                };
14406                let fk = match &lw.ffn {
14407                    FfnKind::Dense(_) => "Dense",
14408                    FfnKind::Moe(_) => "Moe",
14409                    FfnKind::DenseMoe(_) => "DenseMoe",
14410                };
14411                eprintln!("graph L{li}: attn={ak} ffn={fk}");
14412            }
14413            let gffn = match &lw.ffn {
14414                FfnKind::DenseMoe(_) => return None, // dual branch: CPU path
14415                // A tube layer is several matrices, not one — the
14416                // whole-layer graph has no shape for it yet.
14417                FfnKind::Dense(d) if !d.segs.is_empty() => return None,
14418                FfnKind::Dense(d) => {
14419                    // An activation the graph has no kernel arm for keeps
14420                    // the CPU/per-op owner, by name — the dense graph FFN
14421                    // used to compute SiLU for whatever the model asked.
14422                    let Some(act) = d.act.wgpu_graph_act() else {
14423                        self.note_graph_decline(
14424                            "wgpu token graph",
14425                            "dense FFN activation without a graph kernel",
14426                        );
14427                        return None;
14428                    };
14429                    crate::gpu::GraphFfn::Dense {
14430                        gate: gw(&d.gate_proj)?,
14431                        up: gw(&d.up_proj)?,
14432                        down: gw(&d.down_proj)?,
14433                        act,
14434                    }
14435                }
14436                FfnKind::Moe(m) => {
14437                    // Adaptive τ and expert masks keep the CPU path, where
14438                    // they are implemented. Sigmoid routing with a selection
14439                    // bias (LFM2-MoE / DeepSeek noaux_tc), a routed scale ≠ 1
14440                    // and an UNGATED shared expert (HunYuan hy_v3: ×2.826 on
14441                    // the routed mix, the shared expert at weight 1) are all
14442                    // graphed — before, every such token fell to the per-op
14443                    // path whole (145 submits/token on Hy-MT2-30B-A3B).
14444                    if m.route_tau.is_some() || m.mask.is_some() {
14445                        return None;
14446                    }
14447                    let shared = m.shared.as_ref();
14448                    // The resident MoE kernels do not yet carry the
14449                    // descriptor-aware transform through router/shared-gate
14450                    // selection.  Refuse the complete layer instead of
14451                    // scoring with an untransformed Prism plane (the dense
14452                    // path has an explicit FWHT boundary below).
14453                    let inter = m.experts.first()?.gate_proj.rows();
14454                    // Flint's shared expert is a 1024-wide SwiGLU while its
14455                    // routed bank is 512-wide.  The resident MoE shader
14456                    // indexes a uniform-width packed bank, so keep that
14457                    // expert out of the routed slots and add it through the
14458                    // dense q4tp path after routed accumulation.  This is
14459                    // also the safe route for a mixed q2-routed/q4-shared
14460                    // artifact: neither layout nor row stride is faked.
14461                    let detached_shared = match shared {
14462                        Some((se, None))
14463                            if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14464                        {
14465                            if !matches!(se.act, Act::Silu)
14466                                || se.gate_proj.mapped_q4tp().is_none()
14467                                || se.up_proj.mapped_q4tp().is_none()
14468                                || se.down_proj.mapped_q4tp().is_none()
14469                            {
14470                                return None;
14471                            }
14472                            Some(crate::gpu::GraphMoeDetachedShared {
14473                                gate: gw(&se.gate_proj)?,
14474                                up: gw(&se.up_proj)?,
14475                                down: gw(&se.down_proj)?,
14476                                width: se.gate_proj.rows(),
14477                            })
14478                        }
14479                        // A separately shaped gated shared expert needs a
14480                        // second sigmoid gate path.  Do not turn it into an
14481                        // ungated one merely to make the graph run.
14482                        Some((se, Some(_)))
14483                            if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14484                        {
14485                            return None;
14486                        }
14487                        _ => None,
14488                    };
14489                    let has_shared = shared.is_some() && detached_shared.is_none();
14490                    let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
14491                    let sgate = match shared {
14492                        Some((_, Some(sg))) if has_shared => gw(sg)?,
14493                        // No gate, no shared expert, or a detached shared
14494                        // expert: the router only keeps the binding total;
14495                        // the selection shader does not read it.
14496                        _ => gw(&m.router)?,
14497                    };
14498                    let router = gw(&m.router)?;
14499                    // The resident MoE kernels do not yet carry the
14500                    // descriptor-aware transform through router/shared-gate
14501                    // selection.  Refuse the complete layer instead of
14502                    // scoring with an untransformed Prism plane (the dense
14503                    // path has an explicit FWHT boundary below).
14504                    if router.prism != crate::gpu::GraphPrismOp::None
14505                        || sgate.prism != crate::gpu::GraphPrismOp::None
14506                        || router.affine
14507                        || sgate.affine
14508                    {
14509                        tracing::warn!(
14510                            "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
14511                        );
14512                        return None;
14513                    }
14514                    let in_bank_shared = shared.filter(|_| has_shared).map(|(se, _)| se);
14515                    let edge = |t: &QTensor| {
14516                        t.graph_weight()
14517                            .or_else(|| t.graph_weight_descriptor())
14518                            .map(|(mm, i, _, _)| (std::sync::Arc::as_ptr(mm) as usize, i))
14519                    };
14520                    let bank_key: Option<GraphMoeKey> = match (
14521                        m.experts.first().and_then(|e| edge(&e.gate_proj)),
14522                        m.experts.last().and_then(|e| edge(&e.down_proj)),
14523                    ) {
14524                        (Some((fm, fi)), Some((_, di))) if graph_moe_bank_on() => Some((
14525                            self.phys_layer(li),
14526                            m.experts.as_ptr() as usize,
14527                            m.experts.len(),
14528                            in_bank_shared.map_or(0, |se| std::ptr::from_ref(se) as usize),
14529                            fm,
14530                            fi,
14531                            di,
14532                        )),
14533                        _ => None,
14534                    };
14535                    let cached =
14536                        bank_key.and_then(|k| self.graph_moe_banks.lock().unwrap().get(&k).cloned());
14537                    let bank = if let Some(b) = cached {
14538                        b
14539                    } else {
14540                    let mut experts = Vec::with_capacity(m.experts.len() + 1);
14541                    let mut bank_model: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14542                    // q4t or q4tp, but not both in one layer — the kernels
14543                    // are picked per layer, not per expert.
14544                    let mut q4tp: Option<bool> = None;
14545                    // The mixed 2-bit profile: q2tp gate/up over a q4tp
14546                    // down. Uniform across the layer, like `q4tp` itself.
14547                    let mut gu_q2: Option<bool> = None;
14548                    // q8_2f experts (gate, up and down): their own kernels.
14549                    let mut q82: Option<bool> = None;
14550                    // One MoE gate/up kernel serves the complete packed bank.
14551                    // Bind an activation only when every routed (and in-bank
14552                    // shared) expert owns the same exact WGPU contract.
14553                    let mut moe_act = None;
14554                    for e in m.experts.iter().chain(in_bank_shared.into_iter()) {
14555                        let Some(e_act) = e.act.wgpu_graph_act() else {
14556                            return None;
14557                        };
14558                        match moe_act {
14559                            Some(existing) if existing != e_act => return None,
14560                            None => moe_act = Some(e_act),
14561                            _ => {}
14562                        }
14563                        if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
14564                            return None;
14565                        }
14566                        // Expert tensors are packed into one resident buffer
14567                        // and the MoE kernels have no transform slot per
14568                        // expert.  Keep the CPU/per-op owner for Prism or
14569                        // affine experts rather than silently using raw bytes.
14570                        for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
14571                            let Some((em, ei, _, _)) = expert_weight
14572                                .graph_weight()
14573                                .or_else(|| expert_weight.graph_weight_descriptor())
14574                            else {
14575                                return None;
14576                            };
14577                            let name = &em.tensors[ei].name;
14578                            if crate::prism::is_forward_weight(em, name)
14579                                || crate::prism::is_inverse_embedding(em, name)
14580                                || crate::prism::is_affine_target(em, name)
14581                            {
14582                                tracing::warn!(
14583                                    "resident MoE declined: expert Prism/affine transform is not implemented"
14584                                );
14585                                return None;
14586                            }
14587                        }
14588                        let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
14589                            Some((mm, gi)) => (
14590                                mm,
14591                                gi,
14592                                e.up_proj.mapped_q4t()?.1,
14593                                e.down_proj.mapped_q4t()?.1,
14594                                false,
14595                                false,
14596                                false,
14597                            ),
14598                            None => match e.gate_proj.mapped_q2tp() {
14599                                Some((mm, gi)) => (
14600                                    mm,
14601                                    gi,
14602                                    e.up_proj.mapped_q2tp()?.1,
14603                                    e.down_proj.mapped_q4tp()?.1,
14604                                    true,
14605                                    true,
14606                                    false,
14607                                ),
14608                                None => match e.gate_proj.mapped_q4tp() {
14609                                    Some((mm, gi)) => (
14610                                        mm,
14611                                        gi,
14612                                        e.up_proj.mapped_q4tp()?.1,
14613                                        e.down_proj.mapped_q4tp()?.1,
14614                                        true,
14615                                        false,
14616                                        false,
14617                                    ),
14618                                    // q8_2f experts: all three projections,
14619                                    // each with its own column field.
14620                                    None => {
14621                                        if !crate::gpu::moe_q82_graph_on() {
14622                                            return None;
14623                                        }
14624                                        let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14625                                        (
14626                                            mm,
14627                                            gi,
14628                                            e.up_proj.mapped_q8_2f()?.1,
14629                                            e.down_proj.mapped_q8_2f()?.1,
14630                                            false,
14631                                            false,
14632                                            true,
14633                                        )
14634                                    }
14635                                },
14636                            },
14637                        };
14638                        if *q4tp.get_or_insert(is_p) != is_p
14639                            || *gu_q2.get_or_insert(is_q2) != is_q2
14640                            || *q82.get_or_insert(is_8) != is_8
14641                        {
14642                            // The shared expert rides in the same packed
14643                            // buffer as the routed ones, so a layer that
14644                            // mixes layouts cannot be indexed by one stride.
14645                            // Say so: the symptom is a whole model quietly
14646                            // running its MoE on the CPU.
14647                            tracing::warn!(
14648                                "MoE layer mixes expert layouts (q4tp={is_p}, q2tp gate/up={is_q2}, q8_2f={is_8})                                  — every expert of a layer, INCLUDING the shared one, must share                                  a layout. The whole-token graph declines this layer."
14649                            );
14650                            return None;
14651                        }
14652                        bank_model.get_or_insert_with(|| mm.clone());
14653                        experts.push((gi, ui, di));
14654                    }
14655                    let b = GraphMoeBank {
14656                        experts,
14657                        q4tp,
14658                        gu_q2,
14659                        q82,
14660                        act: moe_act,
14661                        model: bank_model,
14662                    };
14663                    if let Some(k) = bank_key {
14664                        self.graph_moe_banks.lock().unwrap().insert(k, b.clone());
14665                    }
14666                    b
14667                    };
14668                    if let Some(mm) = &bank.model {
14669                        model.get_or_insert_with(|| mm.clone());
14670                    }
14671                    crate::gpu::GraphFfn::Moe {
14672                        router,
14673                        shared_gate: sgate,
14674                        experts: bank.experts,
14675                        n_exp: m.experts.len(),
14676                        // CMF_TOPK_PROBE: timing probe only — output is WRONG.
14677                        // Fewer experts shrink the MoE arithmetic while the
14678                        // dispatch count stays identical, which is the only
14679                        // clean way to tell a launch-bound decode from a
14680                        // compute-bound one.
14681                        top_k: std::env::var("CMF_TOPK_PROBE")
14682                            .ok()
14683                            .and_then(|v| v.parse::<usize>().ok())
14684                            .filter(|k| *k > 0 && *k <= m.top_k)
14685                            .unwrap_or(m.top_k),
14686                        inter,
14687                        act: bank.act?,
14688                        norm_topk: m.norm_topk_prob,
14689                        q4tp: bank.q4tp?,
14690                        gu_q2: bank.gu_q2.unwrap_or(false),
14691                        q82: bank.q82.unwrap_or(false),
14692                        sigmoid: m.router_sigmoid,
14693                        sigmoid_denom_eps: m.sigmoid_denom_eps,
14694                        bias: m.expert_bias.as_deref(),
14695                        has_shared,
14696                        detached_shared,
14697                        shared_gated,
14698                        route_scale: m.routed_scaling,
14699                    }
14700                }
14701            };
14702            let attn = match &lw.attn {
14703                AttnKind::Full {
14704                    wq,
14705                    wk,
14706                    wv,
14707                    wo,
14708                    q_norm,
14709                    k_norm,
14710                    output_gate,
14711                    softplus_gate,
14712                    bias,
14713                } => {
14714                    if self.attention_heads_per_layer.is_some() {
14715                        return None;
14716                    }
14717                    // A projected output gate rides the graph in one form:
14718                    // Spark-X2.5's head-wise sigmoid (one g_proj row per Q
14719                    // head). Laguna's softplus gate keeps the CPU path.
14720                    let head_gate = match softplus_gate {
14721                        None => None,
14722                        Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
14723                        Some(_) => {
14724                            self.note_graph_decline(
14725                                "wgpu token graph",
14726                                "projected softplus / per-element output gate",
14727                            );
14728                            return None;
14729                        }
14730                    };
14731                    let (m, _, _, _) = wq.graph_weight_or_blr()?;
14732                    model = Some(m.clone());
14733                    crate::gpu::GraphAttn::Full {
14734                        wq: gw(wq)?,
14735                        wk: gw(wk)?,
14736                        wv: gw(wv)?,
14737                        wo: gw(wo)?,
14738                        q_norm: q_norm.as_deref(),
14739                        k_norm: k_norm.as_deref(),
14740                        late_qk_norm: self.qk_norm_after_rope,
14741                        bias: bias
14742                            .as_ref()
14743                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14744                        output_gate: *output_gate,
14745                        cpu_k: self.kv_cache.layers[li].k_heads(),
14746                        cpu_v: self.kv_cache.layers[li].v_heads(),
14747                        cpu_base: self.kv_cache.layers[li].base(),
14748                        geom: self.graph_attn_geom(li),
14749                        head_gate,
14750                    }
14751                }
14752                AttnKind::LinearGdn(w) => {
14753                    let cfg = self.gdn_cfg?;
14754                    let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
14755                    model = Some(m.clone());
14756                    crate::gpu::GraphAttn::Gdn {
14757                        qkv: gw(&w.in_proj_qkv)?,
14758                        z: gw(&w.in_proj_z)?,
14759                        a: gw(&w.in_proj_a)?,
14760                        b: gw(&w.in_proj_b)?,
14761                        out: gw(&w.out_proj)?,
14762                        conv1d: &w.conv1d,
14763                        a_log: &w.a_log,
14764                        dt_bias: &w.dt_bias,
14765                        norm: &w.norm,
14766                        nv: cfg.num_v_heads,
14767                        nk: cfg.num_k_heads,
14768                        dk: cfg.key_head_dim,
14769                        dv: cfg.value_head_dim,
14770                        kk: cfg.conv_kernel,
14771                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14772                        preview: None,
14773                    }
14774                }
14775                AttnKind::ShortConv(w) => {
14776                    let cfg = self.short_conv_cfg?;
14777                    let (m, _, _, _) = w
14778                        .in_proj
14779                        .graph_weight()
14780                        .or_else(|| w.in_proj.graph_weight_descriptor())?;
14781                    model = Some(m.clone());
14782                    crate::gpu::GraphAttn::ShortConv {
14783                        inp: gw(&w.in_proj)?,
14784                        out: gw(&w.out_proj)?,
14785                        taps: &w.conv,
14786                        kernel: cfg.kernel,
14787                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14788                    }
14789                }
14790                _ => return None,
14791            };
14792            layers.push(crate::gpu::GraphLayer {
14793                mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
14794                input_norm: &lw.input_norm,
14795                attn,
14796                post_norm: &lw.post_norm,
14797                attn_out_norm: lw.attn_out_norm.as_deref(),
14798                ffn_out_norm: lw.ffn_out_norm.as_deref(),
14799                layer_scale: lw.layer_scale,
14800                ffn: gffn,
14801            });
14802        }
14803        let model = model?;
14804        // Fold final-norm + lm_head into the graph when this call wants logits
14805        // and the lm_head is a graphable (quantized) weight — the graph then
14806        // reads back logits (into logits_out) instead of the hidden, dropping
14807        // the separate CPU/GPU lm_head op + its sync. Never the f32 fallback:
14808        // an unquantized lm_head is vocab·hidden and must not be uploaded.
14809        let lm_gw = if upto_excl == self.num_layers
14810            && self.graph_want_logits
14811            && std::env::var("CMF_GPU_LMHEAD")
14812                .map(|v| v != "0")
14813                .unwrap_or(true)
14814        {
14815            self.weights
14816                .lm_head
14817                .graph_weight()
14818                .or_else(|| self.weights.lm_head.graph_weight_descriptor())
14819                .map(|(m, i, kind, rs)| {
14820                    let name = &m.tensors[i].name;
14821                    let prism = if crate::prism::is_inverse_embedding(m, name) {
14822                        crate::gpu::GraphPrismOp::InverseEmbedding
14823                    } else if crate::prism::is_forward_weight(m, name) {
14824                        crate::gpu::GraphPrismOp::Forward
14825                    } else {
14826                        crate::gpu::GraphPrismOp::None
14827                    };
14828                    (
14829                        crate::gpu::GraphW {
14830                            idx: i,
14831                            kind,
14832                            row_scale: rs,
14833                            data: &[],
14834                            prism,
14835                            affine: crate::prism::is_affine_target(m, name),
14836                        },
14837                        self.weights.lm_head.rows(),
14838                    )
14839                })
14840        } else {
14841            None
14842        };
14843        let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
14844        // Multi-step re-embeds the winner on the device.
14845        let emb_gw = if steps > 1 {
14846            self.weights
14847                .embed_tokens
14848                .graph_weight()
14849                .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
14850                .map(|(m, i, kind, rs)| {
14851                    let name = &m.tensors[i].name;
14852                    let prism = if crate::prism::is_inverse_embedding(m, name) {
14853                        crate::gpu::GraphPrismOp::InverseEmbedding
14854                    } else if crate::prism::is_forward_weight(m, name) {
14855                        crate::gpu::GraphPrismOp::Forward
14856                    } else {
14857                        crate::gpu::GraphPrismOp::None
14858                    };
14859                    (
14860                        crate::gpu::GraphW {
14861                            idx: i,
14862                            kind,
14863                            row_scale: rs,
14864                            data: &[],
14865                            prism,
14866                            affine: crate::prism::is_affine_target(m, name),
14867                        },
14868                        self.weights.embed_tokens.rows(),
14869                        self.embed_multiplier,
14870                    )
14871                })
14872        } else {
14873            None
14874        };
14875
14876        // Loop boundaries: virtual layer indices after which final_norm is
14877        // applied (mid-stack only; the GLOBAL last layer's norm folds into
14878        // lm_head). Span-relative — the executor compares its enumerate
14879        // index. A span ending mid-stack keeps its boundary norm even when
14880        // it is the span's own last layer.
14881        let loop_norm_at: Vec<usize> = if self.loop_final_norm {
14882            (from..upto_excl.min(self.num_layers - 1))
14883                .filter(|&li| (li + 1) % self.physical_layers == 0)
14884                .map(|li| li - from)
14885                .collect()
14886        } else {
14887            Vec::new()
14888        };
14889        let mut h = hidden.to_vec();
14890        // The normal decode path only needs the fused lm-head logits.  A
14891        // CMF_LOGIT_DUMP diagnostic, however, promises a prompt-boundary
14892        // post-stack hidden alongside those logits; request the existing
14893        // second readback only for that explicit probe instead of dumping
14894        // the input copy left in `h` by a folded-head graph.
14895        let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
14896        let outcome = crate::gpu::forward_token_graph(
14897            &model,
14898            self.graph_kv_id,
14899            &layers,
14900            &o1_views,
14901            self.o1_epoch,
14902            &self.inv_freq,
14903            &mut h,
14904            nh,
14905            nkv,
14906            hd,
14907            self.attn_scale,
14908            rd,
14909            self.hidden_size,
14910            self.intermediate_size,
14911            position,
14912            self.kv_cache.max_seq_len,
14913            gemma,
14914            self.rms_eps as f32,
14915            lm,
14916            &self.weights.final_norm,
14917            logits_out,
14918            &loop_norm_at,
14919            steps,
14920            emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
14921            ids_out,
14922            layers_run,
14923            from,
14924            dump_hidden,
14925        );
14926        match outcome {
14927            crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
14928            crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
14929            crate::gpu::TokenGraphOutcome::Declined => None,
14930        }
14931    }
14932
14933    /// Batched prefill: k contiguous prompt positions through the whole wgpu
14934    /// graph in ONE submit (projections/FFN as GEMMs). `hiddens` is [k·hidden]
14935    /// in/out (embeddings in, layer output out); KV mirror / GDN state advance.
14936    /// false ⇒ unsupported → caller keeps the per-position graph.
14937    /// The b-row Metal graph plan for the whole model: every layer as a
14938    /// GDN run or a full-attention item, all-or-nothing (a layer outside the
14939    /// graph's contract → None, the caller runs plain). Shared by the
14940    /// speculative verify and the batched prefill.
14941    #[cfg(target_os = "macos")]
14942    #[allow(clippy::type_complexity)]
14943    fn metal_rows_plan(
14944        &self,
14945    ) -> Option<(
14946        Vec<MetalRowsItem<'_>>,
14947        std::sync::Arc<cortiq_core::CmfModel>,
14948        Option<crate::gpu_metal::GdnGpuCfg>,
14949    )> {
14950        use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
14951        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
14952        if !graph_force
14953            || !crate::gpu::enabled_here()
14954            || std::env::var("CMF_GPU_BLOCK")
14955                .map(|v| v == "0")
14956                .unwrap_or(false)
14957            || self.attn_softcap > 0.0
14958            || self.o1_active()
14959            || self.swa.is_some()
14960            || self.global_attn.is_some()
14961            || self.attention_heads_per_layer.is_some()
14962            // per-layer KV heads, narrow V, learned sinks (MiMo-V2)
14963            || self.graph_attn_decline_reason().is_some()
14964            || self.attn_v_norm
14965            || self.loop_final_norm
14966            || !self.metal_rope_scale_ok()
14967        {
14968            return None;
14969        }
14970        let attend_contract = self.head_dim % 4 == 0
14971            && self.head_dim <= 256
14972            && self.rotary_dim >= 2
14973            && self.rotary_dim <= self.head_dim
14974            && (self.rotary_dim / 2) % 32 == 0
14975            && self.num_kv_heads > 0
14976            && self.num_heads % self.num_kv_heads == 0;
14977        if !attend_contract {
14978            return None;
14979        }
14980        let mut plan: Vec<MetalRowsItem> = Vec::new();
14981        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14982        for li in 0..self.num_layers {
14983            let lw = &self.weights.layers[self.phys_layer(li)];
14984            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14985                return None;
14986            }
14987            let ffn = match &lw.ffn {
14988                FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
14989                    let (Some(g), Some(u), Some(dn)) = (
14990                        d.gate_proj.metal_graph_parts(),
14991                        d.up_proj.metal_graph_parts(),
14992                        d.down_proj.metal_graph_parts(),
14993                    ) else {
14994                        return None;
14995                    };
14996                    MetalFfn::Dense {
14997                        gate: g,
14998                        up: u,
14999                        down: dn,
15000                        act: 0, // SiLU only (the arm above)
15001                    }
15002                }
15003                _ => return None,
15004            };
15005            match &lw.attn {
15006                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
15007                    let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
15008                        w.in_proj_qkv.metal_graph_parts(),
15009                        w.in_proj_z.metal_graph_parts(),
15010                        w.in_proj_a.f32_parts(),
15011                        w.in_proj_b.f32_parts(),
15012                        w.out_proj.metal_graph_parts(),
15013                    ) else {
15014                        return None;
15015                    };
15016                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
15017                        model_ref.get_or_insert_with(|| model.clone());
15018                    }
15019                    let gl = GdnGpuLayer {
15020                        mhc: None,
15021                        attn_norm: &lw.input_norm,
15022                        post_norm: &lw.post_norm,
15023                        qkv,
15024                        z,
15025                        a,
15026                        b: bb,
15027                        out,
15028                        ffn,
15029                        conv1d: &w.conv1d,
15030                        a_log: &w.a_log,
15031                        dt_bias: &w.dt_bias,
15032                        gnorm: &w.norm,
15033                    };
15034                    match plan.last_mut() {
15035                        Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
15036                        _ => plan.push(MetalRowsItem::Gdn {
15037                            run: vec![gl],
15038                            first: li,
15039                        }),
15040                    }
15041                }
15042                AttnKind::Full {
15043                    wq,
15044                    wk,
15045                    wv,
15046                    wo,
15047                    q_norm,
15048                    k_norm,
15049                    output_gate,
15050                    softplus_gate: None,
15051                    bias: None,
15052                } => {
15053                    let (Some(pq), Some(pk), Some(pv), Some(po)) = (
15054                        wq.metal_graph_parts(),
15055                        wk.metal_graph_parts(),
15056                        wv.metal_graph_parts(),
15057                        wo.metal_graph_parts(),
15058                    ) else {
15059                        return None;
15060                    };
15061                    if let QTensor::Mapped { model, .. } = wq {
15062                        model_ref.get_or_insert_with(|| model.clone());
15063                    }
15064                    let cache = &self.kv_cache.layers[li];
15065                    if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
15066                        return None;
15067                    }
15068                    plan.push(MetalRowsItem::Attn {
15069                        l: AttnGpuLayer {
15070                            mhc: None,
15071                            attn_norm: &lw.input_norm,
15072                            post_norm: &lw.post_norm,
15073                            wq: pq,
15074                            wk: pk,
15075                            wv: pv,
15076                            wo: po,
15077                            ffn,
15078                            attn_out_norm: None,
15079                            ffn_out_norm: None,
15080                            layer_scale: None,
15081                        },
15082                        li,
15083                        q_norm: q_norm.as_deref(),
15084                        k_norm: k_norm.as_deref(),
15085                        output_gate: *output_gate,
15086                    });
15087                }
15088                _ => return None,
15089            }
15090        }
15091        let model = model_ref?;
15092        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
15093            nv: cfg.num_v_heads,
15094            nk: cfg.num_k_heads,
15095            dk: cfg.key_head_dim,
15096            dv: cfg.value_head_dim,
15097            kk: cfg.conv_kernel,
15098            hidden: self.hidden_size,
15099            inter: self.intermediate_size,
15100            c_dim: cfg.conv_dim(),
15101            eps: cfg.rms_eps as f32,
15102            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15103        });
15104        Some((plan, model, gcfg))
15105    }
15106
15107    /// `AttnDeviceParams` for a plan item over the CPU cache as it stands.
15108    #[cfg(target_os = "macos")]
15109    #[allow(clippy::too_many_arguments)]
15110    fn metal_attn_params<'a>(
15111        li: usize,
15112        cache: &'a crate::kv_cache::LayerKvCache,
15113        q_norm: Option<&'a [f32]>,
15114        k_norm: Option<&'a [f32]>,
15115        output_gate: bool,
15116        inv_freq: &'a [f32],
15117        rope_scale: f32,
15118        geom: (usize, usize, usize, usize),
15119        pos0: usize,
15120        kv_id: u64,
15121        scale: f32,
15122        eps: f32,
15123        gemma: bool,
15124        late_qk_norm: bool,
15125    ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
15126        let (nh, nkv, hd, rd) = geom;
15127        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15128        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15129        let cpu_stored = cpu_k[0].len() / hd;
15130        (
15131            crate::gpu_metal::AttnDeviceParams {
15132                kv_id,
15133                layer: li,
15134                nh,
15135                nkv,
15136                hd,
15137                rd,
15138                position: pos0,
15139                scale,
15140                eps,
15141                gemma,
15142                late_qk_norm,
15143                output_gate,
15144                q_norm,
15145                k_norm,
15146                inv_freq,
15147                rope_scale,
15148                cpu_k,
15149                cpu_v,
15150                cpu_stored,
15151                cpu_gen: cache.generation(),
15152                o1: None,
15153                window: None,
15154                softmax_sink: cache.sinks.as_deref(),
15155                head_gate: None,
15156                v_norm: false,
15157            },
15158            cpu_stored,
15159        )
15160    }
15161
15162    /// Run the rows plan over `hiddens` (b rows at `pos0..`): validate,
15163    /// encode every item, optionally the head, sync. Returns the graph
15164    /// (for the commit / state finish) plus the GDN layer indices and the
15165    /// attention layers with the row count they were encoded against.
15166    #[cfg(target_os = "macos")]
15167    #[allow(clippy::type_complexity)]
15168    fn metal_rows_run(
15169        &mut self,
15170        hiddens: &mut [f32],
15171        pos0: usize,
15172        b: usize,
15173        prefill: bool,
15174        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15175        // Greedy verify: (row length scored, the b argmax ids out) — the
15176        // head's argmax runs on the device and the logits plane is NOT
15177        // read back (`spec.2` stays empty).
15178        mut argmax_out: Option<(usize, &mut Vec<u32>)>,
15179    ) -> MetalRowsRun {
15180        use crate::gpu_metal::{GraphDims, VerifyGraph};
15181        // The previous round's commit may still be replaying into the
15182        // trunk GDN owners on the second queue: this graph reads them
15183        // (zero-copy wraps) and may reallocate them below — collect the
15184        // replay first. Normally already complete (the draft chain ran
15185        // in between); a failed replay is terminal like a failed commit.
15186        if !crate::gpu_metal::wait_replay() {
15187            tracing::error!("Metal rows graph: the pending async replay failed");
15188            return MetalRowsRun::Failed;
15189        }
15190        spec_stamp("v.wait");
15191        // Seed the GDN recurrent records the rows graph reads — GDN layers
15192        // ONLY (the record the CPU path would allocate anyway).  Sizing
15193        // every layer here planted a zero GDN-sized record on a bounded
15194        // anchor of a natively bounded file this path then refused, and
15195        // that record counted as recurrent state on macOS.
15196        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
15197        if want > 0 {
15198            let phys = self.physical_layers.max(1);
15199            for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
15200                let is_gdn = self
15201                    .weights
15202                    .layers
15203                    .get(li % phys)
15204                    .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
15205                if is_gdn && l.linear_state.len() != want {
15206                    l.linear_state = vec![0f32; want];
15207                }
15208            }
15209        }
15210        let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
15211            return MetalRowsRun::Declined;
15212        };
15213        spec_stamp("v.plan");
15214        let dims = GraphDims {
15215            hidden: self.hidden_size,
15216            eps: self.rms_eps as f32,
15217            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15218        };
15219        let Some(mut graph) = (if prefill {
15220            VerifyGraph::new_prefill(&model, dims, hiddens, b)
15221        } else {
15222            VerifyGraph::new(&model, dims, hiddens, b)
15223        }) else {
15224            return MetalRowsRun::Declined;
15225        };
15226        let geom = (
15227            self.num_heads,
15228            self.num_kv_heads,
15229            self.head_dim,
15230            self.rotary_dim,
15231        );
15232        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15233        let eps = self.rms_eps as f32;
15234        let kv_id = self.graph_kv_id;
15235        let inv_freq = self.inv_freq.clone();
15236        for item in &plan {
15237            let ok = match item {
15238                MetalRowsItem::Gdn { run, .. } => gcfg
15239                    .as_ref()
15240                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
15241                    .unwrap_or(false),
15242                MetalRowsItem::Attn {
15243                    l,
15244                    li,
15245                    q_norm,
15246                    k_norm,
15247                    output_gate,
15248                } => {
15249                    let (p, _) = Self::metal_attn_params(
15250                        *li,
15251                        &self.kv_cache.layers[*li],
15252                        *q_norm,
15253                        *k_norm,
15254                        *output_gate,
15255                        &inv_freq,
15256                        self.layer_rope_scale(*li),
15257                        geom,
15258                        pos0,
15259                        kv_id,
15260                        self.attn_scale,
15261                        eps,
15262                        gemma,
15263                        self.qk_norm_after_rope,
15264                    );
15265                    graph.attn_ok(l, &p)
15266                }
15267            };
15268            if !ok {
15269                use std::sync::atomic::{AtomicBool, Ordering};
15270                static SAID: AtomicBool = AtomicBool::new(false);
15271                if !SAID.swap(true, Ordering::Relaxed) {
15272                    tracing::warn!("metal rows graph: a layer failed preflight — declining");
15273                }
15274                return MetalRowsRun::Declined;
15275            }
15276        }
15277        let lm = match &spec {
15278            Some((lm, _, _)) => {
15279                if !graph.lm_head_ok(*lm) {
15280                    return MetalRowsRun::Declined;
15281                }
15282                Some(*lm)
15283            }
15284            None => None,
15285        };
15286        let mut gdn_layers = Vec::new();
15287        let mut attn_layers = Vec::new();
15288        for item in &plan {
15289            match item {
15290                MetalRowsItem::Gdn { run, first } => {
15291                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
15292                        .iter()
15293                        .map(|l| l.linear_state.as_slice())
15294                        .collect();
15295                    if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
15296                        return MetalRowsRun::Declined;
15297                    }
15298                    gdn_layers.extend(*first..*first + run.len());
15299                }
15300                MetalRowsItem::Attn {
15301                    l,
15302                    li,
15303                    q_norm,
15304                    k_norm,
15305                    output_gate,
15306                } => {
15307                    let (p, cpu_stored) = Self::metal_attn_params(
15308                        *li,
15309                        &self.kv_cache.layers[*li],
15310                        *q_norm,
15311                        *k_norm,
15312                        *output_gate,
15313                        &inv_freq,
15314                        self.layer_rope_scale(*li),
15315                        geom,
15316                        pos0,
15317                        kv_id,
15318                        self.attn_scale,
15319                        eps,
15320                        gemma,
15321                        self.qk_norm_after_rope,
15322                    );
15323                    if !graph.encode_attn_b(l, &p) {
15324                        return MetalRowsRun::Declined;
15325                    }
15326                    attn_layers.push((*li, cpu_stored));
15327                }
15328            }
15329        }
15330        if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
15331            if !graph.encode_lm_head_b(final_norm, lm) {
15332                return MetalRowsRun::Declined;
15333            }
15334            // The device argmax is an OPTIMISATION, never a reason to
15335            // decline the round: if it will not encode, drop it and read
15336            // the logits plane back the old way (the head is encoded
15337            // either way, so the rows are there).
15338            if let Some((n, _)) = argmax_out.as_ref() {
15339                if !graph.encode_argmax_b(*n) {
15340                    argmax_out = None;
15341                }
15342            }
15343        }
15344        spec_stamp("v.enc");
15345        if !graph.sync() {
15346            return MetalRowsRun::Failed;
15347        }
15348        spec_stamp("v.gpu");
15349        match (spec, argmax_out) {
15350            (Some(_), Some((_, ids))) => {
15351                ids.resize(b, 0);
15352                if !graph.read_argmax(ids) {
15353                    return MetalRowsRun::Failed;
15354                }
15355                spec_stamp("v.am");
15356            }
15357            (Some((lm, _, logits)), None) => {
15358                logits.resize(b * lm.1, 0.0);
15359                if !graph.read_logits(logits) {
15360                    return MetalRowsRun::Failed;
15361                }
15362                spec_stamp("v.lg");
15363            }
15364            (None, _) => {}
15365        }
15366        if !graph.read_hidden(hiddens) {
15367            return MetalRowsRun::Failed;
15368        }
15369        spec_stamp("v.hid");
15370        MetalRowsRun::Completed(MetalVerifyPending {
15371            graph,
15372            gdn_layers,
15373            attn_layers,
15374        })
15375    }
15376
15377    /// Native-Metal twin of `try_batch_graph_wgpu`: the b rows through the
15378    /// whole model on the `VerifyGraph` (one submit), the head folded in
15379    /// when `spec` asks; `hiddens` come back as the last layer's output
15380    /// rows, `spec.2` as `[b][lm_rows]` logits. The graph is parked in
15381    /// `metal_verify` for `metal_verify_commit`.
15382    #[cfg(target_os = "macos")]
15383    fn try_batch_graph_metal(
15384        &mut self,
15385        hiddens: &mut [f32],
15386        positions: &[usize],
15387        b: usize,
15388        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15389        argmax_out: Option<(usize, &mut Vec<u32>)>,
15390    ) -> crate::gpu::BatchGraphOutcome {
15391        let _t0 = std::time::Instant::now();
15392        if positions.len() != b
15393            || self.mhc.is_some()
15394            || self.prompt_mrope.is_some()
15395            || positions.windows(2).any(|w| w[1] != w[0] + 1)
15396            || hiddens.len() != b * self.hidden_size
15397        {
15398            return crate::gpu::BatchGraphOutcome::Declined;
15399        }
15400        let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
15401            MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
15402            MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
15403            MetalRowsRun::Completed(pending) => pending,
15404        };
15405        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15406            eprintln!(
15407                "metal-verify: {:.1} ms | b={b}",
15408                _t0.elapsed().as_secs_f64() * 1e3
15409            );
15410        }
15411        self.metal_verify = Some(pending);
15412        crate::gpu::BatchGraphOutcome::Completed
15413    }
15414
15415    /// Batched prefill on the Metal rows graph: `ids` (≤ 512) at
15416    /// `start_pos..`, states written in place, K/V rows appended to the
15417    /// CPU caches; optional final norm/head logits are returned in `spec`.
15418    /// Declined means no command buffer was admitted; Failed is terminal.
15419    #[cfg(target_os = "macos")]
15420    fn prefill_rows_metal(
15421        &mut self,
15422        ids: &[u32],
15423        start_pos: usize,
15424        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15425    ) -> MetalPrefillOutcome {
15426        let b = ids.len();
15427        if b == 0 || b > 512 {
15428            return MetalPrefillOutcome::Declined;
15429        }
15430        METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15431        let with_head = spec.is_some();
15432        let hs = self.hidden_size;
15433        let mut hiddens = vec![0f32; b * hs];
15434        for (j, &id) in ids.iter().enumerate() {
15435            let e = self.embed_single(id);
15436            hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
15437        }
15438        let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
15439            MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
15440            MetalRowsRun::Failed => {
15441                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15442                return MetalPrefillOutcome::Failed;
15443            }
15444            MetalRowsRun::Completed(pending) => pending,
15445        };
15446        // states are final: copy them to the owners
15447        let idxs = pending.gdn_layers.clone();
15448        let mut outs: Vec<&mut [f32]> = self
15449            .kv_cache
15450            .layers
15451            .iter_mut()
15452            .enumerate()
15453            .filter(|(i, _)| idxs.binary_search(i).is_ok())
15454            .map(|(_, l)| l.linear_state.as_mut_slice())
15455            .collect();
15456        if !pending.graph.finish_states(&mut outs) {
15457            METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15458            return MetalPrefillOutcome::Failed;
15459        }
15460        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15461        // Read every layer before mutating any CPU cache.  A missing mirror
15462        // row is a terminal graph failure, not a reason to append a partial
15463        // prefix and replay the remainder serially.
15464        let mut rows = Vec::with_capacity(pending.attn_layers.len());
15465        for (li, cpu_stored) in &pending.attn_layers {
15466            let mut kbuf = vec![0f32; b * nkv * hd];
15467            let mut vbuf = vec![0f32; b * nkv * hd];
15468            if !crate::gpu_metal::kv_mirror_read_rows(
15469                self.graph_kv_id,
15470                *li,
15471                nkv,
15472                hd,
15473                *cpu_stored,
15474                b,
15475                &mut kbuf,
15476                &mut vbuf,
15477            ) {
15478                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15479                return MetalPrefillOutcome::Failed;
15480            }
15481            rows.push((*li, *cpu_stored, kbuf, vbuf));
15482        }
15483        for (li, cpu_stored, kbuf, vbuf) in rows {
15484            let cache = &mut self.kv_cache.layers[li];
15485            for r in 0..b {
15486                cache.append(
15487                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15488                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15489                    &[],
15490                );
15491            }
15492            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
15493        }
15494        METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15495        if with_head {
15496            METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15497        }
15498        MetalPrefillOutcome::Completed(hiddens)
15499    }
15500
15501    #[cfg(target_os = "macos")]
15502    fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
15503        self.prefill_rows_metal(ids, start_pos, None)
15504    }
15505
15506    /// Exact teacher-forced NLL through the ordinary Metal rows graph.  This
15507    /// is intentionally separate from the serial TokenGraph scorer: every
15508    /// chunk owns a real b-row graph/head completion and the recurrent/KV
15509    /// handoff is committed before the next chunk begins.
15510    #[cfg(target_os = "macos")]
15511    fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
15512        if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
15513            return MetalBatchNllOutcome::Declined;
15514        }
15515        let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
15516            return MetalBatchNllOutcome::Declined;
15517        };
15518        let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
15519            .ok()
15520            .and_then(|v| v.parse::<usize>().ok())
15521            .filter(|&v| (1..=512).contains(&v))
15522            .unwrap_or(32);
15523        let final_norm = self.weights.final_norm.clone();
15524        let mut nll = 0.0f64;
15525        let mut count = 0usize;
15526        let mut pos = 0usize;
15527        let mut completed = 0usize;
15528        while pos < ids.len() {
15529            let end = (pos + chunk).min(ids.len());
15530            let mut logits = Vec::new();
15531            let outcome =
15532                self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
15533            match outcome {
15534                MetalPrefillOutcome::Declined => {
15535                    return if completed == 0 {
15536                        MetalBatchNllOutcome::Declined
15537                    } else {
15538                        MetalBatchNllOutcome::Failed(format!(
15539                            "ordinary Metal NLL batch declined after {completed} chunks"
15540                        ))
15541                    };
15542                }
15543                MetalPrefillOutcome::Failed => {
15544                    return MetalBatchNllOutcome::Failed(
15545                        "ordinary Metal NLL batch failed after admission".to_string(),
15546                    );
15547                }
15548                MetalPrefillOutcome::Completed(_) => {}
15549            }
15550            completed += 1;
15551            let vocab = self.vocab_size.min(lm.1);
15552            if logits.len() != (end - pos) * lm.1 || vocab == 0 {
15553                return MetalBatchNllOutcome::Failed(
15554                    "ordinary Metal NLL head returned an invalid shape".to_string(),
15555                );
15556            }
15557            for row in 0..(end - pos) {
15558                let absolute = pos + row;
15559                if absolute < start || absolute + 1 >= ids.len() {
15560                    continue;
15561                }
15562                let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
15563                if let Some(mu) = self.logit_multiplier {
15564                    for v in lg.iter_mut() {
15565                        *v *= mu;
15566                    }
15567                }
15568                if let Some(c) = self.final_softcap {
15569                    softcap_in_place(lg, c, self.pool.as_deref());
15570                }
15571                let target = ids[absolute + 1] as usize;
15572                if target >= vocab {
15573                    return MetalBatchNllOutcome::Failed(format!(
15574                        "target token {target} exceeds Metal head rows {vocab}"
15575                    ));
15576                }
15577                let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
15578                let lse: f64 = lg
15579                    .iter()
15580                    .map(|&v| ((v - max) as f64).exp())
15581                    .sum::<f64>()
15582                    .ln()
15583                    + max as f64;
15584                nll += lse - lg[target] as f64;
15585                count += 1;
15586            }
15587            pos = end;
15588        }
15589        MetalBatchNllOutcome::Completed(nll, count)
15590    }
15591
15592    /// Commit a Metal verify round: replay the GDN recurrences over the
15593    /// `a + 1` accepted positions into the CPU states, append the accepted
15594    /// K/V rows from the mirrors to the CPU caches, re-point the mirrors.
15595    #[cfg(target_os = "macos")]
15596    fn metal_verify_commit(&mut self, a: usize) -> bool {
15597        let Some(mut pending) = self.metal_verify.take() else {
15598            return false;
15599        };
15600        let n = a + 1;
15601        // encode order == ascending layer order (the plan walks 0..layers)
15602        let idxs = pending.gdn_layers.clone();
15603        let mut outs: Vec<&mut [f32]> = self
15604            .kv_cache
15605            .layers
15606            .iter_mut()
15607            .enumerate()
15608            .filter(|(i, _)| idxs.binary_search(i).is_ok())
15609            .map(|(_, l)| l.linear_state.as_mut_slice())
15610            .collect();
15611        if !pending.graph.commit(n, &mut outs) {
15612            return false;
15613        }
15614        spec_stamp("c.replay");
15615        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15616        // Read every layer before mutating any CPU cache.  Missing rows are
15617        // terminal after the replay has executed; never append a partial KV
15618        // prefix and continue on a serial path.
15619        let mut rows = Vec::with_capacity(pending.attn_layers.len());
15620        for (li, cpu_stored) in &pending.attn_layers {
15621            let mut kbuf = vec![0f32; n * nkv * hd];
15622            let mut vbuf = vec![0f32; n * nkv * hd];
15623            if !crate::gpu_metal::kv_mirror_read_rows(
15624                self.graph_kv_id,
15625                *li,
15626                nkv,
15627                hd,
15628                *cpu_stored,
15629                n,
15630                &mut kbuf,
15631                &mut vbuf,
15632            ) {
15633                return false;
15634            }
15635            rows.push((*li, *cpu_stored, kbuf, vbuf));
15636        }
15637        for (li, cpu_stored, kbuf, vbuf) in rows {
15638            let cache = &mut self.kv_cache.layers[li];
15639            for r in 0..n {
15640                cache.append(
15641                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15642                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15643                    &[],
15644                );
15645            }
15646            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
15647        }
15648        spec_stamp("c.kv");
15649        true
15650    }
15651
15652    /// The round's warm-ups as ONE b-row graph run over the MTP block on
15653    /// Metal: `pairs` = (trunk hidden, next token) at consecutive positions
15654    /// from `first_pos`; the block's input projection is folded in. This
15655    /// half encodes and SUBMITS (no wait); `mtp_warm_batch_finish` waits
15656    /// and pulls the appended K/V rows into the CPU MTP cache. None = the
15657    /// graph declined (nothing submitted, nothing appended).
15658    #[cfg(target_os = "macos")]
15659    fn mtp_warm_batch_submit(
15660        &mut self,
15661        m: &mut MtpModule,
15662        pairs: &[(&[f32], u32)],
15663        first_pos: usize,
15664    ) -> Option<MetalWarmPending> {
15665        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
15666        let b = pairs.len();
15667        if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
15668            return None;
15669        }
15670        let AttnKind::Full {
15671            wq,
15672            wk,
15673            wv,
15674            wo,
15675            q_norm,
15676            k_norm,
15677            output_gate,
15678            softplus_gate: None,
15679            bias: None,
15680        } = &m.layer.attn
15681        else {
15682            return None;
15683        };
15684        let FfnKind::Dense(d) = &m.layer.ffn else {
15685            return None;
15686        };
15687        if !d.segs.is_empty() {
15688            return None;
15689        }
15690        let (Some(pq), Some(pk), Some(pv), Some(po)) =
15691            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
15692        else {
15693            return None;
15694        };
15695        let (Some(g), Some(u), Some(dn)) = (
15696            d.gate_proj.q1_parts(),
15697            d.up_proj.q1_parts(),
15698            d.down_proj.q1_parts(),
15699        ) else {
15700            return None;
15701        };
15702        let Some(eh) = m.eh_proj.q1_parts() else {
15703            return None;
15704        };
15705        let QTensor::Mapped { model, .. } = wq else {
15706            return None;
15707        };
15708        let model = model.clone();
15709        let hs = self.hidden_size;
15710        // [enorm(embed(tok)); hnorm(hidden)] rows
15711        let mut cat = vec![0f32; b * 2 * hs];
15712        for (j, (h, tok)) in pairs.iter().enumerate() {
15713            let e = self.embed_single(*tok);
15714            let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
15715            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
15716            inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
15717        }
15718        let dims = GraphDims {
15719            hidden: hs,
15720            eps: self.rms_eps as f32,
15721            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15722        };
15723        spec_stamp("w.cat");
15724        let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
15725            return None;
15726        };
15727        spec_stamp("w.new");
15728        let l = AttnGpuLayer {
15729            mhc: None,
15730            attn_norm: &m.layer.input_norm,
15731            post_norm: &m.layer.post_norm,
15732            wq: pq,
15733            wk: pk,
15734            wv: pv,
15735            wo: po,
15736            ffn: MetalFfn::Dense {
15737                gate: g,
15738                up: u,
15739                down: dn,
15740                act: (d.act == Act::Gelu) as u8,
15741            },
15742            attn_out_norm: None,
15743            ffn_out_norm: None,
15744            layer_scale: None,
15745        };
15746        let (nh, nkv, hd, rd) = (
15747            self.num_heads,
15748            self.num_kv_heads,
15749            self.head_dim,
15750            self.rotary_dim,
15751        );
15752        let inv_freq = self.inv_freq.clone();
15753        let cpu_stored;
15754        {
15755            let cache = &m.kv;
15756            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15757            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15758            cpu_stored = cpu_k[0].len() / hd;
15759            // The cache may LAG the position (rows nobody warmed): the
15760            // pairs land at cpu_stored.. with their true RoPE positions
15761            // first_pos.., exactly what the one-by-one warm does. A cache
15762            // AHEAD of the position is a real inconsistency.
15763            if cpu_stored > first_pos {
15764                spec_stamp("w.decl");
15765                return None;
15766            }
15767            let p = AttnDeviceParams {
15768                kv_id: self.mtp_kv_id(),
15769                layer: Self::MTP_LAYER_BASE,
15770                nh,
15771                nkv,
15772                hd,
15773                rd,
15774                position: first_pos,
15775                scale: self.attn_scale,
15776                eps: self.rms_eps as f32,
15777                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15778                late_qk_norm: self.qk_norm_after_rope,
15779                output_gate: *output_gate,
15780                q_norm: q_norm.as_deref(),
15781                k_norm: k_norm.as_deref(),
15782                inv_freq: &inv_freq,
15783                rope_scale: self.rope_scale,
15784                cpu_k,
15785                cpu_v,
15786                cpu_stored,
15787                cpu_gen: cache.generation(),
15788                o1: None,
15789                window: None,
15790                softmax_sink: None,
15791                head_gate: None,
15792                v_norm: false,
15793            };
15794            if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
15795                return None;
15796            }
15797        }
15798        spec_stamp("w.enc");
15799        if !graph.submit() {
15800            return None;
15801        }
15802        spec_stamp("w.sub");
15803        Some(MetalWarmPending {
15804            graph,
15805            cpu_stored,
15806            b,
15807        })
15808    }
15809
15810    /// Submit and finish in one call (the prefill's MTP warm-up, where
15811    /// nothing runs in between).
15812    #[cfg(target_os = "macos")]
15813    fn mtp_warm_batch_metal(
15814        &mut self,
15815        m: &mut MtpModule,
15816        pairs: &[(&[f32], u32)],
15817        first_pos: usize,
15818    ) -> bool {
15819        match self.mtp_warm_batch_submit(m, pairs, first_pos) {
15820            Some(p) => self.mtp_warm_batch_finish(m, p),
15821            None => false,
15822        }
15823    }
15824
15825    /// Second half of the batched warm-up: wait for the submitted graph,
15826    /// pull its b appended K/V rows into the CPU MTP cache, re-point the
15827    /// mirror. False = the command buffer failed or the rows are missing
15828    /// (nothing appended; the caller falls back to the one-by-one warm).
15829    #[cfg(target_os = "macos")]
15830    fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
15831        let MetalWarmPending {
15832            mut graph,
15833            cpu_stored,
15834            b,
15835        } = pending;
15836        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15837        if !graph.sync() {
15838            return false;
15839        }
15840        spec_stamp("w.gpu");
15841        let mut kbuf = vec![0f32; b * nkv * hd];
15842        let mut vbuf = vec![0f32; b * nkv * hd];
15843        if !crate::gpu_metal::kv_mirror_read_rows(
15844            self.mtp_kv_id(),
15845            Self::MTP_LAYER_BASE,
15846            nkv,
15847            hd,
15848            cpu_stored,
15849            b,
15850            &mut kbuf,
15851            &mut vbuf,
15852        ) {
15853            return false;
15854        }
15855        for r in 0..b {
15856            m.kv.append(
15857                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15858                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15859                &[],
15860            );
15861        }
15862        crate::gpu_metal::kv_mirror_set_stored(
15863            self.mtp_kv_id(),
15864            Self::MTP_LAYER_BASE,
15865            cpu_stored + b,
15866        );
15867        spec_stamp("w.kv");
15868        true
15869    }
15870
15871    /// A committed token id from the high table (Cyrillic, CJK and the
15872    /// like sit above 131072 in Qwen's vocabulary; Latin subwords past
15873    /// the 65536 cut are rare enough to lose as rejected drafts) switches
15874    /// the draft to the full head for the next 16 tokens; other ids count
15875    /// down. On an M4 the full 660 MB head costs 5.5 ms a draft step
15876    /// against 1.4 for the shortlist, so the streak is kept short.
15877    pub(crate) fn note_draft_id(&mut self, id: u32) {
15878        let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
15879        if (id as usize) >= cut {
15880            self.draft_full_streak = 16;
15881        } else {
15882            self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
15883        }
15884    }
15885
15886    /// The draft head's rows for the next step: the shortlist, or the full
15887    /// head while `draft_full_streak` runs.
15888    fn draft_head_rows(&self, head_rows: usize) -> usize {
15889        if self.draft_full_streak > 0 {
15890            head_rows
15891        } else {
15892            Self::draft_vocab_rows(head_rows)
15893        }
15894    }
15895
15896    /// Draft-head shortlist size: `CMF_DRAFT_VOCAB` rows (default 65536,
15897    /// capped at the head; 0 = full head).
15898    fn draft_vocab_rows(head_rows: usize) -> usize {
15899        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
15900        let n = *N.get_or_init(|| {
15901            std::env::var("CMF_DRAFT_VOCAB")
15902                .ok()
15903                .and_then(|v| v.parse().ok())
15904                .unwrap_or(65536)
15905        });
15906        if n == 0 { head_rows } else { n.min(head_rows) }
15907    }
15908
15909    /// One MTP block step on the native Metal token graph: block input on
15910    /// the host, the attention layer + FFN device-resident over the MTP
15911    /// mirror, the head folded in when `want_logits`. The appended K/V row
15912    /// is pulled into the CPU MTP cache (owner of record) after the sync.
15913    #[cfg(target_os = "macos")]
15914    fn mtp_step_metal(
15915        &mut self,
15916        m: &mut MtpModule,
15917        hidden: &[f32],
15918        next_token: u32,
15919        position: usize,
15920        want_logits: bool,
15921    ) -> Option<(Vec<f32>, Vec<f32>)> {
15922        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
15923        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
15924            || !crate::gpu::q1_force()
15925            || !crate::gpu::enabled_here()
15926            || self.attn_softcap > 0.0
15927            || self.attention_heads_per_layer.is_some()
15928            || m.kv.mode != crate::kv_cache::KvMode::F32
15929            || m.kv.o1.is_some()
15930        {
15931            return None;
15932        }
15933        let AttnKind::Full {
15934            wq,
15935            wk,
15936            wv,
15937            wo,
15938            q_norm,
15939            k_norm,
15940            output_gate,
15941            softplus_gate: None,
15942            bias: None,
15943        } = &m.layer.attn
15944        else {
15945            return None;
15946        };
15947        let FfnKind::Dense(d) = &m.layer.ffn else {
15948            return None;
15949        };
15950        if d.act != Act::Silu || !d.segs.is_empty() {
15951            return None;
15952        }
15953        let (pq, pk, pv, po) = (
15954            wq.q1_parts()?,
15955            wk.q1_parts()?,
15956            wv.q1_parts()?,
15957            wo.q1_parts()?,
15958        );
15959        let (g, u, dn) = (
15960            d.gate_proj.q1_parts()?,
15961            d.up_proj.q1_parts()?,
15962            d.down_proj.q1_parts()?,
15963        );
15964        let QTensor::Mapped { model, .. } = wq else {
15965            return None;
15966        };
15967        let model = model.clone();
15968        let lm = if want_logits {
15969            Some(self.weights.lm_head.q1_parts()?)
15970        } else {
15971            None
15972        };
15973        let dims = GraphDims {
15974            hidden: self.hidden_size,
15975            eps: self.rms_eps as f32,
15976            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15977        };
15978        // The block input `eh_proj · [enorm(e); hnorm(h)]` rides in the
15979        // graph (one submit a step); the host per-op matvec if it cannot.
15980        let hs = self.hidden_size;
15981        let mut x = vec![0f32; hs];
15982        // Let an eligible q4tp MTP draft use the measured masked-nibble
15983        // matvec.  Unlike fused epilogues, this reduces the draft's
15984        // projection work without changing its residual-update schedule;
15985        // the target verifier remains the authority over emitted tokens.
15986        // The guard keeps `CMF_METAL_MVFAST=0` as an immediate rollback
15987        // switch and restores the caller's setting on every early return.
15988        let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
15989            crate::gpu_metal::DENSE_MV,
15990        );
15991        let mut graph = TokenGraph::new(&model, dims, &x)?;
15992        let mut folded = false;
15993        if let Some(eh) = m.eh_proj.q1_parts() {
15994            let e = self.embed_single(next_token);
15995            let mut cat = vec![0.0f32; 2 * hs];
15996            let (cat_e, cat_h) = cat.split_at_mut(hs);
15997            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
15998            inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
15999            folded = graph.encode_input_proj(eh, &cat);
16000        }
16001        if !folded {
16002            x = self.mtp_block_input(m, hidden, next_token);
16003            graph = TokenGraph::new(&model, dims, &x)?;
16004        }
16005        spec_stamp("d.in");
16006        let l = AttnGpuLayer {
16007            mhc: None,
16008            attn_norm: &m.layer.input_norm,
16009            post_norm: &m.layer.post_norm,
16010            wq: pq,
16011            wk: pk,
16012            wv: pv,
16013            wo: po,
16014            ffn: MetalFfn::Dense {
16015                gate: g,
16016                up: u,
16017                down: dn,
16018                act: (d.act == Act::Gelu) as u8,
16019            },
16020            attn_out_norm: None,
16021            ffn_out_norm: None,
16022            layer_scale: None,
16023        };
16024        let (nh, nkv, hd, rd) = (
16025            self.num_heads,
16026            self.num_kv_heads,
16027            self.head_dim,
16028            self.rotary_dim,
16029        );
16030        let inv_freq = self.inv_freq.clone();
16031        {
16032            let cache = &m.kv;
16033            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
16034            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
16035            let cpu_stored = cpu_k[0].len() / hd;
16036            let p = AttnDeviceParams {
16037                kv_id: self.mtp_kv_id(),
16038                layer: Self::MTP_LAYER_BASE,
16039                nh,
16040                nkv,
16041                hd,
16042                rd,
16043                position,
16044                scale: self.attn_scale,
16045                eps: self.rms_eps as f32,
16046                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16047                late_qk_norm: self.qk_norm_after_rope,
16048                output_gate: *output_gate,
16049                q_norm: q_norm.as_deref(),
16050                k_norm: k_norm.as_deref(),
16051                inv_freq: &inv_freq,
16052                rope_scale: self.rope_scale,
16053                cpu_k,
16054                cpu_v,
16055                cpu_stored,
16056                cpu_gen: cache.generation(),
16057                o1: None,
16058                window: None,
16059                softmax_sink: None,
16060                head_gate: None,
16061                v_norm: false,
16062            };
16063            if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
16064                return None;
16065            }
16066        }
16067        // The draft's head over a vocabulary SHORTLIST (the first
16068        // CMF_DRAFT_VOCAB rows — BPE ids run roughly by merge rank, so the
16069        // low ids carry the mass): the verify keeps the full head, so a true
16070        // token past the cut is only a rejected draft, never a wrong token.
16071        // 662 MB a step on Qwen3.8 becomes 170 MB at 65536.
16072        let draft_rows = if let Some(lm) = lm {
16073            self.draft_head_rows(lm.1)
16074        } else {
16075            0
16076        };
16077        if let Some(lm) = lm {
16078            if !graph.lm_head_ok(lm) {
16079                return None;
16080            }
16081            if draft_rows < lm.1 {
16082                if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
16083                    return None;
16084                }
16085            } else {
16086                graph.encode_lm_head(&m.final_norm, lm);
16087            }
16088        }
16089        spec_stamp("d.enc");
16090        if graph.sync_checked().is_err() {
16091            return None;
16092        }
16093        spec_stamp("d.gpu");
16094        let mut logits = Vec::new();
16095        if let Some(lm) = lm {
16096            let n_read = draft_rows.min(lm.1).min(self.vocab_size);
16097            logits = attention::take_buf(n_read);
16098            graph.read_logits(&mut logits);
16099            // ids past the shortlist: never drafted (−∞ in every chain)
16100            pad_mtp_draft_logits(&mut logits, self.vocab_size);
16101        }
16102        graph.finish(&mut x);
16103        let mut krow = attention::take_buf(nkv * hd);
16104        let mut vrow = attention::take_buf(nkv * hd);
16105        if crate::gpu_metal::kv_mirror_read_last(
16106            self.mtp_kv_id(),
16107            Self::MTP_LAYER_BASE,
16108            nkv,
16109            hd,
16110            &mut krow,
16111            &mut vrow,
16112        ) {
16113            m.kv.append(&krow, &vrow, &[]);
16114        }
16115        attention::recycle_buf(&mut krow);
16116        attention::recycle_buf(&mut vrow);
16117        spec_stamp("d.rd");
16118        Some((logits, x))
16119    }
16120
16121    /// `CMF_MTP_CHAIN=0` keeps the per-step draft (one submit and one
16122    /// host round trip per MTP step); the default drafts the whole chain
16123    /// in one command buffer when the round is plain greedy.
16124    ///
16125    /// Measured on an M4 (24 GB), Qwen3.8-27B q4tp, P3 at 160 tokens,
16126    /// k=7, six runs per arm alternating inside one lock window — the
16127    /// round's draft phase (median over the 34 rounds of a run) is
16128    /// 34.5 ms per round old against 30.1 new, i.e. 4.93 → 4.31 ms per
16129    /// draft step. That is the whole prize: the 7 submits cost ~0.6 ms
16130    /// each in host and submit latency and nothing else changes —
16131    /// acceptance (3.41 of 7) and tokens per round (4.41) are identical,
16132    /// and the round is 289 → 285 ms, decode 13.8 → 14.0 tok/s.
16133    fn mtp_chain_on() -> bool {
16134        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16135        *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
16136    }
16137
16138    /// The round's k greedy drafts as ONE command buffer on Metal: the MTP
16139    /// block k times back to back, each step's token embedding gathered
16140    /// on the device from the argmax the step before it wrote, the head
16141    /// over the round's shortlist (or the full head during a full-head
16142    /// streak — decided once, before the chain, exactly as the per-step
16143    /// path decides it per step, since `draft_full_streak` only moves on
16144    /// a commit). One wait, then the k ids and the k appended K/V rows
16145    /// come back; the CPU MTP cache ends where k `mtp_step_metal` calls
16146    /// would have left it. `Err(false)` = declined before anything was
16147    /// committed (the per-step path takes the round); `Err(true)` = the
16148    /// command buffer failed after commit.
16149    #[cfg(target_os = "macos")]
16150    fn mtp_draft_chain_metal(
16151        &mut self,
16152        m: &mut MtpModule,
16153        hidden: &[f32],
16154        t_next: u32,
16155        position: usize,
16156        k: usize,
16157    ) -> Result<Vec<u32>, bool> {
16158        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
16159        if k == 0
16160            || k > 64
16161            || !Self::mtp_chain_on()
16162            || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
16163            || !crate::gpu::q1_force()
16164            || !crate::gpu::enabled_here()
16165            || self.attn_softcap > 0.0
16166            || self.attention_heads_per_layer.is_some()
16167            || m.kv.mode != crate::kv_cache::KvMode::F32
16168            || m.kv.o1.is_some()
16169            // the chain gathers embeddings itself: only the plain table
16170            || self.dsv4.is_some()
16171            || self.dsv41.is_some()
16172            || self.qwen4_exp.is_some()
16173            || self.per_token_stack()
16174        {
16175            return Err(false);
16176        }
16177        let AttnKind::Full {
16178            wq,
16179            wk,
16180            wv,
16181            wo,
16182            q_norm,
16183            k_norm,
16184            output_gate,
16185            softplus_gate: None,
16186            bias: None,
16187        } = &m.layer.attn
16188        else {
16189            return Err(false);
16190        };
16191        let FfnKind::Dense(d) = &m.layer.ffn else {
16192            return Err(false);
16193        };
16194        if d.act != Act::Silu || !d.segs.is_empty() {
16195            return Err(false);
16196        }
16197        let (Some(pq), Some(pk), Some(pv), Some(po)) =
16198            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
16199        else {
16200            return Err(false);
16201        };
16202        let (Some(g), Some(u), Some(dn)) = (
16203            d.gate_proj.q1_parts(),
16204            d.up_proj.q1_parts(),
16205            d.down_proj.q1_parts(),
16206        ) else {
16207            return Err(false);
16208        };
16209        let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
16210            return Err(false);
16211        };
16212        let QTensor::Mapped { model, .. } = wq else {
16213            return Err(false);
16214        };
16215        let model = model.clone();
16216        // the embedding table: a q4tp tensor of the SAME blob, no Prism
16217        // inverse-embedding post-pass
16218        let QTensor::Mapped {
16219            model: em,
16220            idx: eidx,
16221            dtype: cortiq_core::TensorDtype::Q4TiledP,
16222            ..
16223        } = &self.weights.embed_tokens
16224        else {
16225            return Err(false);
16226        };
16227        if !std::sync::Arc::ptr_eq(em, &model)
16228            || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
16229        {
16230            return Err(false);
16231        }
16232        let embed = (
16233            *eidx,
16234            self.weights.embed_tokens.rows(),
16235            self.weights.embed_tokens.cols(),
16236        );
16237        if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
16238            return Err(false);
16239        }
16240        let dims = GraphDims {
16241            hidden: self.hidden_size,
16242            eps: self.rms_eps as f32,
16243            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16244        };
16245        // Keep the chained MTP draft on the masked-nibble Metal matvec
16246        // path.  This scope covers every encode in the chain; the guard
16247        // restores its predecessor when this round returns or declines.
16248        let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
16249            crate::gpu_metal::DENSE_MV,
16250        );
16251        let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
16252            return Err(false);
16253        };
16254        if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
16255            return Err(false);
16256        }
16257        let l = AttnGpuLayer {
16258            mhc: None,
16259            attn_norm: &m.layer.input_norm,
16260            post_norm: &m.layer.post_norm,
16261            wq: pq,
16262            wk: pk,
16263            wv: pv,
16264            wo: po,
16265            ffn: MetalFfn::Dense {
16266                gate: g,
16267                up: u,
16268                down: dn,
16269                act: (d.act == Act::Gelu) as u8,
16270            },
16271            attn_out_norm: None,
16272            ffn_out_norm: None,
16273            layer_scale: None,
16274        };
16275        let (nh, nkv, hd, rd) = (
16276            self.num_heads,
16277            self.num_kv_heads,
16278            self.head_dim,
16279            self.rotary_dim,
16280        );
16281        let inv_freq = self.inv_freq.clone();
16282        let draft_rows = self.draft_head_rows(lm.1);
16283        let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
16284        if n_arg == 0 {
16285            return Err(false);
16286        }
16287        // `CMF_MTP_CHAIN_SPLIT=1` commits each step as it is encoded, so
16288        // the GPU starts on step 0 while the host is still encoding step
16289        // 1 — a probe for whether the host encode is on the critical
16290        // path. It is not: three runs each, draft 30.0 ms per round split
16291        // against 30.1 whole, and the whole chain's host encode measures
16292        // 0.3 ms against a 29.7 ms wait. Kept as a probe, off by default.
16293        let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
16294        let t_chain = std::time::Instant::now();
16295        graph.chain_ids_init(t_next, k);
16296        let cpu_stored;
16297        {
16298            let cache = &m.kv;
16299            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
16300            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
16301            cpu_stored = cpu_k[0].len() / hd;
16302            for j in 0..k {
16303                if !graph.encode_chain_input(
16304                    embed,
16305                    j as u32,
16306                    &m.enorm,
16307                    &m.hnorm,
16308                    self.embed_multiplier,
16309                    eh,
16310                ) {
16311                    return Err(false);
16312                }
16313                // step j's mirror row: the mirror is re-pointed at the CPU
16314                // rows before step 0 and advances by one per step; its
16315                // resync (never taken past step 0) reads the CPU rows
16316                let p = AttnDeviceParams {
16317                    kv_id: self.mtp_kv_id(),
16318                    layer: Self::MTP_LAYER_BASE,
16319                    nh,
16320                    nkv,
16321                    hd,
16322                    rd,
16323                    position: position + j,
16324                    scale: self.attn_scale,
16325                    eps: self.rms_eps as f32,
16326                    gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16327                    late_qk_norm: self.qk_norm_after_rope,
16328                    output_gate: *output_gate,
16329                    q_norm: q_norm.as_deref(),
16330                    k_norm: k_norm.as_deref(),
16331                    inv_freq: &inv_freq,
16332                    rope_scale: self.rope_scale,
16333                    cpu_k: cpu_k.clone(),
16334                    cpu_v: cpu_v.clone(),
16335                    cpu_stored: cpu_stored + j,
16336                    cpu_gen: cache.generation(),
16337                    o1: None,
16338                    window: None,
16339                    softmax_sink: None,
16340                    head_gate: None,
16341                    v_norm: false,
16342                };
16343                if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
16344                    return Err(false);
16345                }
16346                if draft_rows < lm.1 {
16347                    if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
16348                        return Err(false);
16349                    }
16350                } else {
16351                    graph.encode_lm_head(&m.final_norm, lm);
16352                }
16353                if !graph.encode_argmax(n_arg, j as u32 + 1) {
16354                    return Err(false);
16355                }
16356                if split {
16357                    // CMF_MTP_CHAIN_SPLIT=1: commit every step so the GPU
16358                    // starts on step 0 while the host encodes the rest
16359                    graph.commit();
16360                }
16361            }
16362        }
16363        let t_enc = t_chain.elapsed();
16364        if graph.sync_checked().is_err() {
16365            return Err(true);
16366        }
16367        if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
16368            eprintln!(
16369                "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
16370                t_enc.as_secs_f64() * 1e3,
16371                (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
16372                if split { ", split" } else { "" }
16373            );
16374        }
16375        let mut ids = vec![0u32; k];
16376        if !graph.chain_ids_read(&mut ids) {
16377            return Err(true);
16378        }
16379        let mut kbuf = vec![0f32; k * nkv * hd];
16380        let mut vbuf = vec![0f32; k * nkv * hd];
16381        if !crate::gpu_metal::kv_mirror_read_rows(
16382            self.mtp_kv_id(),
16383            Self::MTP_LAYER_BASE,
16384            nkv,
16385            hd,
16386            cpu_stored,
16387            k,
16388            &mut kbuf,
16389            &mut vbuf,
16390        ) {
16391            return Err(true);
16392        }
16393        for r in 0..k {
16394            m.kv.append(
16395                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
16396                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
16397                &[],
16398            );
16399        }
16400        Ok(ids)
16401    }
16402
16403    fn try_batch_graph_wgpu(
16404        &self,
16405        hiddens: &mut [f32],
16406        positions: &[usize],
16407        k: usize,
16408        spec: Option<crate::gpu::SpecTail<'_>>,
16409    ) -> crate::gpu::BatchGraphOutcome {
16410        self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
16411    }
16412
16413    /// `try_batch_graph_wgpu` with the device-prefix mode: `layers_run`
16414    /// Some lets a stack that does not fit run its leading layers (the
16415    /// token graph's prefix rule) and reports how many; `hiddens` then
16416    /// holds the boundary rows and the caller runs the rest on the host.
16417    fn try_batch_graph_wgpu_prefix(
16418        &self,
16419        hiddens: &mut [f32],
16420        positions: &[usize],
16421        k: usize,
16422        spec: Option<crate::gpu::SpecTail<'_>>,
16423        layers_run: Option<&mut usize>,
16424    ) -> crate::gpu::BatchGraphOutcome {
16425        // SenseNova-Loop: the mHC kernels take two streams (the
16426        // CMF_MHC_GRAPH=0 knob keeps the host); MRoPE prompt rows ride the
16427        // per-row attention loop with their own angle tables.
16428        if self.mhc.as_ref().is_some_and(|m| {
16429                m.s.streams != 2 || m.s.fixed_identity || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
16430            })
16431        {
16432            return crate::gpu::BatchGraphOutcome::Declined;
16433        }
16434        let graph_end = match self.mimo_moe.graph_prefix_end() {
16435            Some(end) if end < self.num_layers => {
16436                if layers_run.is_none() || spec.is_some() || end == 0 {
16437                    return crate::gpu::BatchGraphOutcome::Declined;
16438                }
16439                end
16440            }
16441            _ => self.num_layers,
16442        };
16443        let _tb = std::time::Instant::now();
16444        let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
16445        if self.attn_softcap > 0.0 {
16446            return crate::gpu::BatchGraphOutcome::Declined; // capped scores: no graph kernel — CPU path
16447        }
16448        // Same attention contract as the token graph: per-layer geometry
16449        // rides `geom`, anything it cannot express declines by name.
16450        if let Some(reason) = self
16451            .wgpu_graph_attn_decline()
16452            .or_else(|| self.batch_graph_gemma_decline())
16453        {
16454            self.note_graph_decline("wgpu batch graph", reason);
16455            return crate::gpu::BatchGraphOutcome::Declined;
16456        }
16457        let nh = self.num_heads;
16458        let (nkv, hd, rd) = self.layer_geom(0);
16459        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16460        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
16461            if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
16462            {
16463                let name = &m.tensors[i].name;
16464                let prism = if crate::prism::is_inverse_embedding(m, name) {
16465                    crate::gpu::GraphPrismOp::InverseEmbedding
16466                } else if crate::prism::is_forward_weight(m, name) {
16467                    crate::gpu::GraphPrismOp::Forward
16468                } else {
16469                    crate::gpu::GraphPrismOp::None
16470                };
16471                return Some(crate::gpu::GraphW {
16472                    idx: i,
16473                    kind,
16474                    row_scale: rs,
16475                    data: &[],
16476                    prism,
16477                    affine: crate::prism::is_affine_target(m, name),
16478                });
16479            }
16480            if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
16481                eprintln!(
16482                    "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
16483                    t.rows(),
16484                    t.cols()
16485                );
16486            }
16487            t.as_f32().map(|d| crate::gpu::GraphW {
16488                idx: 0,
16489                kind: 4,
16490                row_scale: &[],
16491                data: d,
16492                prism: crate::gpu::GraphPrismOp::None,
16493                affine: false,
16494            })
16495        }
16496        let built: Option<(
16497            Vec<crate::gpu::GraphLayer<'_>>,
16498            std::sync::Arc<cortiq_core::CmfModel>,
16499        )> = (|| {
16500            let mut layers = Vec::with_capacity(graph_end);
16501            let mut model = None;
16502            for li in 0..graph_end {
16503                let lw = &self.weights.layers[self.phys_layer(li)];
16504                // MoE routes per token, so its experts are encoded token by
16505                // token inside the batched submit while attention and the
16506                // projections stay GEMMs. Refusing MoE here is what left
16507                // prefill running one position at a time: 33 tok/s against
16508                // 54 on decode, i.e. reading the prompt was slower than
16509                // writing the answer.
16510                let gffn = match &lw.ffn {
16511                    FfnKind::Dense(d) if !d.segs.is_empty() => {
16512                        if batch_debug {
16513                            eprintln!("batch graph: dense segmented FFN at layer {li}");
16514                        }
16515                        return None;
16516                    }
16517                    FfnKind::Dense(d) => {
16518                        let Some(act) = d.act.wgpu_graph_act() else {
16519                            if batch_debug {
16520                                eprintln!(
16521                                    "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
16522                                    d.act
16523                                );
16524                            }
16525                            return None;
16526                        };
16527                        crate::gpu::GraphFfn::Dense {
16528                            gate: gw(&d.gate_proj)?,
16529                            up: gw(&d.up_proj)?,
16530                            down: gw(&d.down_proj)?,
16531                            act,
16532                        }
16533                    }
16534                    FfnKind::Moe(m) => {
16535                        // Adaptive τ and expert masks stay on the CPU path.
16536                        // Sigmoid scores, the selection bias, a routed scale
16537                        // ≠ 1 and an ungated shared expert (hy_v3) ride the
16538                        // same flags word as the token graph — before, this
16539                        // refusal sent every Hy-MT2-30B prompt to the chunked
16540                        // fallback (8 tok/s of ingest against 53 of decode).
16541                        if m.route_tau.is_some() || m.mask.is_some() {
16542                            return None;
16543                        }
16544                        // A shared expert rides as slot top_k (gated or
16545                        // not is a flag on the select kernel); without one
16546                        // (MiMo-V2, LFM2-MoE) the kernels run top_k slots.
16547                        let shared = m.shared.as_ref();
16548                        let has_shared = shared.is_some();
16549                        let shared_gated = matches!(shared, Some((_, Some(_))));
16550                        let sgate = match shared {
16551                            Some((_, Some(sg))) => gw(sg)?,
16552                            // Ungated or absent: the router plane stands in
16553                            // so the plumbing stays total; the kernel pins
16554                            // weight 1 or never reads it.
16555                            _ => gw(&m.router)?,
16556                        };
16557                        let router = gw(&m.router)?;
16558                        // The batch MoE kernels still consume raw per-token
16559                        // rows and do not carry the descriptor-aware Prism
16560                        // transform/affine bit for router or shared-gate
16561                        // planes.  Refuse rather than route an untransformed
16562                        // source activation.
16563                        if router.prism != crate::gpu::GraphPrismOp::None
16564                            || router.affine
16565                            || sgate.prism != crate::gpu::GraphPrismOp::None
16566                            || sgate.affine
16567                        {
16568                            return None;
16569                        }
16570                        let inter = m.experts.first()?.gate_proj.rows();
16571                        let mut experts = Vec::with_capacity(m.experts.len() + 1);
16572                        let mut q4tp: Option<bool> = None;
16573                        let mut gu_q2: Option<bool> = None;
16574                        let mut q82: Option<bool> = None;
16575                        // The packed MoE graph has one gate/up program per
16576                        // layer, so accept only one exact activation contract
16577                        // across routed and in-bank shared experts.
16578                        let mut moe_act = None;
16579                        for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
16580                            let Some(e_act) = e.act.wgpu_graph_act() else {
16581                                return None;
16582                            };
16583                            match moe_act {
16584                                Some(existing) if existing != e_act => return None,
16585                                None => moe_act = Some(e_act),
16586                                _ => {}
16587                            }
16588                            if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
16589                                return None;
16590                            }
16591                            // Same ladder as the token graph: q4t → q2tp
16592                            // (mixed profile: 2-bit gate/up over a q4tp
16593                            // down) → q4tp → q8_2f. Uniform across the layer.
16594                            let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
16595                            {
16596                                Some((mm, gi)) => (
16597                                    mm,
16598                                    gi,
16599                                    e.up_proj.mapped_q4t()?.1,
16600                                    e.down_proj.mapped_q4t()?.1,
16601                                    false,
16602                                    false,
16603                                    false,
16604                                ),
16605                                None => match e.gate_proj.mapped_q2tp() {
16606                                    Some((mm, gi)) => (
16607                                        mm,
16608                                        gi,
16609                                        e.up_proj.mapped_q2tp()?.1,
16610                                        e.down_proj.mapped_q4tp()?.1,
16611                                        true,
16612                                        true,
16613                                        false,
16614                                    ),
16615                                    None => match e.gate_proj.mapped_q4tp() {
16616                                        Some((mm, gi)) => (
16617                                            mm,
16618                                            gi,
16619                                            e.up_proj.mapped_q4tp()?.1,
16620                                            e.down_proj.mapped_q4tp()?.1,
16621                                            true,
16622                                            false,
16623                                            false,
16624                                        ),
16625                                        None => {
16626                                            if !crate::gpu::moe_q82_graph_on() {
16627                                                return None;
16628                                            }
16629                                            let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
16630                                            (
16631                                                mm,
16632                                                gi,
16633                                                e.up_proj.mapped_q8_2f()?.1,
16634                                                e.down_proj.mapped_q8_2f()?.1,
16635                                                false,
16636                                                false,
16637                                                true,
16638                                            )
16639                                        }
16640                                    },
16641                                },
16642                            };
16643                            if *q4tp.get_or_insert(is_p) != is_p
16644                                || *gu_q2.get_or_insert(is_q2) != is_q2
16645                                || *q82.get_or_insert(is_8) != is_8
16646                            {
16647                                return None;
16648                            }
16649                            if [gi, ui, di].into_iter().any(|idx| {
16650                                mm.tensors.get(idx).is_some_and(|t| {
16651                                    crate::prism::is_forward_weight(mm, &t.name)
16652                                        || crate::prism::is_affine_target(mm, &t.name)
16653                                })
16654                            }) {
16655                                return None;
16656                            }
16657                            model.get_or_insert_with(|| mm.clone());
16658                            experts.push((gi, ui, di));
16659                        }
16660                        crate::gpu::GraphFfn::Moe {
16661                            router,
16662                            shared_gate: sgate,
16663                            experts,
16664                            n_exp: m.experts.len(),
16665                            top_k: m.top_k,
16666                            inter,
16667                            act: moe_act?,
16668                            norm_topk: m.norm_topk_prob,
16669                            q4tp: q4tp?,
16670                            gu_q2: gu_q2.unwrap_or(false),
16671                            q82: q82.unwrap_or(false),
16672                            sigmoid: m.router_sigmoid,
16673                            sigmoid_denom_eps: m.sigmoid_denom_eps,
16674                            bias: m.expert_bias.as_deref(),
16675                            has_shared,
16676                            detached_shared: None,
16677                            shared_gated,
16678                            route_scale: m.routed_scaling,
16679                        }
16680                    }
16681                    _ => return None,
16682                };
16683                let attn = match &lw.attn {
16684                    AttnKind::Full {
16685                        wq,
16686                        wk,
16687                        wv,
16688                        wo,
16689                        q_norm,
16690                        k_norm,
16691                        output_gate,
16692                        softplus_gate,
16693                        bias,
16694                    } => {
16695                        if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
16696                            if batch_debug {
16697                                eprintln!(
16698                                    "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
16699                                    softplus_gate.is_some(),
16700                                    self.attention_heads_per_layer.is_some()
16701                                );
16702                            }
16703                            return None;
16704                        }
16705                        let (m, _, _, _) =
16706                            wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
16707                        model = Some(m.clone());
16708                        crate::gpu::GraphAttn::Full {
16709                            wq: gw(wq)?,
16710                            wk: gw(wk)?,
16711                            wv: gw(wv)?,
16712                            wo: gw(wo)?,
16713                            q_norm: q_norm.as_deref(),
16714                            k_norm: k_norm.as_deref(),
16715                            late_qk_norm: self.qk_norm_after_rope,
16716                            bias: bias
16717                                .as_ref()
16718                                .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16719                            output_gate: *output_gate,
16720                            cpu_k: self.kv_cache.layers[li].k_heads(),
16721                            cpu_v: self.kv_cache.layers[li].v_heads(),
16722                            cpu_base: self.kv_cache.layers[li].base(),
16723                            geom: self.graph_attn_geom(li),
16724                            // The batched graph has no head-gate arm; a
16725                            // gated layer is refused above.
16726                            head_gate: None,
16727                        }
16728                    }
16729                    AttnKind::LinearGdn(w) => {
16730                        let Some(cfg) = self.gdn_cfg else {
16731                            if batch_debug {
16732                                eprintln!("batch graph: no GDN config at layer {li}");
16733                            }
16734                            return None;
16735                        };
16736                        let (m, _, _, _) = w
16737                            .in_proj_qkv
16738                            .graph_weight()
16739                            .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
16740                        model = Some(m.clone());
16741                        crate::gpu::GraphAttn::Gdn {
16742                            qkv: gw(&w.in_proj_qkv)?,
16743                            z: gw(&w.in_proj_z)?,
16744                            a: gw(&w.in_proj_a)?,
16745                            b: gw(&w.in_proj_b)?,
16746                            out: gw(&w.out_proj)?,
16747                            conv1d: &w.conv1d,
16748                            a_log: &w.a_log,
16749                            dt_bias: &w.dt_bias,
16750                            norm: &w.norm,
16751                            nv: cfg.num_v_heads,
16752                            nk: cfg.num_k_heads,
16753                            dk: cfg.key_head_dim,
16754                            dv: cfg.value_head_dim,
16755                            kk: cfg.conv_kernel,
16756                            cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
16757                            preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
16758                        }
16759                    }
16760                    _ => return None,
16761                };
16762                // Gemma-4's sandwich norms and layer scalar (`gemma_batch`);
16763                // every other model hands the graph none, as before.
16764                let g4 = self.global_attn.is_some() || self.attn_v_norm;
16765                layers.push(crate::gpu::GraphLayer {
16766                    mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
16767                    input_norm: &lw.input_norm,
16768                    attn,
16769                    post_norm: &lw.post_norm,
16770                    attn_out_norm: if g4 {
16771                        lw.attn_out_norm.as_deref()
16772                    } else {
16773                        None
16774                    },
16775                    ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
16776                    layer_scale: if g4 { lw.layer_scale } else { None },
16777                    ffn: gffn,
16778                });
16779            }
16780            Some((layers, model?))
16781        })();
16782        let Some((layers, model)) = built else {
16783            {
16784                use std::sync::atomic::{AtomicBool, Ordering};
16785                static SAID: AtomicBool = AtomicBool::new(false);
16786                if !SAID.swap(true, Ordering::Relaxed) {
16787                    tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
16788                }
16789            }
16790            return crate::gpu::BatchGraphOutcome::Declined;
16791        };
16792        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
16793            eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
16794        }
16795        // SenseNova-Loop and Mage-VL: their q8_2f prompt chunks take the
16796        // batched int8 GEMM.
16797        #[cfg(feature = "gpu")]
16798        crate::gpu_wgpu::set_batch_q82_mm(self.mhc.is_some() || self.onevision2_prefill());
16799        // SenseNova-Loop image prompt: each row's absolute MRoPE angles.
16800        #[cfg(feature = "gpu")]
16801        let _mrope = self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| {
16802            let half = rd / 2;
16803            let mut tab = vec![0f32; k * 64];
16804            for (i, &p) in positions.iter().enumerate() {
16805                for j in 0..half.min(64) {
16806                    let a = pos.get(p).map_or(p as f32, |q| q[crate::sensenova::mrope_axis(j, sections)] as f32);
16807                    tab[i * 64 + j] = a * self.inv_freq[j];
16808                }
16809            }
16810            crate::gpu_wgpu::set_batch_mrope(Some(tab));
16811            struct Clear;
16812            impl Drop for Clear {
16813                fn drop(&mut self) {
16814                    crate::gpu_wgpu::set_batch_mrope(None);
16815                }
16816            }
16817            Clear
16818        });
16819        crate::gpu::forward_batch_graph(
16820            &model,
16821            self.graph_kv_id,
16822            &layers,
16823            &self.inv_freq,
16824            hiddens,
16825            nh,
16826            nkv,
16827            hd,
16828            rd,
16829            self.hidden_size,
16830            self.intermediate_size,
16831            positions,
16832            self.kv_cache.max_seq_len,
16833            gemma,
16834            self.rms_eps as f32,
16835            self.attn_scale,
16836            k,
16837            &(0..graph_end)
16838                .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
16839                .collect::<Vec<_>>(),
16840            self.o1_epoch,
16841            spec,
16842            layers_run,
16843        )
16844    }
16845
16846    /// Same, stopping after layer `upto` inclusive (routing probe φ).
16847    /// `CMF_DSV4_DRAFT_PROBE=1` — grade the draft against what the trunk goes on
16848    /// to produce. Off by default; it runs a whole draft per decoded token.
16849    fn draft_probe() -> bool {
16850        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16851        *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
16852    }
16853
16854    /// `CMF_DSV4_DRAFT_PROBE=1`: measure how much of the draft the trunk
16855    /// would have agreed with, WITHOUT verifying or rolling anything back.
16856    ///
16857    /// The number this produces decides the whole speculation design — at
16858    /// acceptance a, a block of B positions yields 1 + a + a² + ... tokens
16859    /// per trunk pass — so it is worth measuring before any of the machinery
16860    /// that would exploit it exists. Each draft is parked with the position
16861    /// it was made at, and graded as the real tokens arrive.
16862    /// `CMF_DSV4_SPEC=1` — the DeepSeek-V4 speculative decode: draft five
16863    /// on the card, verify them in one batched trunk pass, commit the
16864    /// accepted prefix, roll the rest back.
16865    #[cfg(feature = "gpu")]
16866    fn dsv4_spec_on() -> bool {
16867        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16868        *ON.get_or_init(|| {
16869            // Test-only runtime gate: model loading still performs the same
16870            // reservation and trunk packing, which gives rollback parity a
16871            // topology-identical non-speculative control arm.
16872            if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
16873                return v != "0";
16874            }
16875            // An explicit value is a diagnostic force/escape hatch.  With no
16876            // knob, speculation is eligible only when model loading reserved
16877            // its bounded pack.  On small q4tp cards the geometric reserve
16878            // gate deliberately leaves this at zero: trying to build DSpark
16879            // after the exact trunk filled VRAM is both slower and a device
16880            // OOM (measured on A40).
16881            std::env::var("CMF_DSV4_SPEC")
16882                .map(|v| v != "0")
16883                .unwrap_or_else(|_| {
16884                    crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
16885                })
16886        })
16887    }
16888
16889    /// One speculative round at the decode tip. `t_next` is the token the
16890    /// sampler just committed for `next_pos`. Returns the EXTRA accepted
16891    /// tokens (possibly none) and the new position, with `graph_logits`
16892    /// left holding the last accepted position's logits — exactly what the
16893    /// loop top expects. `None` means "speculate not this round": nothing
16894    /// was committed, the caller forwards normally.
16895    #[cfg(feature = "gpu")]
16896    fn dsv4_spec_step(
16897        &mut self,
16898        tip_token: u32,
16899        t_next: u32,
16900        next_pos: usize,
16901        max_extra: usize,
16902        drafted: &mut usize,
16903        accepted_ctr: &mut usize,
16904    ) -> Option<(Vec<u32>, usize)> {
16905        let t_all = std::time::Instant::now();
16906        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
16907            thread_local! {
16908                static LAST: std::cell::Cell<Option<std::time::Instant>> =
16909                    const { std::cell::Cell::new(None) };
16910            }
16911            LAST.with(|l| {
16912                if let Some(prev) = l.get() {
16913                    eprintln!(
16914                        "between rounds {:.1} ms",
16915                        prev.elapsed().as_secs_f64() * 1e3
16916                    );
16917                }
16918                l.set(Some(std::time::Instant::now()));
16919            });
16920        }
16921        if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16922            eprintln!("spec_step: enter pos={next_pos}");
16923        }
16924        let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
16925        let cfg = self.dsv4.as_ref().map(|b| b.2)?;
16926        // The draft state and its capture, armed exactly as the probe does.
16927        if self.dspark.is_none() {
16928            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16929            if t.is_empty() {
16930                return None;
16931            }
16932            crate::dsv4::dspark_arm(&t, cfg.dim);
16933            self.dspark = Some(crate::dsv4::DsparkState::new(
16934                self.dsv4_mtp.len(),
16935                &cfg,
16936                t.len(),
16937            ));
16938        }
16939        let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16940        let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
16941        if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16942            eprintln!("spec_step: the pack was not built (targets {targets:?})");
16943        }
16944        let pack = pack?;
16945        let block = crate::dsv4::dspark_block();
16946        let b_box = self.dsv4.as_mut()?;
16947        let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
16948        let ds = self.dspark.as_mut()?;
16949        // The tip's captures: either this token ran on a normal path that
16950        // filled the thread-local, or the previous spec round left them.
16951        let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
16952        if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
16953            if dbg {
16954                eprintln!("spec_step: no capture");
16955            }
16956            return None;
16957        }
16958        ds.have_hidden = true;
16959        let tip_pos = next_pos.checked_sub(1)?;
16960        let draft_started = std::time::Instant::now();
16961        let mut conf = Vec::new();
16962        let props = crate::dsv4::dspark_draft_gpu(
16963            g,
16964            &self.dsv4_mtp,
16965            &cfg,
16966            ds,
16967            pack,
16968            st.kv_id,
16969            tip_token,
16970            tip_pos,
16971            self.pool.as_deref(),
16972            &mut conf,
16973        );
16974        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
16975        *drafted += block;
16976        if props.is_empty() || props[0] != t_next {
16977            if dbg {
16978                eprintln!(
16979                    "spec_step: draft {} (props0={:?} t_next={t_next})",
16980                    if props.is_empty() {
16981                        "empty"
16982                    } else {
16983                        "miss"
16984                    },
16985                    props.first()
16986                );
16987            }
16988            return None;
16989        }
16990        // `fed[0]` is `t_next`, which the outer loop has already committed;
16991        // only `fed[1..]` become additional output tokens. Cap the verify
16992        // transaction itself to the caller's remaining output budget instead
16993        // of merely truncating the returned vector: otherwise the KV/state
16994        // would advance past `max_tokens` and a 64-token request could return
16995        // 66 tokens (and poison a reused session with two invisible steps).
16996        let mut k_verify = crate::dsv4::dspark_verify_k()
16997            .min(props.len())
16998            .min(max_extra.saturating_add(1));
16999        // Adaptive depth: positions the draft itself doubts are paid for on
17000        // every verify and delivered almost never (natural-text survival
17001        // [.67 .50 .29 .08 .04]). `CMF_DSPARK_CONF_MIN=p` trims the fed
17002        // prefix at the first proposal whose confidence drops below p; on
17003        // predictable text the confidences stay high and nothing changes.
17004        let conf_min = {
17005            static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
17006            *M.get_or_init(|| {
17007                std::env::var("CMF_DSPARK_CONF_MIN")
17008                    .ok()
17009                    .and_then(|v| v.parse().ok())
17010                    .unwrap_or(0.0)
17011            })
17012        };
17013        if conf_min > 0.0 && conf.len() >= props.len() {
17014            let mut keep = 1usize;
17015            while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
17016                keep += 1;
17017            }
17018            k_verify = k_verify.min(keep.max(2));
17019        }
17020        if k_verify < 2 {
17021            return None;
17022        }
17023        let mut fed = Vec::with_capacity(k_verify);
17024        fed.push(t_next);
17025        fed.extend_from_slice(&props[1..k_verify]);
17026        let mut argmax = Vec::new();
17027        let mut logits_all = Vec::new();
17028        let mut walked = Vec::new();
17029        let txn = crate::dsv4::dsv4_verify_chunk(
17030            g,
17031            layers,
17032            &cfg,
17033            st,
17034            &fed,
17035            next_pos,
17036            &self.inv_freq,
17037            self.pool.as_deref(),
17038            &targets,
17039            &mut argmax,
17040            &mut logits_all,
17041            &mut walked,
17042        );
17043        if txn.is_none() && dbg {
17044            eprintln!("spec_step: verify declined");
17045        }
17046        let txn = txn?;
17047        let spec_gpu_end = txn.gpu_end;
17048        let b = fed.len();
17049        let mut accepted = 1usize;
17050        while accepted < b && fed[accepted] == argmax[accepted - 1] {
17051            accepted += 1;
17052        }
17053        // `CMF_DSV4_SPEC_FORCE_REJECT=1` — accept nothing beyond the known
17054        // token, every round: the pure rollback exerciser. The output must
17055        // stay byte-identical to the plain walk; anything else is a
17056        // transaction bug, isolated from the acceptance logic.
17057        if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
17058            accepted = 1;
17059        }
17060        if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
17061            eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
17062        }
17063        let t_fin = std::time::Instant::now();
17064        if !crate::dsv4::dsv4_spec_finish(
17065            g,
17066            layers,
17067            &cfg,
17068            st,
17069            txn,
17070            accepted,
17071            &fed,
17072            &self.inv_freq,
17073            self.pool.as_deref(),
17074        ) {
17075            tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
17076            return None;
17077        }
17078        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
17079            eprintln!(
17080                "finish(k={accepted}): {:.1} ms",
17081                t_fin.elapsed().as_secs_f64() * 1e3
17082            );
17083        }
17084        *accepted_ctr += accepted - 1;
17085        // Captures per accepted token: device targets photographed by the
17086        // batch, host targets from the verify's own walk. The last one
17087        // becomes the new tip's draft input; every one owes the ring an
17088        // entry for its position.
17089        let (hc, dim) = (cfg.hc_mult, cfg.dim);
17090        // Complete-chain layers are photographed by the fused submission;
17091        // partial device layers overwrite that slot after exact host cold-
17092        // expert correction.  Thus every target in the contiguous device
17093        // prefix has a valid per-token capture.
17094        let dev_caps: Vec<usize> = targets
17095            .iter()
17096            .copied()
17097            .filter(|&t| t < spec_gpu_end)
17098            .collect();
17099        let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
17100        if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
17101            return None;
17102        }
17103        for t in 0..accepted {
17104            let tip = t + 1 == accepted;
17105            for (slot, &tl) in targets.iter().enumerate() {
17106                if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
17107                    let lo = (di * b + t) * hc * dim;
17108                    crate::dsv4::dspark_capture(
17109                        &caps_all[lo..lo + hc * dim],
17110                        &cfg,
17111                        slot,
17112                        &mut ds.main_hidden,
17113                    );
17114                } else if tip
17115                    && crate::dsv4::dspark_peek_slot(slot, dim, {
17116                        let lo = slot * dim;
17117                        &mut ds.main_hidden[lo..lo + dim]
17118                    })
17119                {
17120                    // The tip's host-layer captures are the walk's own
17121                    // per-layer notes — exact. (The walk that ran last ended
17122                    // on exactly this token, on both the accept-all and the
17123                    // rollback path.)
17124                } else {
17125                    // Intermediate tokens: the post-tail state stands in for
17126                    // the per-layer capture on host targets below the last
17127                    // layer. Ring-entry quality only; the tip is exact.
17128                    crate::dsv4::dspark_capture(
17129                        &walked[t * hc * dim..(t + 1) * hc * dim],
17130                        &cfg,
17131                        slot,
17132                        &mut ds.main_hidden,
17133                    );
17134                }
17135            }
17136            crate::dsv4::dspark_ring_append(
17137                g,
17138                &self.dsv4_mtp,
17139                &cfg,
17140                ds,
17141                next_pos + t,
17142                self.pool.as_deref(),
17143            );
17144        }
17145        let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
17146        self.graph_logits = Some(row);
17147        // The speculative loop never runs the probe, so the trunk tally has
17148        // no other place to cycle. Armed only when someone asked for the
17149        // dump; the host tail is the only tallying path here, which is
17150        // precisely the population a partial pack would serve.
17151        if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
17152            crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
17153            crate::dsv4::pick_tally_arm();
17154        }
17155        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
17156            eprintln!(
17157                "spec_step total {:.1} ms (k={accepted})",
17158                t_all.elapsed().as_secs_f64() * 1e3
17159            );
17160        }
17161        Some((fed[1..accepted].to_vec(), next_pos + accepted))
17162    }
17163
17164    fn dspark_probe(&mut self, position: usize, token_id: u32) {
17165        if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
17166            return;
17167        }
17168        // What the trunk just routed to, for this token.
17169        let trunk_now = crate::dsv4::pick_tally_take();
17170        crate::dsv4::trunk_freq_note(&trunk_now);
17171        if !trunk_now.is_empty() {
17172            self.dspark_trunk_picks.push(trunk_now);
17173            let keep = crate::dsv4::dspark_block();
17174            if self.dspark_trunk_picks.len() > keep {
17175                self.dspark_trunk_picks.remove(0);
17176            }
17177        }
17178        // Grade whatever is waiting: the token just decoded sits at
17179        // `position`, so it answers the draft made at `position - 1 - i`.
17180        for p in std::mem::take(&mut self.dspark_pending) {
17181            let Some(i) = position.checked_sub(p.0 + 1) else {
17182                continue;
17183            };
17184            let mut p = p;
17185            if i < p.1.len() {
17186                if p.2 && p.1[i] == token_id {
17187                    p.3 = i + 1;
17188                } else {
17189                    p.2 = false;
17190                }
17191                if i + 1 < p.1.len() {
17192                    self.dspark_pending.push(p);
17193                    continue;
17194                }
17195            }
17196            self.dspark_hist.push(p.3);
17197            self.dspark_real.push(token_id);
17198        }
17199        let Some(b) = &mut self.dsv4 else { return };
17200        let (g, layers, cfg) = (&b.0, &b.1, b.2);
17201        let n_layers = layers.len();
17202        if self.dspark.is_none() {
17203            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
17204            if t.is_empty() {
17205                return;
17206            }
17207            eprintln!(
17208                "DSpark: capture from layers {t:?}, block {}",
17209                crate::dsv4::dspark_block()
17210            );
17211            crate::dsv4::dspark_arm(&t, cfg.dim);
17212            self.dspark = Some(crate::dsv4::DsparkState::new(
17213                self.dsv4_mtp.len(),
17214                &cfg,
17215                t.len(),
17216            ));
17217        }
17218        let ds = self.dspark.as_mut().unwrap();
17219        if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
17220            return; // this token ran on a path that captures nothing
17221        }
17222        let mut conf = Vec::new();
17223        crate::dsv4::pick_tally_arm();
17224        // The trunk has already consumed the adaptive VRAM budget. Until the
17225        // draft owns an explicit bounded device pack, its tensors are an
17226        // out-of-core CPU/disk tier by contract: never let per-op probes try
17227        // to squeeze another multi-gigabyte MTP expert cache onto the card.
17228        let draft_started = std::time::Instant::now();
17229        #[cfg(feature = "gpu")]
17230        let gpu_draft = crate::dsv4::dspark_gpu_on();
17231        #[cfg(not(feature = "gpu"))]
17232        let gpu_draft = false;
17233        let props = if gpu_draft {
17234            #[cfg(feature = "gpu")]
17235            {
17236                let kv_id = b.3.kv_id;
17237                match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
17238                    Some(pk) => crate::dsv4::dspark_draft_gpu(
17239                        g,
17240                        &self.dsv4_mtp,
17241                        &cfg,
17242                        ds,
17243                        pk,
17244                        kv_id,
17245                        token_id,
17246                        position,
17247                        self.pool.as_deref(),
17248                        &mut conf,
17249                    ),
17250                    None => Vec::new(),
17251                }
17252            }
17253            #[cfg(not(feature = "gpu"))]
17254            Vec::new()
17255        } else {
17256            crate::gpu::cpu_scope(|| {
17257                crate::dsv4::dspark_draft(
17258                    g,
17259                    &self.dsv4_mtp,
17260                    &cfg,
17261                    ds,
17262                    token_id,
17263                    position,
17264                    self.pool.as_deref(),
17265                    &mut conf,
17266                )
17267            })
17268        };
17269        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
17270        let draft_picks = crate::dsv4::pick_tally_take();
17271        crate::dsv4::dspark_freq_note(&draft_picks);
17272        // Re-arm for the NEXT trunk token; the probe runs after the forward,
17273        // so this is the only place that can.
17274        crate::dsv4::pick_tally_arm();
17275        if !props.is_empty() {
17276            // Two ratios, side by side: what a batched verify over the trunk
17277            // would read against what it asks for, and the same for the
17278            // draft's three stages. Near 1.0 means a batch amortises nothing.
17279            let (tu, tt) = {
17280                let flat: Vec<(usize, Vec<usize>)> = self
17281                    .dspark_trunk_picks
17282                    .iter()
17283                    .flat_map(|v| v.iter().cloned())
17284                    .collect();
17285                // Per layer, across the window of tokens.
17286                let mut per: std::collections::HashMap<usize, Vec<usize>> =
17287                    std::collections::HashMap::new();
17288                for (li, picks) in flat {
17289                    per.entry(li).or_default().extend(picks);
17290                }
17291                let n = per.len().max(1);
17292                let mut u = 0usize;
17293                let mut t = 0usize;
17294                for (_, v) in per {
17295                    t += v.len();
17296                    u += v.iter().collect::<std::collections::HashSet<_>>().len();
17297                }
17298                (u / n, t / n)
17299            };
17300            let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
17301            self.dspark_exp.push((tu, tt, du, dt));
17302            self.dspark_pending.push((position, props, true, 0));
17303        }
17304        if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
17305            let n = self.dspark_hist.len() as f32;
17306            let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
17307            let block = crate::dsv4::dspark_block();
17308            let mut at = vec![0usize; block + 1];
17309            for &k in &self.dspark_hist {
17310                at[k] += 1;
17311            }
17312            // Prefix survival: S_i = P(the first i positions all held).
17313            let mut surv = Vec::with_capacity(block);
17314            for i in 1..=block {
17315                let k = at[i..].iter().sum::<usize>() as f32 / n;
17316                surv.push(format!("{k:.2}"));
17317            }
17318            let distinct = self
17319                .dspark_real
17320                .iter()
17321                .collect::<std::collections::HashSet<_>>()
17322                .len();
17323            let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
17324                (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
17325            });
17326            let m = self.dspark_exp.len().max(1);
17327            eprintln!(
17328                "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
17329                 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
17330                self.dspark_hist.len(),
17331                mean + 1.0,
17332                surv.join(" ")
17333            );
17334            eprintln!(
17335                "DSpark: {distinct} distinct tokens of {} (degeneracy), \
17336                 trunk experts {}/{} per layer over {block} tokens, \
17337                 draft {}/{} per block, draft {:.2} ms/block",
17338                self.dspark_real.len(),
17339                tu / m,
17340                tt / m,
17341                du / m,
17342                dt / m,
17343                self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
17344            );
17345        }
17346    }
17347
17348    fn forward_layers_upto(
17349        &mut self,
17350        hidden: &[f32],
17351        position: usize,
17352        task_mask: Option<&TaskMask>,
17353        upto: Option<usize>,
17354    ) -> Vec<f32> {
17355        // In-process multi-GPU: each segment runs pinned to its card,
17356        // and the only thing crossing the boundary is one hidden vector
17357        // that never leaves this address space. Same layer split the
17358        // network mode does, minus the second process, the socket, the
17359        // serialization and the dir_hash handshake.
17360        if let Some(plan) = self.gpu_plan.clone() {
17361            if upto.is_none() && plan.len() > 1 {
17362                let mut h = hidden.to_vec();
17363                for &(dev, from, upto_incl) in plan.iter() {
17364                    h = crate::gpu::with_device(dev, || {
17365                        self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
17366                    });
17367                }
17368                return h;
17369            }
17370        }
17371        self.forward_layers_span(hidden, position, task_mask, 0, upto)
17372    }
17373
17374    /// Split this pipeline's layer stack across local GPUs: segment i
17375    /// runs on `devices[i]`. Contiguous and even by layer count — the
17376    /// VRAM-weighted planner is the next step, and an uneven card pair
17377    /// is why it will be needed. `None` clears the plan.
17378    pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
17379        self.set_gpu_plan_at(devices, None)
17380    }
17381
17382    /// The same, with an explicit first boundary (`--peer-split`): card
17383    /// 0 takes layers `[0..at)`, the rest split what remains. Uneven
17384    /// cards, or an attention-heavy head, are why this knob exists.
17385    pub fn set_gpu_plan_at(
17386        &mut self,
17387        devices: Option<&[usize]>,
17388        at: Option<usize>,
17389    ) -> Result<(), String> {
17390        let Some(devs) = devices.filter(|d| d.len() > 1) else {
17391            self.gpu_plan = None;
17392            return Ok(());
17393        };
17394        self.split_supported()?;
17395        let n = self.num_layers;
17396        if devs.len() > n {
17397            return Err(format!("{} devices for {n} layers", devs.len()));
17398        }
17399        if let Some(k) = at {
17400            if k == 0 || k >= n {
17401                return Err(format!("split at {k}: the model has {n} layers"));
17402            }
17403            if devs.len() == 2 {
17404                self.gpu_plan = Some(std::sync::Arc::new(vec![
17405                    (devs[0], 0, k - 1),
17406                    (devs[1], k, n - 1),
17407                ]));
17408                return Ok(());
17409            }
17410            return Err(format!(
17411                "an explicit split point takes exactly 2 devices, got {}",
17412                devs.len()
17413            ));
17414        }
17415        let per = n.div_ceil(devs.len());
17416        let mut plan = Vec::with_capacity(devs.len());
17417        let mut from = 0usize;
17418        for &d in devs {
17419            if from >= n {
17420                break;
17421            }
17422            let upto = (from + per - 1).min(n - 1);
17423            plan.push((d, from, upto));
17424            from = upto + 1;
17425        }
17426        self.gpu_plan = Some(std::sync::Arc::new(plan));
17427        Ok(())
17428    }
17429
17430    /// The active in-process split, if any: (device, first layer, last).
17431    pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
17432        self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
17433    }
17434
17435    /// Layer span [from ..= upto] (upto None = last layer): the building
17436    /// block the network pipeline-split rides on. `from > 0` skips the
17437    /// arch escape hatches (the pub `forward_span` refuses those archs
17438    /// first) and the whole-token graph — the plain per-layer loop is
17439    /// the canonical executor for a partial stack.
17440    fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
17441        if let Some(x) = t.as_f32() {
17442            return x.to_vec();
17443        }
17444        let mut out = vec![0.0; t.rows() * t.cols()];
17445        for r in 0..t.rows() {
17446            t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
17447        }
17448        out
17449    }
17450
17451    fn embryo_resident_eligible(&self) -> bool {
17452        // One mixer family per file: vmf_phase (kind 0/1) or
17453        // gated_delta_net (kind 4); the anchors are full (2) or bounded (3).
17454        if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
17455            || self.num_layers != self.physical_layers
17456            || self.loop_final_norm
17457            || self.weights.layers.len() != self.num_layers
17458            || self.head_clusters.is_none()
17459            || self.final_softcap.is_some()
17460            || self.logit_multiplier.is_some()
17461            || self.attn_softcap != 0.0
17462            || self.mtp.is_some()
17463            || self.per_token_stack()
17464            || self.dsv4.is_some()
17465            || self.dsv41.is_some()
17466            || self.qwen4_exp.is_some()
17467            // Dynamic routing swaps FFN weights mid-sequence under the
17468            // packed graph; a blend has no single overlay. A STATIC skill
17469            // (`from_model_with_skill`) is fine: the pack reads the live
17470            // `weights.layers[*].ffn`, i.e. the skill's tensors, and a
17471            // later `set_active_skill` change drops the pack
17472            // (`invalidate_for_weight_change`).
17473            || self.dyn_router.is_some()
17474            || self.dyn_phi_layer.is_some()
17475            || self.dyn_blend_loaded
17476            || self.o1_cfg.is_some()
17477            || self.swa.is_some()
17478            || self.sliding_layers.is_some()
17479            || self.global_attn.is_some()
17480            || self.attention_heads_per_layer.is_some()
17481            || self.attn_v_norm
17482            || self
17483                .kv_cache
17484                .layers
17485                .iter()
17486                .any(|l| l.mode != crate::kv_cache::KvMode::F32)
17487            || self.rope_scale != 1.0
17488            || self.rope_scale_local != 1.0
17489            || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
17490            || self.hidden_size == 0
17491            || self.hidden_size > 1024
17492            || self.intermediate_size > 1024
17493            || self.num_heads == 0
17494            || self.num_kv_heads == 0
17495            || self.num_heads % self.num_kv_heads != 0
17496            || self.num_heads.saturating_mul(self.head_dim) > 1024
17497            || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
17498            || self.vocab_size == 0
17499            || self.kv_cache.max_seq_len == 0
17500            || self.rotary_dim == 0
17501            || self.rotary_dim > self.head_dim
17502            || self.rotary_dim % 2 != 0
17503            || self.inv_freq.len() < self.rotary_dim / 2
17504        {
17505            return false;
17506        }
17507        // The resident shader is deliberately an f32 profile.  Dequantizing
17508        // a Q4/Q8 tensor into the packed buffer would silently change the
17509        // operator relative to the CPU quantized path, so quantized CMFs
17510        // retain the exact ordinary executor instead of claiming parity.
17511        // Measured consequence (RTX PRO 4000, S4 bounded export requantized
17512        // with `cortiq requant --quant q4tp-quantize`): `eligible=false`,
17513        // the generic wgpu whole-token graph refuses too, and the per-op
17514        // path decodes at ~73 tok/s against ~200 tok/s on the CPU q4tp
17515        // path — a q4tp Embryo-O1 file is a CPU artifact today; the
17516        // resident graph serves the f32 export.
17517        if self.weights.lm_head.as_f32().is_none()
17518            || self.weights.embed_tokens.as_f32().is_none()
17519            || self.weights.lm_head.rows() < self.vocab_size
17520            || self.weights.lm_head.cols() != self.hidden_size
17521            || self.weights.embed_tokens.rows() < self.vocab_size
17522            || self.weights.embed_tokens.cols() != self.hidden_size
17523            || self.weights.final_norm.len() != self.hidden_size
17524        {
17525            return false;
17526        }
17527        if let Some(cfg) = self.vmf_cfg {
17528            if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
17529                || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
17530                || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
17531                || cfg.state_len() == 0
17532            {
17533                return false;
17534            }
17535        }
17536        if let Some(g) = self.gdn_cfg {
17537            // The resident GDN kernels (gpu_wgpu.rs `embryo_core_gdn_*`):
17538            // fused projection ≤ 2048 rows, nv·dv ≤ 1024, dk ≤ 128 lanes,
17539            // dv ≤ 256 lanes in vec4 rows, SiLU output gate (the Embryo
17540            // export), same rms eps as the stack.
17541            if g.num_v_heads == 0
17542                || g.num_k_heads == 0
17543                || g.num_v_heads % g.num_k_heads != 0
17544                || g.key_head_dim == 0
17545                || g.key_head_dim > 128
17546                || g.value_head_dim == 0
17547                || g.value_head_dim > 256
17548                || g.value_head_dim % 4 != 0
17549                || g.conv_kernel == 0
17550                || g.num_v_heads > 512
17551                || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
17552                || g.conv_dim() > 2048
17553                || g.conv_dim() % 4 != 0
17554                || g.hidden_size != self.hidden_size
17555                || g.output_gate_sigmoid
17556                || g.rms_eps != self.rms_eps
17557                || g.state_len() == 0
17558            {
17559                return false;
17560            }
17561        }
17562        let mut full_seen = false;
17563        for lw in &self.weights.layers {
17564            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
17565                return false;
17566            }
17567            match &lw.attn {
17568                AttnKind::LinearGdn(w) => {
17569                    let Some(g) = self.gdn_cfg else {
17570                        return false;
17571                    };
17572                    let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
17573                    if w.in_proj_qkv.rows() != g.conv_dim()
17574                        || w.in_proj_qkv.cols() != self.hidden_size
17575                        || w.in_proj_qkv.as_f32().is_none()
17576                        || w.in_proj_z.rows() != nv * dv
17577                        || w.in_proj_z.cols() != self.hidden_size
17578                        || w.in_proj_z.as_f32().is_none()
17579                        || w.in_proj_a.rows() != nv
17580                        || w.in_proj_a.cols() != self.hidden_size
17581                        || w.in_proj_a.as_f32().is_none()
17582                        || w.in_proj_b.rows() != nv
17583                        || w.in_proj_b.cols() != self.hidden_size
17584                        || w.in_proj_b.as_f32().is_none()
17585                        || w.conv1d.len() != g.conv_dim() * kk
17586                        || w.a_log.len() != nv
17587                        || w.dt_bias.len() != nv
17588                        || w.norm.len() != dv
17589                        || w.out_proj.rows() != self.hidden_size
17590                        || w.out_proj.cols() != nv * dv
17591                        || w.out_proj.as_f32().is_none()
17592                    {
17593                        return false;
17594                    }
17595                }
17596                AttnKind::Linear(w) => {
17597                    let Some(cfg) = self.vmf_cfg else {
17598                        return false;
17599                    };
17600                    if w.thq.rows() != cfg.num_heads * cfg.nphase
17601                        || w.thq.cols() != self.hidden_size
17602                        || w.thq.as_f32().is_none()
17603                        || w.thk.rows() != cfg.num_heads * cfg.nphase
17604                        || w.thk.cols() != self.hidden_size
17605                        || w.thk.as_f32().is_none()
17606                        || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
17607                        || w.v_proj.cols() != self.hidden_size
17608                        || w.v_proj.as_f32().is_none()
17609                        || w.out_proj.rows() != self.hidden_size
17610                        || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
17611                        || w.out_proj.as_f32().is_none()
17612                        || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
17613                    {
17614                        return false;
17615                    }
17616                    if let Some((kg, kb)) = &w.k_gate {
17617                        if kg.rows() != cfg.num_heads
17618                            || kg.cols() != self.hidden_size
17619                            || kg.as_f32().is_none()
17620                            || kb.len() != cfg.num_heads
17621                        {
17622                            return false;
17623                        }
17624                    }
17625                    if let Some(conv) = &w.conv {
17626                        if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
17627                            return false;
17628                        }
17629                    }
17630                }
17631                AttnKind::Full {
17632                    wq,
17633                    wk,
17634                    wv,
17635                    wo,
17636                    q_norm,
17637                    k_norm,
17638                    output_gate,
17639                    softplus_gate,
17640                    bias,
17641                } => {
17642                    if full_seen
17643                        || q_norm.is_some()
17644                        || k_norm.is_some()
17645                        || *output_gate
17646                        || softplus_gate.is_some()
17647                        || bias.is_some()
17648                        || wq.as_f32().is_none()
17649                        || wk.as_f32().is_none()
17650                        || wv.as_f32().is_none()
17651                        || wo.as_f32().is_none()
17652                        || wq.rows() != self.num_heads * self.head_dim
17653                        || wk.rows() != self.num_kv_heads * self.head_dim
17654                        || wv.rows() != self.num_kv_heads * self.head_dim
17655                        || wq.cols() != self.hidden_size
17656                        || wk.cols() != self.hidden_size
17657                        || wv.cols() != self.hidden_size
17658                        || wo.rows() != self.hidden_size
17659                        || wo.cols() != self.num_heads * self.head_dim
17660                    {
17661                        return false;
17662                    }
17663                    full_seen = true;
17664                }
17665                AttnKind::Bounded(w) => {
17666                    // The resident bounded attend walks S + W keys in
17667                    // 256-lane tiles (online softmax), any window the format
17668                    // allows; per-group windows come from header word 22.
17669                    let Some(ac) = self.anchor_core.as_ref() else {
17670                        return false;
17671                    };
17672                    if self.bounded_rope.is_none()
17673                        || w.window != ac.window
17674                        || w.sink != ac.sink
17675                        || w.group_windows != ac.group_windows
17676                        || w.window == 0
17677                        || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
17678                        || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
17679                        || w.wq.as_f32().is_none()
17680                        || w.wk.as_f32().is_none()
17681                        || w.wv.as_f32().is_none()
17682                        || w.wo.as_f32().is_none()
17683                        || w.wq.rows() != self.num_heads * self.head_dim
17684                        || w.wk.rows() != self.num_kv_heads * self.head_dim
17685                        || w.wv.rows() != self.num_kv_heads * self.head_dim
17686                        || w.wq.cols() != self.hidden_size
17687                        || w.wk.cols() != self.hidden_size
17688                        || w.wv.cols() != self.hidden_size
17689                        || w.wo.rows() != self.hidden_size
17690                        || w.wo.cols() != self.num_heads * self.head_dim
17691                    {
17692                        return false;
17693                    }
17694                }
17695                _ => return false,
17696            }
17697            match &lw.ffn {
17698                FfnKind::Dense(d) => {
17699                    if d.act != Act::Silu
17700                        || !d.segs.is_empty()
17701                        || d.gate_proj.as_f32().is_none()
17702                        || d.up_proj.as_f32().is_none()
17703                        || d.down_proj.as_f32().is_none()
17704                        || d.gate_proj.rows() != self.intermediate_size
17705                        || d.gate_proj.cols() != self.hidden_size
17706                        || d.up_proj.rows() != self.intermediate_size
17707                        || d.up_proj.cols() != self.hidden_size
17708                        || d.down_proj.rows() != self.hidden_size
17709                        || d.down_proj.cols() != self.intermediate_size
17710                    {
17711                        return false;
17712                    }
17713                }
17714                FfnKind::Moe(m) => {
17715                    if m.resonance.is_none()
17716                        || m.top_k != 1
17717                        || m.router_sigmoid
17718                        || !m.norm_topk_prob
17719                        || m.expert_bias.is_some()
17720                        || m.routed_scaling != 1.0
17721                        || m.route_tau.is_some()
17722                        || m.shared.is_none()
17723                        || m.mask.is_some()
17724                        || m.per_expert_scale.is_some()
17725                        || m.router_input_norm
17726                        || m.experts.is_empty()
17727                        || m.experts.len() > 8
17728                    {
17729                        return false;
17730                    }
17731                    let r = m.resonance.as_ref().unwrap();
17732                    if r.mu.len() != m.experts.len() * self.hidden_size
17733                        || r.bias.len() != m.experts.len()
17734                        || r.u.len() != m.experts.len() * r.k * self.hidden_size
17735                        || r.k > 128
17736                    {
17737                        return false;
17738                    }
17739                    let Some((shared, gate)) = &m.shared else {
17740                        return false;
17741                    };
17742                    if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
17743                        return false;
17744                    }
17745                    if shared.gate_proj.as_f32().is_none()
17746                        || shared.up_proj.as_f32().is_none()
17747                        || shared.down_proj.as_f32().is_none()
17748                        || shared.gate_proj.rows() != self.intermediate_size
17749                        || shared.gate_proj.cols() != self.hidden_size
17750                        || shared.up_proj.rows() != self.intermediate_size
17751                        || shared.up_proj.cols() != self.hidden_size
17752                        || shared.down_proj.rows() != self.hidden_size
17753                        || shared.down_proj.cols() != self.intermediate_size
17754                    {
17755                        return false;
17756                    }
17757                    for e in &m.experts {
17758                        if e.act != Act::Silu
17759                            || !e.segs.is_empty()
17760                            || e.gate_proj.as_f32().is_none()
17761                            || e.up_proj.as_f32().is_none()
17762                            || e.down_proj.as_f32().is_none()
17763                            || e.gate_proj.rows() != self.intermediate_size
17764                            || e.gate_proj.cols() != self.hidden_size
17765                            || e.up_proj.rows() != self.intermediate_size
17766                            || e.up_proj.cols() != self.hidden_size
17767                            || e.down_proj.rows() != self.hidden_size
17768                            || e.down_proj.cols() != self.intermediate_size
17769                        {
17770                            return false;
17771                        }
17772                    }
17773                }
17774                FfnKind::DenseMoe(_) => return false,
17775            }
17776            if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
17777                return false;
17778            }
17779        }
17780        if full_seen && self.anchor_core.is_some() {
17781            return false;
17782        }
17783        full_seen || self.num_layers > 0
17784    }
17785
17786    /// The resident Embryo graph is the owner of this pipeline's forward:
17787    /// the same gate `forward_layers_span` applies before handing a token
17788    /// to `forward_embryo_graph` (both graph phases on, the explicit
17789    /// opt-in, a wgpu device, no earlier refusal, an eligible stack).
17790    fn embryo_resident_wanted(&self) -> bool {
17791        crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17792            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17793            && matches!(
17794                std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17795                Ok("1") | Ok("parallel")
17796            )
17797            && crate::gpu::enabled_here()
17798            && !self.graph_refused()
17799            && self.embryo_resident_eligible()
17800    }
17801
17802    /// Chunked prefill on the resident graph: `ids` from `start` in
17803    /// chunks of `EMBRYO_CHUNK_MAX`, one submit each, projections/FFN as
17804    /// chunk GEMMs and the recurrent layers walked in time on the device.
17805    /// Returns the last position's logits when the whole span ran there.
17806    /// `None` = refused before any device work (the per-position path
17807    /// takes the span).  `CMF_EMBRYO_CHUNK=0` keeps the per-position
17808    /// prefill (A/B and the parity reference).
17809    fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
17810        if ids.len() < 2
17811            || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
17812            || !self.embryo_resident_wanted()
17813        {
17814            return None;
17815        }
17816        let model = self.ensure_embryo_graph()?;
17817        let cmax = std::env::var("CMF_EMBRYO_CHUNK")
17818            .ok()
17819            .and_then(|v| v.parse::<usize>().ok())
17820            .filter(|&v| v >= 1)
17821            .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
17822            .min(crate::gpu::EMBRYO_CHUNK_MAX);
17823        let hs = self.hidden_size;
17824        let n = ids.len();
17825        let mut pos = start;
17826        let mut last = None;
17827        let mut rows = Vec::with_capacity(cmax * hs);
17828        while pos < n {
17829            let end = (pos + cmax).min(n);
17830            rows.clear();
17831            for &id in &ids[pos..end] {
17832                rows.extend_from_slice(&self.embed_single(id));
17833            }
17834            let mut lg = Vec::new();
17835            if !crate::gpu::forward_embryo_graph_chunk(
17836                &model,
17837                self.graph_kv_id,
17838                &rows,
17839                pos,
17840                end - pos,
17841                &mut lg,
17842            ) {
17843                if pos == start {
17844                    if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17845                        eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
17846                    }
17847                    return None;
17848                }
17849                // The device sequence advanced through the earlier chunks;
17850                // a host continuation would mix two owners of the state.
17851                // Fail this sequence alone, leaving no stale state or key.
17852                self.kv_cache.clear();
17853                self.clear_history();
17854                crate::gpu::graph_kv_reset(self.graph_kv_id);
17855                panic!(
17856                    "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
17857                );
17858            }
17859            last = Some(lg);
17860            pos = end;
17861        }
17862        if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17863            eprintln!(
17864                "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
17865                n - start,
17866                (n - start).div_ceil(cmax)
17867            );
17868        }
17869        last
17870    }
17871
17872    fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
17873        if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
17874            const UMAX: u32 = u32::MAX;
17875            const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
17876            const REC: usize = 64;
17877            struct Pack {
17878                data: Vec<f32>,
17879            }
17880            impl Pack {
17881                fn put(&mut self, x: &[f32]) -> u32 {
17882                    if x.is_empty() {
17883                        return u32::MAX;
17884                    }
17885                    let off = self.data.len();
17886                    self.data.extend_from_slice(x);
17887                    off as u32
17888                }
17889            }
17890            // The mixer family of the file: vmf_phase geometry fills the
17891            // phase header words, gated_delta_net fills words 24..29.  A
17892            // file has exactly one linear core, so at most one is live.
17893            let vmf = self.vmf_cfg;
17894            let gdn = self.gdn_cfg;
17895            let mut pack = Pack { data: Vec::new() };
17896            let mut meta = vec![0u32; HEADER];
17897            meta[0] = self.hidden_size as u32;
17898            meta[1] = self.intermediate_size as u32;
17899            meta[2] = self.vocab_size as u32;
17900            meta[3] = self.num_layers as u32;
17901            meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
17902            meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
17903            meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
17904            if let Some(g) = gdn {
17905                meta[24] = g.num_v_heads as u32;
17906                meta[25] = g.num_k_heads as u32;
17907                meta[26] = g.key_head_dim as u32;
17908                meta[27] = g.value_head_dim as u32;
17909                meta[28] = g.conv_kernel as u32;
17910                meta[29] = g.conv_dim() as u32;
17911            }
17912            meta[7] = self.num_heads as u32;
17913            meta[8] = self.num_kv_heads as u32;
17914            meta[9] = self.head_dim as u32;
17915            meta[10] = self.kv_cache.max_seq_len as u32;
17916            let clusters = self.head_clusters.as_ref().unwrap();
17917            let cluster_count = clusters.len() / self.hidden_size;
17918            if clusters.len() % self.hidden_size != 0
17919                || cluster_count == 0
17920                || cluster_count > 1024
17921                || self.vocab_size % cluster_count != 0
17922                || self.weights.lm_head.rows() < self.vocab_size
17923                || self.weights.final_norm.len() != self.hidden_size
17924            {
17925                return None;
17926            }
17927            meta[11] = cluster_count as u32;
17928            meta[12] = (self.vocab_size / cluster_count) as u32;
17929            meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
17930            meta[16] = self.rotary_dim as u32;
17931            meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
17932            meta[19] = (self.rms_eps as f32).to_bits();
17933            let max_conv = self
17934                .weights
17935                .layers
17936                .iter()
17937                .filter_map(|lw| match &lw.attn {
17938                    AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
17939                    _ => None,
17940                })
17941                .max()
17942                .unwrap_or(1);
17943            // One state slot per recurrent layer: the phase state plus its
17944            // hidden-wide conv ring, or the GDN record `[conv ring | S]`
17945            // (`GdnCfg::state_len`).  A file carries one mixer family, so
17946            // the stride is exactly that family's record and the device
17947            // state buffer equals the header's recurrent bytes.
17948            let phase_stride = vmf
17949                .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
17950                .unwrap_or(0);
17951            let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
17952            let state_stride = phase_stride.max(gdn_stride);
17953            // Bounded genome: the KV plane of an anchor is its ring
17954            // `[kvh][W][hd]` K + V, and only anchors own one.  Legacy full
17955            // anchors keep the `max_seq` planes indexed by layer.
17956            let bounded = self.anchor_core.clone();
17957            let (anchor_window, anchor_sink) = bounded
17958                .as_ref()
17959                .map(|ac| (ac.window, ac.sink))
17960                .unwrap_or((0, 0));
17961            let kv_stride = if bounded.is_some() {
17962                2usize
17963                    .saturating_mul(self.num_kv_heads)
17964                    .saturating_mul(anchor_window)
17965                    .saturating_mul(self.head_dim)
17966            } else {
17967                2usize
17968                    .saturating_mul(self.num_kv_heads)
17969                    .saturating_mul(self.kv_cache.max_seq_len)
17970                    .saturating_mul(self.head_dim)
17971            };
17972            meta[14] = state_stride as u32;
17973            meta[15] = kv_stride as u32;
17974            meta[18] = anchor_window as u32;
17975            meta[20] = anchor_sink as u32;
17976            meta[21] = match &self.bounded_rope {
17977                Some(rope) => {
17978                    // [W][half] cos then [W][half] sin, one contiguous table.
17979                    let off = pack.put(&rope.cos);
17980                    let _ = pack.put(&rope.sin);
17981                    off
17982                }
17983                None => UMAX,
17984            };
17985            // swa_sink_v2: one window per KV group (f32 in the pack; exact
17986            // for any window the format allows); UMAX = every group `W`.
17987            meta[22] = match bounded.as_ref().filter(|ac| !ac.group_windows.is_empty()) {
17988                Some(ac) => {
17989                    let gw: Vec<f32> = ac.group_windows.iter().map(|&w| w as f32).collect();
17990                    pack.put(&gw)
17991                }
17992                None => UMAX,
17993            };
17994            let mut full_seen = false;
17995            let mut bounded_seen = 0usize;
17996            // Recurrent state slots belong to mixer layers only (phase or
17997            // GDN): an anchor owns a ring, not a state stride, so the
17998            // device state buffer is exactly the header's recurrent bytes.
17999            let mut phase_seen = 0usize;
18000            let mut gdn_seen = 0usize;
18001            for (li, lw) in self.weights.layers.iter().enumerate() {
18002                let base = meta.len();
18003                meta.resize(base + REC, UMAX);
18004                meta[base] = match &lw.attn {
18005                    AttnKind::Linear(w) if w.phase_delta => 1,
18006                    AttnKind::Linear(_) => 0,
18007                    AttnKind::Full { .. } => 2,
18008                    AttnKind::Bounded(_) => 3,
18009                    AttnKind::LinearGdn(_) => 4,
18010                    _ => UMAX,
18011                };
18012                meta[base + 1] = pack.put(&lw.input_norm);
18013                meta[base + 2] = pack.put(&lw.post_norm);
18014                meta[base + 25] = match &lw.attn {
18015                    AttnKind::Linear(_) => {
18016                        let off = ((phase_seen + gdn_seen) * state_stride) as u32;
18017                        phase_seen += 1;
18018                        off
18019                    }
18020                    AttnKind::LinearGdn(_) => {
18021                        let off = ((phase_seen + gdn_seen) * state_stride) as u32;
18022                        gdn_seen += 1;
18023                        off
18024                    }
18025                    _ => UMAX,
18026                };
18027                match &lw.attn {
18028                    AttnKind::LinearGdn(w) => {
18029                        // Layer record words 56..63 + 29, as the resident
18030                        // kernels read them (gpu_wgpu.rs `embryo_core_gdn_*`).
18031                        meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
18032                        meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
18033                        meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
18034                        meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
18035                        meta[base + 60] = pack.put(&w.conv1d);
18036                        meta[base + 61] = pack.put(&w.a_log);
18037                        meta[base + 62] = pack.put(&w.dt_bias);
18038                        meta[base + 63] = pack.put(&w.norm);
18039                        meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
18040                        meta[base + 24] = 0;
18041                    }
18042                    AttnKind::Linear(w) => {
18043                        meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
18044                        meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
18045                        meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
18046                        meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
18047                        let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
18048                        meta[base + 7] = pack.put(&decay);
18049                        if let Some((kg, kb)) = &w.k_gate {
18050                            meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
18051                            meta[base + 9] = pack.put(kb);
18052                        }
18053                        if let Some(conv) = &w.conv {
18054                            meta[base + 10] = pack.put(conv);
18055                            meta[base + 24] = (conv.len() / self.hidden_size) as u32;
18056                        } else {
18057                            meta[base + 24] = 0;
18058                        }
18059                    }
18060                    AttnKind::Full { wq, wk, wv, wo, .. } => {
18061                        full_seen = true;
18062                        meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
18063                        meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
18064                        meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
18065                        meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
18066                        meta[base + 26] = (li * kv_stride) as u32;
18067                    }
18068                    AttnKind::Bounded(w) => {
18069                        meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
18070                        meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
18071                        meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
18072                        meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
18073                        // Ring slot of this anchor (anchors only, packed).
18074                        meta[base + 26] = (bounded_seen * kv_stride) as u32;
18075                        meta[base + 27] = pack.put(&w.sink_k);
18076                        meta[base + 28] = pack.put(&w.sink_v);
18077                        bounded_seen += 1;
18078                    }
18079                    _ => return None,
18080                }
18081                match &lw.ffn {
18082                    FfnKind::Dense(d) => {
18083                        meta[base + 15] = 0;
18084                        meta[base + 16] = 0;
18085                        meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
18086                        meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
18087                        meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
18088                    }
18089                    FfnKind::Moe(m) => {
18090                        let r = m.resonance.as_ref().unwrap();
18091                        let (shared, _) = m.shared.as_ref().unwrap();
18092                        meta[base + 15] = 1;
18093                        meta[base + 16] = m.experts.len() as u32;
18094                        meta[base + 17] = pack.put(&r.mu);
18095                        meta[base + 18] = pack.put(&r.u);
18096                        meta[base + 19] = pack.put(&r.bias);
18097                        meta[base + 20] = r.k as u32;
18098                        // Word 30: the growth shell as the runtime applies
18099                        // it now (`+inf` on trunk rows, the stored finite
18100                        // shell on grown rows, all `+inf` under
18101                        // `CMF_GROWTH_SHELL=off`), followed by one `−∞`
18102                        // sentinel at index E the kernel writes as the
18103                        // score of an expert outside its shell — WGSL has
18104                        // no infinity literal, so the value travels as
18105                        // data (`embryo_core_route_finalize`).
18106                        let mut shell = r.effective_shell(m.experts.len());
18107                        shell.push(f32::NEG_INFINITY);
18108                        meta[base + 30] = pack.put(&shell);
18109                        meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
18110                        meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
18111                        meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
18112                        for (e, ex) in m.experts.iter().enumerate() {
18113                            meta[base + 32 + e * 3] =
18114                                pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
18115                            meta[base + 33 + e * 3] =
18116                                pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
18117                            meta[base + 34 + e * 3] =
18118                                pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
18119                        }
18120                    }
18121                    FfnKind::DenseMoe(_) => return None,
18122                }
18123            }
18124            if !full_seen && self.num_layers == 0 {
18125                return None;
18126            }
18127            let id = {
18128                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
18129                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
18130            };
18131            let model = crate::gpu::EmbryoGraphModel {
18132                id,
18133                hidden: self.hidden_size,
18134                intermediate: self.intermediate_size,
18135                vocab: self.vocab_size,
18136                layers: self.num_layers,
18137                phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
18138                nphase: vmf.map(|c| c.nphase).unwrap_or(0),
18139                phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
18140                anchor_q_heads: self.num_heads,
18141                anchor_kv_heads: self.num_kv_heads,
18142                anchor_head_dim: self.head_dim,
18143                rotary_dim: self.rotary_dim,
18144                max_seq: self.kv_cache.max_seq_len,
18145                cluster_count,
18146                cluster_size: self.vocab_size / cluster_count,
18147                phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
18148                state_stride,
18149                kv_stride,
18150                norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
18151                phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
18152                weights: pack.data,
18153                meta,
18154                lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
18155                clusters: clusters.as_ref().clone(),
18156                final_norm: self.weights.final_norm.clone(),
18157                inv_freq: self.inv_freq.as_ref().clone(),
18158                bounded: bounded.is_some(),
18159                kv_layers: if bounded.is_some() {
18160                    bounded_seen
18161                } else {
18162                    self.num_layers
18163                },
18164                state_layers: phase_seen + gdn_seen,
18165                anchor_window,
18166                anchor_sink,
18167                phase_layers: phase_seen,
18168                gdn_layers: gdn_seen,
18169                gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
18170                gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
18171                gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
18172                gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
18173                gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
18174            };
18175            self.embryo_graph = Some(std::sync::Arc::new(model));
18176        }
18177        self.embryo_graph.clone()
18178    }
18179
18180    fn forward_layers_span(
18181        &mut self,
18182        hidden: &[f32],
18183        position: usize,
18184        task_mask: Option<&TaskMask>,
18185        from: usize,
18186        upto: Option<usize>,
18187    ) -> Vec<f32> {
18188        #[cfg(target_os = "macos")]
18189        let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
18190            position,
18191            from,
18192            upto.map_or(self.num_layers, |u| u.saturating_add(1)),
18193        );
18194        debug_assert!(
18195            from == 0
18196                || (self.dsv4.is_none()
18197                    && self.dsv41.is_none()
18198                    && self.qwen4_exp.is_none()
18199                    && !self.per_token_stack())
18200        );
18201        // Every plain forward — the whole-token Metal graph (`q1_graph_gpu`
18202        // wraps the GDN owners zero-copy and reallocates them on a size
18203        // change) and the CPU layer loop (reads/swaps `linear_state`) —
18204        // must see the previous speculative commit's asynchronous replay
18205        // complete. One mutex probe when nothing is pending.
18206        #[cfg(target_os = "macos")]
18207        if !crate::gpu_metal::wait_replay() {
18208            self.fail_metal_graph("the pending async replay failed before a plain forward");
18209            return vec![0.0; self.hidden_size];
18210        }
18211        if let Some(b) = &mut self.qwen4_exp {
18212            let _ = (task_mask, upto);
18213            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18214            let mut logits = Vec::new();
18215            crate::qwen4_exp::forward_token(
18216                &b.0,
18217                &b.1,
18218                &b.2,
18219                &mut b.3,
18220                token_id,
18221                position,
18222                &self.inv_freq,
18223                self.pool.as_deref(),
18224                &mut logits,
18225                true,
18226            );
18227            self.graph_logits = Some(logits);
18228            return vec![0.0; self.hidden_size];
18229        }
18230        // DeepSeek-V4 runs its own stack: the state is hc_mult copies, and
18231        // the forward returns LOGITS, not a hidden — the head is inside it
18232        // (the final fold sits between the last layer and the norm). The
18233        // token id rides in `hidden[0]`, written by embed_single, because
18234        // the hash layers route by id rather than by content.
18235        if let Some(b) = &mut self.dsv4 {
18236            let _ = (task_mask, upto);
18237            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18238            let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
18239            st.pos = position;
18240            let mut logits = Vec::new();
18241            crate::dsv4::forward_token(
18242                g,
18243                layers,
18244                &cfg,
18245                st,
18246                token_id,
18247                &self.inv_freq,
18248                self.pool.as_deref(),
18249                &mut logits,
18250            );
18251            self.graph_logits = Some(logits);
18252            self.dspark_probe(position, token_id);
18253            // The caller expects a hidden; the logits went out of band, as
18254            // with the fused lm_head path.
18255            return vec![0.0; self.hidden_size];
18256        }
18257        // DeepSeek-V4.1 owns its complete stack and emits logits out of band.
18258        if let Some(b) = &mut self.dsv41 {
18259            let _ = (task_mask, upto);
18260            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18261            let mut logits = Vec::new();
18262            crate::dsv41::forward_token(
18263                &b.0,
18264                &b.1,
18265                &b.2,
18266                &mut b.3,
18267                token_id,
18268                position,
18269                self.pool.as_deref(),
18270                &mut logits,
18271            );
18272            self.graph_logits = Some(logits);
18273            return vec![0.0; self.hidden_size];
18274        }
18275        // Gemma-3n runs its own stack (4 AltUp replicas don't fit this
18276        // loop); `hidden` is the extended embedding from embed_single.
18277        // Gemma-4 E-series: same hand-off — its dual-geometry attention
18278        // with shared-KV tail layers and the PLE injection live in
18279        // `g4e_forward`.
18280        if let Some(b) = &self.g4e {
18281            let _ = (task_mask, upto);
18282            return crate::g4e::g4e_forward(
18283                &b.0,
18284                &b.1,
18285                hidden,
18286                position,
18287                &mut self.kv_cache.layers,
18288                self.num_heads,
18289                self.num_kv_heads,
18290                self.pool.as_deref(),
18291            );
18292        }
18293        if let Some(b) = &self.g3n {
18294            let _ = (task_mask, upto);
18295            return crate::g3n::g3n_forward(
18296                &b.0,
18297                &b.1,
18298                hidden,
18299                position,
18300                &mut self.kv_cache.layers,
18301                self.num_heads,
18302                self.num_kv_heads,
18303                self.head_dim,
18304                self.pool.as_deref(),
18305            );
18306        }
18307        // Cortiq Embryo owns a separate resident graph: phase recurrent
18308        // state, resonance routing, the GQA anchor KV and hierarchical head
18309        // all execute in one Vulkan submit. It is limited to a complete
18310        // unmasked stack; spans and task masks retain the exact host path.
18311        // `CMF_EMBRYO_DBG=1` names the gate that keeps a token off the
18312        // resident graph — every refusal below is otherwise silent.
18313        if from == 0
18314            && upto.is_none()
18315            && task_mask.is_none()
18316            && self.anchor_core.is_some()
18317            && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
18318        {
18319            static ONCE: std::sync::Once = std::sync::Once::new();
18320            ONCE.call_once(|| {
18321                eprintln!(
18322                    "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
18323                     unsupported={} eligible={}",
18324                    crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
18325                    crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
18326                    std::env::var("CMF_EMBRYO_RESIDENT").ok(),
18327                    crate::gpu::enabled_here(),
18328                    self.graph_refused(),
18329                    self.embryo_resident_eligible(),
18330                );
18331            });
18332        }
18333        if from == 0
18334            && upto.is_none()
18335            && task_mask.is_none()
18336            // Embryo's recurrent/KV state has no host import path.  Do not
18337            // seed it for a prefill-only graph and then silently decode from
18338            // an empty CPU cache; both phases must select the resident owner.
18339            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
18340            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
18341            // This whole-token Embryo path remains explicitly opt-in.
18342            // `CMF_GPU_WGPU_GRAPH=1` still enables the mature generic graph,
18343            // but must not silently select this model-specific resident path.
18344            && matches!(
18345                std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
18346                Ok("1") | Ok("parallel")
18347            )
18348            && crate::gpu::enabled_here()
18349            && !self.graph_refused()
18350            // Sequence owner: past position zero the device continues only
18351            // a sequence it holds. A host-owned sequence (the graph refused
18352            // at its start, or its prefix was prefilled on the host) keeps
18353            // the host path to its end — never a device attempt at p > 0
18354            // over an empty device image.
18355            && (position == 0 || self.device_sequence_position().is_some())
18356            && self.embryo_resident_eligible()
18357            && let Some(model) = self.ensure_embryo_graph()
18358        {
18359            let mut lg = Vec::new();
18360            if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
18361            {
18362                self.graph_logits = Some(lg);
18363                return vec![0.0; self.hidden_size];
18364            }
18365            if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
18366                eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
18367            }
18368            // The refusal is THIS pipeline's: falling through once is safe
18369            // at position zero (the host owns the sequence from here),
18370            // while a refusal at a later position of a device-owned
18371            // sequence would mix a host KV/state path with a partial
18372            // device sequence.
18373            self.mark_graph_refused();
18374            if position != 0 {
18375                // Fail this sequence alone and leave nothing stale behind:
18376                // no reuse key, no host or device state for the next
18377                // request on this slot to "extend".
18378                self.kv_cache.clear();
18379                self.clear_history();
18380                crate::gpu::graph_kv_reset(self.graph_kv_id);
18381                panic!(
18382                    "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
18383                );
18384            }
18385        }
18386        let mut h = hidden.to_vec();
18387        // MiMo-V2 expert placement: decided before the graph or the per-op
18388        // arena can claim the budget the expert bank needs.
18389        self.mimo_moe_prepare();
18390        let _mimo_q8 = self
18391            .mimo_moe
18392            .is_on()
18393            .then(crate::qtensor::enter_full_gpu_q8_scope);
18394        // Split borrows: copy scalars / clone handles so the per-layer
18395        // cfg does not hold `&self` while the KV cache is `&mut`.
18396        let (nh, _nkv, _hd, hs, _rd, eps) = (
18397            self.num_heads,
18398            self.num_kv_heads,
18399            self.head_dim,
18400            self.hidden_size,
18401            self.rotary_dim,
18402            self.rms_eps,
18403        );
18404        let pool = self.pool.clone();
18405        // Opt-in wgpu token-graph attention (discrete Vulkan/DX12): the whole
18406        // attention sub-block runs resident in one submit. Off by default.
18407        // Whole-token wgpu graph: eligibility + arbitration.
18408        //  - explicit CMF_GPU_WGPU_GRAPH forces it on/off;
18409        //  - discrete adapters (4090: decode 76 -> 137 tok/s) and GDN
18410        //    hybrids (recurrent state device-resident, no CPU twin to
18411        //    race) TRUST it;
18412        //  - integrated/mobile adapters RACE it against the normal path
18413        //    at generation granularity (gpu::graph_race_*) — tiled
18414        //    mobile GPUs can turn the ~300-dispatch graph into seconds
18415        //    per token, while a fast phone GPU keeps its win.
18416        let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
18417        let graph_on = match graph_env.as_deref() {
18418            Some("0") => false,
18419            Some("prefill") => false, // decode keeps the per-op path
18420            Some(_) => true,
18421            // Unset: same discrete-only default as every other graph
18422            // site. "Is the GPU on" used to stand in here — which made
18423            // the 0.2 tok/s whole-token graph race-eligible on mobile
18424            // adapters and cost 12-14× on first tokens (cmfmobile
18425            // TUNING.md); integrated GPUs keep the per-op probe path.
18426            None => crate::gpu::wgpu_graph_default(),
18427        };
18428        let graph_trusted =
18429            graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
18430        let race_eligible =
18431            graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
18432        let mut tail_start = 0usize;
18433        if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
18434            let t_graph = std::time::Instant::now();
18435            let mut lg = Vec::new();
18436            let mut gl = 0usize;
18437            let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
18438            let declined = built.is_none();
18439            let built = match built {
18440                Some(Ok(hh)) => Some(hh),
18441                Some(Err(())) => {
18442                    // O(1) state was admitted before the device failure; the
18443                    // CPU mirrors are stale by construction.  Clear the whole
18444                    // sequence and stop rather than walking that stale state.
18445                    self.clear_sequence_state();
18446                    self.graph_failed
18447                        .store(true, std::sync::atomic::Ordering::Relaxed);
18448                    self.cancel
18449                        .store(true, std::sync::atomic::Ordering::Relaxed);
18450                    tracing::error!("token graph failed after admission; sequence state cleared");
18451                    return vec![0.0; self.hidden_size];
18452                }
18453                None => None,
18454            };
18455            // Past the transient guards (o1 still collecting, a softcap)
18456            // a refusal is about the weights and will never change —
18457            // remember it instead of walking every layer again next
18458            // token.
18459            if declined && !self.o1_active() && self.attn_softcap == 0.0 {
18460                self.mark_graph_refused();
18461            }
18462            graph_note(built.is_some(), gl, self.num_layers);
18463            if let Some(hh) = built {
18464                let dur = t_graph.elapsed();
18465                if std::env::var("CMF_GRAPH_PROF").is_ok() {
18466                    eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
18467                }
18468                if gl > 0 && gl < self.num_layers {
18469                    // Device prefix: the graph ran layers 0..gl and handed
18470                    // back the boundary hidden — the loop below owns the
18471                    // tail. The prefix layers' KV/state advanced on the
18472                    // device; the tail's advances on the host below. One
18473                    // boundary crossing per token.
18474                    h = hh;
18475                    tail_start = gl;
18476                } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
18477                    if !graph_trusted {
18478                        crate::gpu::graph_race_record(true, dur);
18479                    }
18480                    if !lg.is_empty() {
18481                        // Graph produced logits (final-norm + lm_head folded in) —
18482                        // pad/cap to vocab and hand them to the sampler directly.
18483                        lg.resize(self.vocab_size, 0.0);
18484                        if let Some(c) = self.final_softcap {
18485                            softcap_in_place(&mut lg, c, self.pool.as_deref());
18486                        }
18487                        self.graph_logits = Some(lg);
18488                    }
18489                    return hh;
18490                }
18491                // Hopeless first graph token: discard it and fall through
18492                // to the normal path. Safe exactly here — the prompt KV is
18493                // still CPU-owned (chunked prefill), so recomputing this
18494                // position is exact; the mirror's extra row is never read
18495                // (the race just settled on the normal path).
18496            }
18497        }
18498        // KIMI-LINEAR HAS NO SPLIT BUG. The 2.6× reported from the
18499        // model rotation (12.2 tok/s on one card against 4.6 on two)
18500        // was a single measurement of a model whose arm arbitration is
18501        // borderline, and it did not survive repetition. Three runs an
18502        // arm, same binary, back to back:
18503        //   probe on : 1 GPU 9.5 / 5.7 / 5.9   2 GPU 7.8 / 13.0 / 13.3
18504        //   pinned   : 1 GPU 5.6 / 5.3 / 5.2   2 GPU 3.5 / 4.2 / 3.4
18505        // With the arms pinned the split costs about 1.45×, which is
18506        // what a layer split costs. With the probe free, TWO CARDS RUN
18507        // FASTER — because for this model the CPU arm wins some op
18508        // classes and the probe finds that.
18509        //
18510        // Two things do stand, and both are measured. The token graph
18511        // builds NOTHING here (`covered 0 of 14 layers [0..14)`), so
18512        // every layer walks per-op on either arm — that is where the
18513        // headroom is, not in the split. And this model's benchmark is
18514        // unusable without `CMF_GPU_PROBE=0`: the arbitration alone
18515        // moves it by more than 2×.
18516        //
18517        // Span runs (network split): the graph covers exactly [from..=upto]
18518        // — one submit per SEGMENT per token. No race: its state is global
18519        // and calibrated on full stacks, so spans take the graph only where
18520        // it is trusted by default (discrete adapters / CMF_GPU_WGPU_GRAPH).
18521        let span = from > 0 || upto.is_some();
18522        if span && graph_on && task_mask.is_none() && graph_trusted {
18523            let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
18524            let mut lg = Vec::new();
18525            let mut gl = 0usize;
18526            let span_res =
18527                self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
18528            let span_res = match span_res {
18529                Some(Ok(hh)) => Some(hh),
18530                Some(Err(())) => {
18531                    self.clear_sequence_state();
18532                    self.graph_failed
18533                        .store(true, std::sync::atomic::Ordering::Relaxed);
18534                    self.cancel
18535                        .store(true, std::sync::atomic::Ordering::Relaxed);
18536                    tracing::error!(
18537                        "span token graph failed after admission; sequence state cleared"
18538                    );
18539                    return vec![0.0; self.hidden_size];
18540                }
18541                None => None,
18542            };
18543            graph_note(span_res.is_some(), gl, upto_excl - from);
18544            if std::env::var("CMF_GPU_DEBUG").is_ok() {
18545                // How much of the span the graph actually covered. A
18546                // prefix of nothing means every layer walks per-op and
18547                // the split's extra cost is elsewhere.
18548                static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
18549                if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
18550                    eprintln!(
18551                        "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
18552                        upto_excl - from,
18553                        span_res.is_some()
18554                    );
18555                }
18556            }
18557            if let Some(hh) = span_res {
18558                if gl == upto_excl - from {
18559                    if !lg.is_empty() {
18560                        lg.resize(self.vocab_size, 0.0);
18561                        if let Some(c) = self.final_softcap {
18562                            softcap_in_place(&mut lg, c, self.pool.as_deref());
18563                        }
18564                        self.graph_logits = Some(lg);
18565                    }
18566                    crate::gpu::set_layer(-1);
18567                    return hh;
18568                }
18569                // Partial device prefix of the span: CPU owns the tail.
18570                h = hh;
18571                tail_start = from + gl;
18572            }
18573        }
18574        // Layers the host is about to run whose device mirror moved ahead
18575        // of the host cache (a device prefix that shrank since the prompt,
18576        // a batched-prefill prefix longer than this token's): bring their
18577        // rows over first. One comparison per layer when nothing lags.
18578        let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
18579
18580        // A partial graph is an explicit GPU-prefix / CPU-tail split. Keep
18581        // the tail PURE host-side: letting its QTensor hooks re-enter the
18582        // residency arena streams every omitted layer through Vulkan and the
18583        // driver's freed-allocation cache can grow to the full model size
18584        // (25.4 GiB observed with a 14 GiB budget on Granite 30B Q8_2F).
18585        // With a MiMo expert bank the tail is not a whole-layer host
18586        // stream: its experts run from the bank (never the arena) and its
18587        // projections stay per-op on the device, which the bank's placement
18588        // left room for.
18589        let host_tail = tail_start > from;
18590        let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
18591        let automatic_gpu_prefix = self.automatic_gpu_prefix();
18592
18593        let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
18594        #[cfg(target_os = "macos")]
18595        let mut gpu_skip_until = 0usize;
18596        for li in tail_start.max(from)..self.num_layers {
18597            let _capacity_tail = automatic_gpu_prefix
18598                .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
18599                .map(|_| crate::gpu::enter_cpu_scope());
18600            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU (CMF_GPU_LAYERS)
18601            if let Some(u) = upto {
18602                if li > u {
18603                    break;
18604                }
18605            }
18606            if let Some(mask) = task_mask {
18607                if !mask.layer_alive(li) {
18608                    continue; // dead layer: residual pass-through
18609                }
18610            }
18611            // MiniMax's dynamic tail: attention + pooled MoE of this layer as
18612            // ONE device frame (single-layer graph call), cold experts on
18613            // the host. Any decline falls through to the ordinary host
18614            // layer; a post-admission device failure stops the sequence.
18615            if host_tail && task_mask.is_none() {
18616                match self.m3_pooled_layer(li, position, &mut h) {
18617                    Ok(true) => {
18618                        crate::gpu::set_layer(-1);
18619                        continue;
18620                    }
18621                    Ok(false) => {}
18622                    Err(()) => {
18623                        self.clear_sequence_state();
18624                        self.graph_failed
18625                            .store(true, std::sync::atomic::Ordering::Relaxed);
18626                        self.cancel
18627                            .store(true, std::sync::atomic::Ordering::Relaxed);
18628                        tracing::error!(
18629                            "pooled tail token graph failed after admission; sequence state cleared"
18630                        );
18631                        return vec![0.0; self.hidden_size];
18632                    }
18633                }
18634            }
18635            // Whole-block q1 token graph: a run of consecutive q1
18636            // layers — GDN and full attention — executes with one sync
18637            // per CPU attend instead of per op (macOS/Metal).
18638            #[cfg(target_os = "macos")]
18639            {
18640                if li < gpu_skip_until {
18641                    continue;
18642                }
18643                if task_mask.is_none() {
18644                    let end = self.q1_graph_gpu(li, upto, position, &mut h);
18645                    if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
18646                        // The graph may have mutated device state before a
18647                        // command-buffer error. Never continue with a CPU
18648                        // tail or read a stale host mirror after admission.
18649                        return vec![0.0; self.hidden_size];
18650                    }
18651                    if end > li {
18652                        gpu_skip_until = end;
18653                        // Looped Transformer: the graph stopped at a loop
18654                        // boundary — apply final norm before the next iteration.
18655                        if self.is_loop_end(end - 1) && end < self.num_layers {
18656                            h = inference::rms_norm(
18657                                &h,
18658                                &self.weights.final_norm,
18659                                self.rms_eps,
18660                                self.norm_style,
18661                            );
18662                        }
18663                        continue;
18664                    }
18665                }
18666            }
18667
18668            if task_mask.is_none() {
18669                match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
18670                    crate::gpu::BatchGraphOutcome::Completed => continue,
18671                    crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
18672                    crate::gpu::BatchGraphOutcome::Declined => {}
18673                }
18674            }
18675            #[cfg(feature = "gpu")]
18676            self.pull_lagging_host_kv(li, li + 1, position);
18677            // SenseNova-Loop mHC: the attention reads the routed branch of
18678            // the streams.
18679            let mhc_attn = self.mhc_layer(li).then(|| self.mhc_pre(li, false, &mut h));
18680            let lw = &self.weights.layers[self.phys_layer(li)];
18681            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
18682                if tp.parse::<usize>().ok() == Some(position) {
18683                    let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
18684                    eprintln!(
18685                        "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
18686                        h[0], h[1]
18687                    );
18688                }
18689            }
18690            // Norm into the pipeline scratch — the returning rms_norm
18691            // allocated twice per layer per token (roadmap §3 P0).
18692            let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
18693            inference::rms_norm_into(
18694                &h,
18695                &lw.input_norm,
18696                self.rms_eps,
18697                self.norm_style,
18698                &mut self.ws.n1,
18699            );
18700            drop(prof);
18701
18702            let attn_out = match &lw.attn {
18703                AttnKind::Mla(w) => {
18704                    let inv_freq_l = self.layer_inv_freq(li);
18705                    let rs = self.layer_rope_scale(li);
18706                    let eps = self.rms_eps;
18707                    let pool = self.pool.clone();
18708                    mla_attention(
18709                        w,
18710                        &self.ws.n1,
18711                        &mut self.kv_cache.layers[li],
18712                        position,
18713                        &inv_freq_l,
18714                        rs,
18715                        eps,
18716                        pool.as_deref(),
18717                    )
18718                }
18719                AttnKind::Linear(w) => {
18720                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
18721                    vmf_phase_forward(
18722                        &self.ws.n1,
18723                        w,
18724                        &cfg,
18725                        &mut self.kv_cache.layers[li].linear_state,
18726                        self.pool.as_deref(),
18727                    )
18728                }
18729                AttnKind::Kda(w) => {
18730                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
18731                    crate::linear_core::kda_forward(
18732                        &self.ws.n1,
18733                        w,
18734                        &cfg,
18735                        &mut self.kv_cache.layers[li].linear_state,
18736                        self.pool.as_deref(),
18737                    )
18738                }
18739                AttnKind::LinearGdn(w) => {
18740                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
18741                    gdn_forward(
18742                        &self.ws.n1,
18743                        w,
18744                        &cfg,
18745                        &mut self.kv_cache.layers[li].linear_state,
18746                        self.pool.as_deref(),
18747                    )
18748                }
18749                AttnKind::ShortConv(w) => {
18750                    let cfg = self
18751                        .short_conv_cfg
18752                        .expect("short-conv layer without short_conv_cfg");
18753                    short_conv_forward(
18754                        &self.ws.n1,
18755                        w,
18756                        &cfg,
18757                        &mut self.kv_cache.layers[li].linear_state,
18758                        self.pool.as_deref(),
18759                    )
18760                }
18761                AttnKind::Bounded(w) => {
18762                    // Natively bounded anchor: insert into the ring, attend
18763                    // over sinks ∪ window. No position, nothing appended.
18764                    let rope = self
18765                        .bounded_rope
18766                        .clone()
18767                        .expect("bounded layer without an installed rotation table");
18768                    let cfg = crate::bounded::BoundedAttnCfg {
18769                        num_heads: self.num_heads,
18770                        num_kv_heads: self.num_kv_heads,
18771                        head_dim: self.head_dim,
18772                        hidden_size: hs,
18773                        scale: self.attn_scale,
18774                        rope: &rope,
18775                        pool: pool.as_deref(),
18776                    };
18777                    crate::bounded::bounded_attention(
18778                        &self.ws.n1,
18779                        w,
18780                        &mut self.kv_cache.layers[li],
18781                        &cfg,
18782                    )
18783                }
18784                AttnKind::Full {
18785                    wq,
18786                    wk,
18787                    wv,
18788                    wo,
18789                    q_norm,
18790                    k_norm,
18791                    output_gate,
18792                    softplus_gate,
18793                    bias,
18794                } if self.kv_cache.layers[li].o1_sealed() => {
18795                    // O(1) override: decode on the sealed Nyström state
18796                    // instead of the growing KV cache.
18797                    let inv_freq_l = self.layer_inv_freq(li);
18798                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18799                    let cfg = QwenAttnCfg {
18800                        mrope: None,
18801                        num_heads: self.layer_num_heads(li),
18802                        num_kv_heads: nkv_l,
18803                        head_dim: hd_l,
18804                        hidden_size: hs,
18805                        position,
18806                        inv_freq: &inv_freq_l,
18807                        rotary_dim: rd_l,
18808                        scale: self.attn_scale,
18809                        softcap: self.attn_softcap,
18810                        window: None,
18811                        v_norm: self.attn_v_norm,
18812                        qk_norm_after_rope: self.qk_norm_after_rope,
18813                        gate_sigmoid: self.proj_gate_sigmoid,
18814                        q_norm: q_norm.as_deref(),
18815                        k_norm: k_norm.as_deref(),
18816                        output_gate: *output_gate,
18817                        softplus_gate: softplus_gate
18818                            .as_ref()
18819                            .map(|(gate, per_head)| (gate, *per_head)),
18820                        rope_scale: self.layer_rope_scale(li),
18821                        bias: bias
18822                            .as_ref()
18823                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18824                        rms_eps: eps,
18825                        norm_style: self.norm_style,
18826                        pool: pool.as_deref(),
18827                        v_head_dim: self.layer_v_dim(li),
18828                    };
18829                    attention::qwen_attention_nystrom(
18830                        &self.ws.n1,
18831                        wq,
18832                        wk,
18833                        wv,
18834                        wo,
18835                        &mut self.kv_cache.layers[li],
18836                        &cfg,
18837                    )
18838                }
18839                AttnKind::Full {
18840                    wq,
18841                    wk,
18842                    wv,
18843                    wo,
18844                    q_norm,
18845                    k_norm,
18846                    output_gate,
18847                    softplus_gate,
18848                    bias,
18849                } => 'attn: {
18850                    // wgpu token-graph attention (opt-in): whole sub-block in
18851                    // one submit, device K/V mirror. q1 only, no gate/bias/mask.
18852                    // Its kernel has no window, sink or narrow-V slot and
18853                    // one mirror geometry: such models stay on the CPU attend.
18854                    let dropin_reason =
18855                        graph_on.then(|| self.graph_attn_decline_reason()).flatten();
18856                    if let Some(reason) = dropin_reason {
18857                        self.note_graph_decline("wgpu attn dropin", reason);
18858                    }
18859                    if graph_on
18860                        && dropin_reason.is_none()
18861                        && !*output_gate
18862                        && softplus_gate.is_none()
18863                        && self.attention_heads_per_layer.is_none()
18864                        && bias.is_none()
18865                        && task_mask.is_none()
18866                    {
18867                        let inv_freq_l = self.layer_inv_freq(li);
18868                        let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18869                        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
18870                        if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
18871                            wq.mapped_q1(),
18872                            wk.mapped_q1(),
18873                            wv.mapped_q1(),
18874                            wo.mapped_q1(),
18875                        ) {
18876                            let gm = gm.clone();
18877                            let mut out = vec![0f32; hs];
18878                            let cache = &self.kv_cache.layers[li];
18879                            if crate::gpu::attn_dropin(
18880                                &gm,
18881                                self.graph_kv_id,
18882                                li,
18883                                &self.ws.n1,
18884                                qi,
18885                                ki,
18886                                vi,
18887                                oi,
18888                                q_norm.as_deref(),
18889                                k_norm.as_deref(),
18890                                self.qk_norm_after_rope,
18891                                &inv_freq_l,
18892                                nh,
18893                                nkv_l,
18894                                hd_l,
18895                                rd_l,
18896                                hs,
18897                                position,
18898                                self.kv_cache.max_seq_len,
18899                                gemma,
18900                                eps as f32,
18901                                cache.k_heads(),
18902                                cache.v_heads(),
18903                                &mut out,
18904                            ) {
18905                                break 'attn out;
18906                            }
18907                        }
18908                    }
18909                    let masked = task_mask
18910                        .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
18911                        .unwrap_or(false);
18912                    let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
18913                    // The masked kernel knows one pipeline-wide geometry and
18914                    // RoPE table, no window and no sink.
18915                    let plain = self.layer_attn_plain(li);
18916                    match (masked, f32_view) {
18917                        // Historical masked path (f32 slices; the loader
18918                        // keeps masked models in f32).
18919                        (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
18920                            let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
18921                            attention::multi_head_attention(
18922                                &self.ws.n1,
18923                                q,
18924                                k,
18925                                v,
18926                                o,
18927                                &mut self.kv_cache.layers[li],
18928                                self.num_heads,
18929                                self.num_kv_heads,
18930                                self.head_dim,
18931                                self.hidden_size,
18932                                position,
18933                                &active_heads,
18934                                &self.inv_freq,
18935                            )
18936                        }
18937                        (masked, _) => {
18938                            if masked {
18939                                tracing::warn!(
18940                                    "layer {li}: head mask on quantized weights or on a \
18941                                     window/sink/per-layer-geometry layer not supported \
18942                                     yet — executing dense"
18943                                );
18944                            }
18945                            let inv_freq_l = self.layer_inv_freq(li);
18946                            let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18947                            let cfg = QwenAttnCfg {
18948                                mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
18949                                num_heads: self.layer_num_heads(li),
18950                                num_kv_heads: nkv_l,
18951                                head_dim: hd_l,
18952                                hidden_size: hs,
18953                                position,
18954                                inv_freq: &inv_freq_l,
18955                                rotary_dim: rd_l,
18956                                scale: self.attn_scale,
18957                                softcap: self.attn_softcap,
18958                                window: self.layer_window(li),
18959                                v_norm: self.attn_v_norm,
18960                                qk_norm_after_rope: self.qk_norm_after_rope,
18961                                gate_sigmoid: self.proj_gate_sigmoid,
18962                                q_norm: q_norm.as_deref(),
18963                                k_norm: k_norm.as_deref(),
18964                                output_gate: *output_gate,
18965                                softplus_gate: softplus_gate
18966                                    .as_ref()
18967                                    .map(|(gate, per_head)| (gate, *per_head)),
18968                                rope_scale: self.layer_rope_scale(li),
18969                                bias: bias
18970                                    .as_ref()
18971                                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18972                                rms_eps: eps,
18973                                norm_style: self.norm_style,
18974                                pool: pool.as_deref(),
18975                                v_head_dim: self.layer_v_dim(li),
18976                            };
18977                            attention::qwen_attention(
18978                                &self.ws.n1,
18979                                wq,
18980                                wk,
18981                                wv,
18982                                wo,
18983                                &mut self.kv_cache.layers[li],
18984                                &cfg,
18985                            )
18986                        }
18987                    }
18988                }
18989            };
18990            // Gemma sandwich norm: normalize the attention branch before
18991            // it joins the residual stream.
18992            let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
18993                Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
18994                None => attn_out,
18995            };
18996            let mhc_ffn = match mhc_attn {
18997                Some(c) => {
18998                    // mHC: fold the attention output in, route the FFN's
18999                    // branch, and norm that.
19000                    self.mhc_post(li, false, &attn_out, &c, &mut h);
19001                    let cf = self.mhc_pre(li, true, &mut h);
19002                    let lw = &self.weights.layers[self.phys_layer(li)];
19003                    inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
19004                    Some(cf)
19005                }
19006                None => {
19007                    let lw = &self.weights.layers[self.phys_layer(li)];
19008                    let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
19009                    inference::add_rmsnorm_fused_into(
19010                        &mut h,
19011                        &attn_out,
19012                        &lw.post_norm,
19013                        self.rms_eps,
19014                        self.norm_style,
19015                        &mut self.ws.p1,
19016                    );
19017                    drop(prof);
19018                    None
19019                }
19020            };
19021            let lw = &self.weights.layers[self.phys_layer(li)];
19022            let mut attn_out = attn_out;
19023            attention::recycle_buf(&mut attn_out);
19024            let post_normed = &self.ws.p1;
19025
19026            let ffn_masked = task_mask
19027                .map(|m| m.ffn_active_count(li) < self.intermediate_size)
19028                .unwrap_or(false);
19029            // One masked dense CONTRACT, dispatched by cost. The
19030            // activation-zeroing arm (the batched sweep's, validated
19031            // against the replica to 0.8%) computes the FULL fused FFN
19032            // and zeroes the dead — right whenever most neurons live.
19033            // The sparse arm reads ONLY active rows and down columns —
19034            // per-row dots are slower per element than the fused kernel,
19035            // so it pays only once the mask is deep enough. The 0.5
19036            // crossover is first-principles (fused kernels run ~2x the
19037            // per-row dot throughput); a shallow specialist (95% alive)
19038            // stays fused, a --target-sparsity bake flips arms on its
19039            // own weight.
19040            let ffn_out = match (ffn_masked, &lw.ffn) {
19041                // A defragged tube layer answers its own mask: the core
19042                // always runs, each tube runs when its bit is on, and
19043                // the tubes that are off are never read from the mmap.
19044                (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
19045                    let row = task_mask
19046                        .and_then(|tm| tm.ffn_masks.get(li))
19047                        .map(|v| v.as_slice());
19048                    tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
19049                }
19050                (true, FfnKind::Dense(d)) => {
19051                    let tm = task_mask.unwrap();
19052                    let alive = tm.ffn_active_count(li);
19053                    let deep = alive * 2 <= self.intermediate_size;
19054                    if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
19055                        let active = tm.ffn_active_indices(li);
19056                        sparse_ffn_quant(
19057                            d,
19058                            post_normed,
19059                            &active,
19060                            self.hidden_size,
19061                            self.pool.as_deref(),
19062                        )
19063                    } else if deep
19064                        && let (Some(g), Some(u), Some(dn)) = (
19065                            d.gate_proj.as_f32(),
19066                            d.up_proj.as_f32(),
19067                            d.down_proj.as_f32(),
19068                        )
19069                    {
19070                        let active = tm.ffn_active_indices(li);
19071                        inference::sparse_ffn_forward(
19072                            post_normed,
19073                            g,
19074                            u,
19075                            dn,
19076                            self.hidden_size,
19077                            self.intermediate_size,
19078                            &active,
19079                            self.pool.as_deref(),
19080                        )
19081                    } else {
19082                        let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
19083                        dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
19084                    }
19085                }
19086                (true, FfnKind::Moe(m)) => {
19087                    // MoE is sparse by expert selection; a task mask
19088                    // narrows the ROUTABLE set via its expert fields
19089                    // (spec §5) when it carries them.
19090                    let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
19091                    ffn_forward(
19092                        &lw.ffn,
19093                        post_normed,
19094                        self.pool.as_deref(),
19095                        allowed.as_deref(),
19096                    )
19097                }
19098                (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
19099                    dm,
19100                    post_normed,
19101                    &h,
19102                    self.rms_eps,
19103                    self.norm_style,
19104                    self.pool.as_deref(),
19105                ),
19106                (false, _) => match &lw.ffn {
19107                    FfnKind::DenseMoe(dm) => dense_moe_ffn(
19108                        dm,
19109                        post_normed,
19110                        &h,
19111                        self.rms_eps,
19112                        self.norm_style,
19113                        self.pool.as_deref(),
19114                    ),
19115                    FfnKind::Moe(m)
19116                        if task_mask.is_none()
19117                            && host_tail
19118                            && li >= tail_start
19119                            && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
19120                    {
19121                        moe_ffn_minicpm_tail(
19122                            &mut self.minicpm_tail,
19123                            li,
19124                            self.num_layers,
19125                            m,
19126                            post_normed,
19127                            self.pool.as_deref(),
19128                        )
19129                    }
19130                    FfnKind::Moe(m)
19131                        if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
19132                    {
19133                        moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
19134                    }
19135                    _ => {
19136                        let allowed = match (&lw.ffn, task_mask) {
19137                            (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
19138                            _ => None,
19139                        };
19140                        ffn_forward(
19141                            &lw.ffn,
19142                            post_normed,
19143                            self.pool.as_deref(),
19144                            allowed.as_deref(),
19145                        )
19146                    }
19147                },
19148            };
19149            let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
19150                Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
19151                None => ffn_out,
19152            };
19153            match mhc_ffn {
19154                Some(c) => self.mhc_post(li, true, &ffn_out, &c, &mut h),
19155                None => {
19156                    for (i, &f) in ffn_out.iter().enumerate() {
19157                        h[i] += f;
19158                    }
19159                }
19160            }
19161            let mut ffn_out = ffn_out;
19162            attention::recycle_buf(&mut ffn_out);
19163
19164            // Gemma-4: the layer output is scaled by a learned scalar.
19165            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
19166                for v in h.iter_mut() {
19167                    *v *= sc;
19168                }
19169            }
19170            // CMF_LAYER_DUMP: this position's hidden after layer li.
19171            if self.layer_dump.is_some() {
19172                self.dump_layer_row(position, li, &h);
19173            }
19174
19175            // Looped Transformer: apply final norm at the end of each loop iteration.
19176            // Nanbeige 4.2: after layer 21 (virtual), apply norm before looping back to layer 0.
19177            if self.is_loop_end(li) && li + 1 < self.num_layers {
19178                h = inference::rms_norm(
19179                    &h,
19180                    &self.weights.final_norm,
19181                    self.rms_eps,
19182                    self.norm_style,
19183                );
19184            }
19185
19186            // Dynamic routing φ capture (on-policy): the
19187            // EMA of the post-residual hidden at the router's phi_layer,
19188            // updated as the context evolves during decode.
19189            if self.dyn_phi_layer == Some(li) {
19190                self.update_dyn_phi(&h);
19191            }
19192        }
19193        crate::gpu::set_layer(-1); // layers done — lm_head outside layer-split
19194        if let Some(t) = t_race_cpu {
19195            crate::gpu::graph_race_record(false, t.elapsed());
19196        }
19197
19198        h
19199    }
19200
19201    /// EMA of φ at the router layer (rolling, weight 0.2 = ~5-token
19202    /// horizon). First observation seeds it exactly.
19203    fn update_dyn_phi(&mut self, h: &[f32]) {
19204        const A: f32 = 0.2;
19205        if self.dyn_phi_ema.len() != h.len() {
19206            self.dyn_phi_ema = vec![0.0; h.len()];
19207            self.dyn_phi_seen = 0;
19208        }
19209        if self.dyn_phi_seen == 0 {
19210            self.dyn_phi_ema.copy_from_slice(h);
19211        } else {
19212            for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
19213                *e = (1.0 - A) * *e + A * v;
19214            }
19215        }
19216        self.dyn_phi_seen += 1;
19217    }
19218
19219    /// Current router φ (EMA at phi_layer); empty until first capture.
19220    pub fn dyn_phi(&self) -> &[f32] {
19221        &self.dyn_phi_ema
19222    }
19223
19224    /// Enable/disable φ capture at the router layer, reset the EMA.
19225    pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
19226        self.dyn_phi_layer = layer;
19227        self.dyn_phi_ema.clear();
19228        self.dyn_phi_seen = 0;
19229    }
19230
19231    /// Skills eligible for dynamic switching: (index, id, phi_layer).
19232    pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
19233        let Some(model) = &self.model else {
19234            return Vec::new();
19235        };
19236        model
19237            .header
19238            .skills
19239            .iter()
19240            .enumerate()
19241            .filter_map(|(i, sk)| {
19242                // A v2 record routes only through the request-level
19243                // backbone-gated decision (its status and gate are
19244                // checked there), never per token.
19245                if sk.is_v2() {
19246                    return None;
19247                }
19248                let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
19249                let sel = sk.selection.as_ref()?;
19250                (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
19251            })
19252            .collect()
19253    }
19254
19255    /// Index of the currently overlaid skill (None = backbone).
19256    pub fn active_skill(&self) -> Option<usize> {
19257        self.dyn_active
19258    }
19259
19260    /// Enable dynamic per-token skill routing: build the hysteresis
19261    /// router from the container's routable skills, start φ capture at
19262    /// their (shared) phi_layer. Returns the number of routable skills
19263    /// (0 = nothing to route; router stays off). Idempotent.
19264    pub fn enable_dynamic_routing(&mut self) -> usize {
19265        use crate::swarm::{DynRouter, RoutableSkill};
19266        let Some(model) = self.model.clone() else {
19267            return 0;
19268        };
19269        // Router policy v2 (spec §9.4) routes per REQUEST: the backbone is
19270        // the default and only the backbone-gated decision may pick a
19271        // skill. A per-token switch would bypass that gate (and change
19272        // the O(1) state mid-sequence), so the hysteresis router never
19273        // runs on such a file; the caller keeps the request-level
19274        // decision.
19275        if let Some(r) = &model.header.router {
19276            tracing::warn!(
19277                "dynamic routing disabled: this file declares router policy '{}' with \
19278                 granularity \"{}\" — the request-level decision applies instead",
19279                r.policy,
19280                r.granularity
19281            );
19282            return 0;
19283        }
19284        // Format-v2 skill records (bit SKILLS_V2) without a router policy:
19285        // their status/gate contract ("auto-routing requires active +
19286        // measured") lives in the backbone-gated decision only — the
19287        // hysteresis router would switch into a quarantined record
19288        // (fail-open). Refuse the whole file, not just its v2 records.
19289        if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
19290            || model.header.skills.iter().any(|s| s.is_v2())
19291        {
19292            tracing::warn!(
19293                "dynamic routing disabled: this file carries format-v2 skill records \
19294                 (SKILLS_V2) — they route per request through a router policy only"
19295            );
19296            return 0;
19297        }
19298        // A blend materialized f32 working tensors into the layers; there
19299        // is no single skill index to revert from → refuse (honest).
19300        if self.dyn_blend_loaded {
19301            tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
19302            return 0;
19303        }
19304        // A statically-overlaid skill that is NOT FFN-eligible can't be
19305        // cheaply reverted at generation start → refuse rather than
19306        // silently keep it overlaid.
19307        if let Some(a) = self.dyn_active {
19308            if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
19309                tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
19310                return 0;
19311            }
19312        }
19313        let hidden = self.hidden_size;
19314        let mut skills = Vec::new();
19315        for (idx, id, _phi) in self.dynamic_skills() {
19316            if let Some(sel) = model.header.skills[idx].selection.as_ref() {
19317                if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
19318                    skills.push(rs);
19319                }
19320            }
19321        }
19322        if skills.is_empty() {
19323            return 0;
19324        }
19325        // Skills should share a phi_layer; warn (not fail) if they don't.
19326        let phi = skills[0].phi_layer;
19327        if skills.iter().any(|s| s.phi_layer != phi) {
19328            tracing::warn!("routable skills disagree on phi_layer; using {phi}");
19329        }
19330        let n = skills.len();
19331        self.set_dyn_phi_layer(Some(phi));
19332        self.dyn_router = Some(DynRouter::new(skills));
19333        n
19334    }
19335
19336    /// Human-readable switch log from the last dynamic-routed generation.
19337    pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
19338        self.dyn_router
19339            .as_ref()
19340            .map(|r| r.switches.clone())
19341            .unwrap_or_default()
19342    }
19343
19344    /// LM head: hidden → logits [vocab_size]. The dominant matvec of
19345    /// every decode step — row-parallel on the worker pool.
19346    fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
19347        let _mimo_q8 = self
19348            .mimo_moe
19349            .is_on()
19350            .then(crate::qtensor::enter_full_gpu_q8_scope);
19351        let rows = self.weights.lm_head.rows();
19352        let mut logits = attention::take_buf(rows.min(self.vocab_size));
19353        // Banked MiMo uses the same exact projection family for the
19354        // plain/draft head and the batched verification head. Read both
19355        // scale planes in-place instead of preparing per-op scale buffers.
19356        let served = self.mimo_moe.is_on()
19357            && crate::gpu::mimo_q8_short_enabled()
19358            && rows == self.vocab_size
19359            && !self.weights.lm_head.has_prism_contract()
19360            && self
19361                .weights
19362                .lm_head
19363                .graph_weight()
19364                .is_some_and(|(model, idx, kind, _)| {
19365                    kind == 7
19366                        && crate::gpu::q82_short_rows(
19367                            model,
19368                            idx,
19369                            hidden,
19370                            1,
19371                            rows,
19372                            self.hidden_size,
19373                            &mut logits,
19374                        )
19375                });
19376        if !served {
19377            self.weights
19378                .lm_head
19379                .matvec(hidden, &mut logits, self.pool.as_deref());
19380        }
19381        logits.resize(self.vocab_size, 0.0);
19382        if let Some(m) = self.logit_multiplier {
19383            for l in logits.iter_mut() {
19384                *l *= m;
19385            }
19386        }
19387        if let Some(c) = self.final_softcap {
19388            softcap_in_place(&mut logits, c, self.pool.as_deref());
19389        }
19390        if let Some(cm) = self.head_clusters.as_ref() {
19391            self.hierarchical_head_logprobs(hidden, cm, &mut logits);
19392        }
19393        logits
19394    }
19395
19396    /// Two-level head (Cortiq Embryo): in place, logits[v] ← log p(v) =
19397    /// (lc[c] − lse(lc)) + (logit[v] − lse over v's cluster block), c = v / S.
19398    fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
19399        let h = hidden.len();
19400        let ncl = cm.len() / h.max(1);
19401        if ncl == 0 || logits.len() % ncl != 0 {
19402            return;
19403        }
19404        let cs = logits.len() / ncl;
19405        // cluster logits + log-softmax
19406        let mut lc = vec![0.0f32; ncl];
19407        for c in 0..ncl {
19408            let row = &cm[c * h..(c + 1) * h];
19409            let mut s = 0.0f32;
19410            for j in 0..h {
19411                s += row[j] * hidden[j];
19412            }
19413            lc[c] = s;
19414        }
19415        let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19416        let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
19417        for c in 0..ncl {
19418            let blk = &mut logits[c * cs..(c + 1) * cs];
19419            let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19420            let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
19421            let add = lc[c] - lse - bl;
19422            for v in blk.iter_mut() {
19423                *v += add;
19424            }
19425        }
19426    }
19427
19428    /// Prefill `ids` and return the next-token logits — what the model
19429    /// would predict next, WITHOUT committing to generation (introspection
19430    /// for `cortiq explain`). Clears and repopulates the KV cache; leaves
19431    /// the active overlay untouched.
19432    pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
19433        #[cfg(target_os = "macos")]
19434        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
19435        self.clear_sequence_state();
19436        // This helper is used by the pooled classification endpoint, where
19437        // every request is a fresh sequence. The shared reset also clears the
19438        // wgpu token graph's device-side recurrent state.
19439        crate::gpu::graph_race_begin_generation();
19440        if task_mask.is_none() {
19441            self.o1_begin();
19442        }
19443        let mut hidden = vec![0.0f32; self.hidden_size];
19444        for (pos, &id) in ids.iter().enumerate() {
19445            let emb = self.embed_single(id);
19446            hidden = self.forward_layers(&emb, pos, task_mask);
19447        }
19448        if let Err(err) = self.o1_seal_checked() {
19449            self.o1_fail(err);
19450        }
19451        // Stacks that own their head (V4, V4.1, Qwen3.8-Flash-Next, GLM-5)
19452        // return a zero hidden and hand the logits out of band.
19453        if let Some(logits) = self.graph_logits.take() {
19454            return logits;
19455        }
19456        inference::rms_norm_into(
19457            &hidden,
19458            &self.weights.final_norm,
19459            self.rms_eps,
19460            self.norm_style,
19461            &mut self.ws.n1,
19462        );
19463        self.lm_head_forward(&self.ws.n1)
19464    }
19465}
19466
19467/// Convenience: deterministic tiny pipeline for tests.
19468pub fn create_test_pipeline(
19469    hidden_size: usize,
19470    intermediate_size: usize,
19471    num_heads: usize,
19472    num_kv_heads: usize,
19473    head_dim: usize,
19474    num_layers: usize,
19475    vocab_size: usize,
19476) -> Pipeline {
19477    // Small pseudo-random weights: constant weights make attention
19478    // degenerate and hide indexing bugs.
19479    let synth = |n: usize, salt: usize| -> Vec<f32> {
19480        (0..n)
19481            .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
19482            .collect()
19483    };
19484    let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
19485        QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19486    };
19487    let layer_weights: Vec<LayerWeights> = (0..num_layers)
19488        .map(|li| LayerWeights {
19489            input_norm: vec![1.0; hidden_size],
19490            post_norm: vec![1.0; hidden_size],
19491            attn_out_norm: None,
19492            ffn_out_norm: None,
19493            layer_scale: None,
19494            ffn: FfnKind::Dense(DenseFfn {
19495                gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
19496                up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
19497                down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
19498                act: Act::Silu,
19499                down_t: None,
19500                segs: Vec::new(),
19501            }),
19502            attn: AttnKind::Full {
19503                bias: None,
19504                wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
19505                wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
19506                wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
19507                wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
19508                q_norm: None,
19509                k_norm: None,
19510                output_gate: false,
19511                softplus_gate: None,
19512            },
19513        })
19514        .collect();
19515
19516    Pipeline::new(
19517        Tokenizer::byte_level(),
19518        PipelineWeights {
19519            embed_tokens: qt(vocab_size, hidden_size, 100),
19520            layers: layer_weights,
19521            lm_head: qt(vocab_size, hidden_size, 200),
19522            final_norm: vec![1.0; hidden_size],
19523        },
19524        hidden_size,
19525        intermediate_size,
19526        num_heads,
19527        num_kv_heads,
19528        head_dim,
19529        num_layers,
19530        num_layers, // physical_layers = num_layers (non-looped)
19531        false,      // loop_final_norm
19532        vocab_size,
19533        1e-6,
19534        10_000.0,
19535        NormStyle::Qwen,
19536        4096,
19537        SamplerConfig {
19538            seed: Some(42),
19539            ..Default::default()
19540        },
19541    )
19542}
19543
19544/// Batched dense-FFN: gate/up/down via matmat (element-wise the same
19545/// math as b × dense_ffn — the same dot kernels).
19546/// One mask bit, LSB-first per byte — `TaskMask::ffn_active_indices`'s
19547/// convention.
19548#[inline]
19549fn mask_bit(row: &[u8], j: usize) -> bool {
19550    (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
19551}
19552
19553/// Zero the CLOSED neurons' activations in a [rows × inter] panel — the
19554/// masked-inference fast path's whole trick: full fused quant compute,
19555/// then the mask lands on the ACTIVATIONS, which is arithmetically the
19556/// pruned network without touching a quantized weight byte. Whole open
19557/// bytes (0xFF = 8 open neurons) skip in one test.
19558/// `CMF_FFN_MASK_GAIN` — Patent 12 FIG. 4, variance-preserving
19559/// rescaling: truncation removes a share of the layer's output energy,
19560/// so the survivors are scaled up to put the variance back where the
19561/// downstream norm expects it. A scalar here; per layer it is
19562/// `sqrt(total energy / kept energy)`.
19563fn mask_gain() -> f32 {
19564    static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
19565    *G.get_or_init(|| {
19566        std::env::var("CMF_FFN_MASK_GAIN")
19567            .ok()
19568            .and_then(|v| v.parse().ok())
19569            .unwrap_or(1.0)
19570    })
19571}
19572
19573fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
19574    // With CMF_FFN_MEANFILL a closed neuron contributes its average
19575    // instead of nothing — same bytes read, one constant restored.
19576    let fill = meanfill().and_then(|(i, v)| {
19577        let li = crate::gpu::cur_layer();
19578        (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
19579    });
19580    for r in 0..rows {
19581        let base = r * inter;
19582        for (bi, &byte) in row.iter().enumerate() {
19583            if byte == 0xFF {
19584                continue;
19585            }
19586            let j0 = bi * 8;
19587            for bit in 0..8 {
19588                let j = j0 + bit;
19589                if j < inter && byte & (1 << bit) == 0 {
19590                    g[base + j] = fill.map_or(0.0, |f| f[j]);
19591                }
19592            }
19593        }
19594    }
19595    let gain = mask_gain();
19596    if gain != 1.0 {
19597        for v in g[..rows * inter].iter_mut() {
19598            *v *= gain;
19599        }
19600    }
19601}
19602
19603/// True when neuron `i`'s bit is set (no mask = everything runs).
19604#[inline]
19605fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
19606    row.is_none_or(|r| mask_bit(r, i))
19607}
19608
19609/// Every bit below `n` set — the common case for a tube file's CORE,
19610/// where only the tube bits vary per task.
19611fn all_bits_on(row: &[u8], n: usize) -> bool {
19612    (0..n).all(|i| mask_bit(row, i))
19613}
19614
19615/// `CMF_TUBE_TOPK` — how many tubes a TOKEN may open (0 = the task mask
19616/// decides alone). This is the dense FFN read as a mixture: the tubes
19617/// are the experts a k-means over `gate_proj` rows found, and the token
19618/// picks among them. `CMF_TUBE_SCORE=gate` scores a tube by its own
19619/// gate (realizable: only `up`/`down` of the losers go unread),
19620/// `=oracle` scores by the true `silu(gate)·up` mass (the ceiling —
19621/// only `down` is saved, and the selection has read what it predicts).
19622fn tube_topk() -> usize {
19623    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19624    *K.get_or_init(|| {
19625        std::env::var("CMF_TUBE_TOPK")
19626            .ok()
19627            .and_then(|v| v.parse().ok())
19628            .unwrap_or(0)
19629    })
19630}
19631
19632fn tube_score_oracle() -> bool {
19633    static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19634    *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
19635}
19636
19637/// The routed arm of `tube_ffn`: a token opens only its best `k` tubes.
19638/// At `b == 1` (decode) the losers are genuinely never read — that is
19639/// the speed. At `b > 1` (the scoring sweep) every tube is computed and
19640/// the losers' activations are zeroed instead: same arithmetic, so the
19641/// perplexity is the routed model's, measured without a per-token
19642/// gather in the middle of a GEMM.
19643fn tube_ffn_routed(
19644    d: &DenseFfn,
19645    xs: &[f32],
19646    b: usize,
19647    pool: Option<&Pool>,
19648    mask_row: Option<&[u8]>,
19649    k: usize,
19650) -> Vec<f32> {
19651    let hidden = d.down_proj.rows();
19652    let core = d.gate_proj.rows();
19653    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19654    let mut out = match (b, core_full, mask_row) {
19655        (1, true, _) => dense_ffn(d, xs, pool),
19656        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19657        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19658        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19659    };
19660    let cand: Vec<usize> = (0..d.segs.len())
19661        .filter(|&i| tube_bit(mask_row, d.segs[i].start))
19662        .collect();
19663    if cand.is_empty() {
19664        return out;
19665    }
19666    // gate (and, where the score or the batch needs it, up) per tube.
19667    // The SCORE is taken at the point the serving path could take it:
19668    // off the gate alone, or off the finished activation for the oracle.
19669    let oracle = tube_score_oracle();
19670    let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
19671    let mut scores = vec![0f32; b * cand.len()];
19672    for (ci, &i) in cand.iter().enumerate() {
19673        let seg = &d.segs[i];
19674        let w = seg.width;
19675        let mut g = vec![0.0f32; b * w];
19676        if b == 1 {
19677            seg.gate.matvec(xs, &mut g, pool);
19678        } else {
19679            seg.gate.matmat(xs, b, &mut g, pool);
19680        }
19681        for v in g.iter_mut() {
19682            *v = Act::Silu.combine(*v, 1.0);
19683        }
19684        if !oracle {
19685            for t in 0..b {
19686                scores[t * cand.len() + ci] =
19687                    g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19688            }
19689        }
19690        if oracle || b > 1 {
19691            let mut u = vec![0.0f32; b * w];
19692            if b == 1 {
19693                seg.up.matvec(xs, &mut u, pool);
19694            } else {
19695                seg.up.matmat(xs, b, &mut u, pool);
19696            }
19697            for (a, &v) in g.iter_mut().zip(u.iter()) {
19698                *a *= v;
19699            }
19700            if oracle {
19701                for t in 0..b {
19702                    scores[t * cand.len() + ci] =
19703                        g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19704                }
19705            }
19706        }
19707        acts.push(g);
19708    }
19709    // per-token scores and the winners
19710    let keep = k.min(cand.len());
19711    let mut scratch: Vec<f32> = Vec::new();
19712    for t in 0..b {
19713        let mut sc: Vec<(f32, usize)> = (0..cand.len())
19714            .map(|ci| (scores[t * cand.len() + ci], ci))
19715            .collect();
19716        sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
19717        let mut alive = vec![false; cand.len()];
19718        for &(_, ci) in sc.iter().take(keep) {
19719            alive[ci] = true;
19720        }
19721        if b > 1 {
19722            for (ci, a) in acts.iter_mut().enumerate() {
19723                if !alive[ci] {
19724                    let w = d.segs[cand[ci]].width;
19725                    a[t * w..(t + 1) * w].fill(0.0);
19726                }
19727            }
19728        } else {
19729            // decode: finish only the winners — the losers' up/down
19730            // (and, with the gate score, everything but their gate)
19731            // are never touched.
19732            for (ci, &i) in cand.iter().enumerate() {
19733                if !alive[ci] {
19734                    continue;
19735                }
19736                let seg = &d.segs[i];
19737                let w = seg.width;
19738                let g = &mut acts[ci];
19739                if !tube_score_oracle() {
19740                    scratch.clear();
19741                    scratch.resize(w, 0.0);
19742                    seg.up.matvec(xs, &mut scratch, pool);
19743                    for (a, &v) in g.iter_mut().zip(scratch.iter()) {
19744                        *a *= v;
19745                    }
19746                }
19747                let mut acc = vec![0.0f32; hidden];
19748                seg.down.matvec(g, &mut acc, pool);
19749                for (o, a) in out.iter_mut().zip(&acc) {
19750                    *o += *a;
19751                }
19752            }
19753        }
19754    }
19755    if b > 1 {
19756        for (ci, &i) in cand.iter().enumerate() {
19757            let seg = &d.segs[i];
19758            let mut acc = vec![0.0f32; b * hidden];
19759            seg.down.matmat(&acts[ci], b, &mut acc, pool);
19760            for (o, a) in out.iter_mut().zip(&acc) {
19761                *o += *a;
19762            }
19763        }
19764    }
19765    out
19766}
19767
19768/// FFN of a defragged tube layer: the always-on core plus the tubes the
19769/// task mask switches on. Each tube is a normal tensor triple, so the
19770/// same kernels run it and an inactive tube's bytes are never read —
19771/// that is the whole point of the defrag (a scattered mask cannot skip
19772/// bytes; a contiguous one is just a smaller matrix).
19773fn tube_ffn(
19774    d: &DenseFfn,
19775    xs: &[f32],
19776    b: usize,
19777    pool: Option<&Pool>,
19778    mask_row: Option<&[u8]>,
19779) -> Vec<f32> {
19780    if tube_topk() > 0 {
19781        return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
19782    }
19783    let hidden = d.down_proj.rows();
19784    let core = d.gate_proj.rows();
19785    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19786    let mut out = match (b, core_full, mask_row) {
19787        (1, true, _) => dense_ffn(d, xs, pool),
19788        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19789        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19790        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19791    };
19792    TUBE_SCRATCH.with(|sc| {
19793        let mut sc = sc.borrow_mut();
19794        let [g, u, acc] = &mut *sc;
19795        for seg in &d.segs {
19796            if !tube_bit(mask_row, seg.start) {
19797                continue;
19798            }
19799            let w = seg.width;
19800            g.resize(b * w, 0.0);
19801            if b == 1
19802                && d.act == Act::Silu
19803                && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
19804            {
19805                // g holds silu(gate)·up.
19806            } else {
19807                u.resize(b * w, 0.0);
19808                if b == 1 {
19809                    QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
19810                } else {
19811                    seg.gate.matmat(xs, b, g, pool);
19812                    seg.up.matmat(xs, b, u, pool);
19813                }
19814                for i in 0..b * w {
19815                    g[i] = d.act.combine(g[i], u[i]);
19816                }
19817            }
19818            acc.resize(b * hidden, 0.0);
19819            acc.fill(0.0);
19820            if b == 1 {
19821                seg.down.matvec(g, acc, pool);
19822            } else {
19823                seg.down.matmat(g, b, acc, pool);
19824            }
19825            for (o, a) in out.iter_mut().zip(acc.iter()) {
19826                *o += *a;
19827            }
19828        }
19829        out
19830    })
19831}
19832
19833thread_local! {
19834    /// gate / up / down-accumulator scratch for the tube loop — a tube
19835    /// runs once per layer per token, and a fresh Vec each time is a
19836    /// malloc per tube per layer per token.
19837    static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
19838        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
19839}
19840
19841/// CMF_PREFILL_PROF: cumulative ns of the batched walk's attention and
19842/// FFN halves (all layers, all chunks).
19843static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
19844    std::sync::atomic::AtomicU64::new(0),
19845    std::sync::atomic::AtomicU64::new(0),
19846];
19847
19848/// The wgpu side of `CMF_PREFILL_PROF`: q|k|v GEMMs' enqueue and readback
19849/// wait, the chunk attend's readback wait (ms, cumulative) and the int8
19850/// GEMMs the `zi_mm` route took.
19851fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
19852    #[cfg(feature = "gpu")]
19853    {
19854        use std::sync::atomic::Ordering::Relaxed;
19855        let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
19856        (
19857            ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
19858            ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
19859            ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
19860            crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
19861        )
19862    }
19863    #[cfg(not(feature = "gpu"))]
19864    (0.0, 0.0, 0.0, 0)
19865}
19866
19867/// The wgpu prefill mirror attend's outcome counters
19868/// (`gpu_wgpu::MIRROR_EVENTS`).
19869fn wgpu_mirror_counters() -> [u64; 6] {
19870    #[cfg(feature = "gpu")]
19871    {
19872        crate::gpu_wgpu::MIRROR_EVENTS
19873            .each_ref()
19874            .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
19875    }
19876    #[cfg(not(feature = "gpu"))]
19877    [0; 6]
19878}
19879
19880fn prefill_prof_on() -> bool {
19881    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19882    *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
19883}
19884
19885/// Accumulate the dense FFN activations which feed the down projection.
19886///
19887/// MTP calibration intentionally calls this independently of the trunk's
19888/// `FFN_PROBE` TLS recorder: an MTP sidecar can use a different FFN width,
19889/// and borrowing the trunk's layer-numbered accumulator would silently turn
19890/// draft statistics into trunk proxies.  The caller owns the execution scope
19891/// (the public MTP probe uses `gpu::cpu_scope`) and supplies one accumulator
19892/// per actual MTP FFN channel.
19893pub(crate) fn accumulate_dense_ffn_activation_mass(
19894    d: &DenseFfn,
19895    xs: &[f32],
19896    b: usize,
19897    pool: Option<&Pool>,
19898    mass: &mut [f64],
19899    squared_mass: &mut [f64],
19900) -> Result<(), String> {
19901    let inter = d.gate_proj.rows();
19902    let hidden = d.gate_proj.cols();
19903    if mass.len() != inter || squared_mass.len() != inter {
19904        return Err(format!(
19905            "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
19906            mass.len(),
19907            squared_mass.len()
19908        ));
19909    }
19910    if d.up_proj.rows() != inter
19911        || d.up_proj.cols() != hidden
19912        || d.down_proj.cols() != inter
19913        || d.down_proj.rows() != hidden
19914    {
19915        return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
19916    }
19917    if !d.segs.is_empty() {
19918        return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
19919    }
19920    if xs.len() != b.saturating_mul(hidden) {
19921        return Err(format!(
19922            "MTP FFN probe: {} input values for {b} × {hidden} rows",
19923            xs.len()
19924        ));
19925    }
19926    if b == 0 {
19927        return Ok(());
19928    }
19929    let mut gate = vec![0.0f32; b * inter];
19930    let mut up = vec![0.0f32; b * inter];
19931    d.gate_proj.matmat(xs, b, &mut gate, pool);
19932    d.up_proj.matmat(xs, b, &mut up, pool);
19933    for row in 0..b {
19934        for i in 0..inter {
19935            let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
19936            let a = a as f64;
19937            mass[i] += a.abs();
19938            squared_mass[i] += a * a;
19939        }
19940    }
19941    Ok(())
19942}
19943
19944fn dense_ffn_batch(
19945    d: &DenseFfn,
19946    xs: &[f32],
19947    b: usize,
19948    pool: Option<&Pool>,
19949    mask_row: Option<&[u8]>,
19950) -> Vec<f32> {
19951    let inter = d.gate_proj.rows();
19952    let hidden = d.down_proj.rows();
19953    // Fused on-device SwiGLU when the device is in play: three separate
19954    // `matmat` calls are three round trips per layer, and the gate/up
19955    // panels (b × inter — 22 MB each at a 512-token chunk) cross the bus
19956    // twice for nothing. The kernel already existed for the image DiT;
19957    // the LLM prefill was simply never wired to it. A task mask needs the
19958    // activations on the host between the halves, so it keeps the CPU
19959    // arm below.
19960    // SiLU/GELU have device arms everywhere they are admitted.  MiniMax
19961    // M3's frozen OAI contract has an additional WGPU-only arm; callers on
19962    // native Metal still get an honest decline from `q4_ffn_act` rather than
19963    // an accidental SiLU evaluation.
19964    let fused_act = d.act.wgpu_graph_act();
19965    if mask_row.is_none()
19966        && fused_act.is_some()
19967        && b >= 32
19968        && crate::gpu::enabled_here()
19969        && !crate::gpu::mm_killed()
19970        // The refit pass needs this layer's activations on the host; the
19971        // fused chain keeps them on the device. Refusing it here costs
19972        // one round trip and keeps every GEMM on the card — the
19973        // alternative was running the whole calibration on the CPU.
19974        && refit_dir().is_none()
19975        // Same for the mass/hit probes. The accumulator at the bottom of
19976        // this function only sees `g` when `g` came back to the host, so
19977        // a fused batch would leave it summing nothing — a probe that
19978        // reports zeros rather than failing, which is worse.
19979        && !ffn_probe_active()
19980        // GPTQ calibration folds every projection's input on the host
19981        // (`QTensor::matmat`); the fused chain would hand gate/up/down
19982        // their inputs on the card and leave the FFN uncalibrated.
19983        && !crate::gptq_capture::capturing()
19984    {
19985        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
19986            d.gate_proj.mapped_q4t(),
19987            d.up_proj.mapped_q4t(),
19988            d.down_proj.mapped_q4t(),
19989        ) {
19990            let mut out = vec![0.0f32; b * hidden];
19991            let act = fused_act.expect("checked above");
19992            if crate::gpu::q4_ffn_act(
19993                model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
19994            ) {
19995                return out;
19996            }
19997        }
19998        // The q4tp twin (same kernel family, scale from the row ladder) —
19999        // the DiT has run it in production since the pipeline containers;
20000        // the LLM prefill was simply never wired to it, so a q4tp model's
20001        // prefill panels stayed on the CPU.
20002        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
20003            d.gate_proj.mapped_q4tp(),
20004            d.up_proj.mapped_q4tp(),
20005            d.down_proj.mapped_q4tp(),
20006        ) {
20007            let mut out = vec![0.0f32; b * hidden];
20008            let act = fused_act.expect("checked above");
20009            if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
20010            {
20011                return out;
20012            }
20013        }
20014        // Every other device codec — int8, or gate/up and down in different
20015        // codecs: the three GEMMs with the panels kept on the card. Without
20016        // it a q8_2f prefill read both b·inter panels home, folded them on
20017        // one host thread, and sent the result back for down.
20018        // CMF_FFN_KEEP=0 keeps the per-GEMM path (A/B).
20019        if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
20020            std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
20021            d.gate_proj.mapped_device_gemm(),
20022            d.up_proj.mapped_device_gemm(),
20023            d.down_proj.mapped_device_gemm(),
20024        ) {
20025            let mut out = vec![0.0f32; b * hidden];
20026            let act = fused_act.expect("checked above");
20027            if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
20028                return out;
20029            }
20030        }
20031    }
20032    let mut g = vec![0.0f32; b * inter];
20033    d.gate_proj.matmat(xs, b, &mut g, pool);
20034    let mut u = vec![0.0f32; b * inter];
20035    d.up_proj.matmat(xs, b, &mut u, pool);
20036    if gate_topk() > 0 && d.act == Act::Silu {
20037        for t in 0..b {
20038            let row = &mut g[t * inter..(t + 1) * inter];
20039            for v in row.iter_mut() {
20040                *v = Act::Silu.combine(*v, 1.0);
20041            }
20042            keep_top_k(row, gate_topk());
20043        }
20044        for i in 0..b * inter {
20045            g[i] *= u[i];
20046        }
20047    } else {
20048        for i in 0..b * inter {
20049            g[i] = d.act.combine(g[i], u[i]);
20050        }
20051    }
20052    if let Some(row) = mask_row {
20053        zero_masked_cols(&mut g, b, inter, row);
20054    }
20055    if oracle_topk() > 0 {
20056        for t in 0..b {
20057            keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
20058        }
20059    }
20060    let mut out = vec![0.0f32; b * hidden];
20061    d.down_proj.matmat(&g, b, &mut out, pool);
20062    if refit_dir().is_some() {
20063        let li = crate::gpu::cur_layer();
20064        if li >= 0 {
20065            refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
20066        }
20067    }
20068    // The DTG-MA probe, on the batched path: one prefill sweep gives the
20069    // same per-neuron statistic the per-position probe does, and on a 27B
20070    // that is minutes instead of hours.
20071    FFN_PROBE.with(|pr| {
20072        if let Some(acc) = pr.borrow_mut().as_mut() {
20073            let li = crate::gpu::cur_layer();
20074            if li < 0 {
20075                return;
20076            }
20077            let Some(row) = acc.get_mut(li as usize) else {
20078                return;
20079            };
20080            let sq = probe_sq();
20081            for t in 0..b {
20082                for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
20083                    *a += if sq {
20084                        (v as f64) * (v as f64)
20085                    } else {
20086                        (v as f64).abs()
20087                    };
20088                }
20089            }
20090        }
20091    });
20092    out
20093}
20094
20095/// Batched MoE-FFN: router batched, positions are GROUPED by expert —
20096/// an expert's weights are read once for all its positions in the chunk
20097/// (the main prefill-GEMM win on MoE: 960MB/token of 35B experts).
20098/// Accumulate per-channel activation energy for `CMF_RMS_TRACE`.
20099fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
20100    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20101    static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20102    let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
20103    let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
20104    if (!on && !dump) || b == 0 {
20105        return;
20106    }
20107    let hidden = xs.len() / b;
20108    if on {
20109        let mut acc = m.act_sq.borrow_mut();
20110        if acc.len() < hidden {
20111            acc.resize(hidden, 0.0);
20112        }
20113        for t in 0..b {
20114            let row = &xs[t * hidden..(t + 1) * hidden];
20115            for (a, &v) in acc.iter_mut().zip(row) {
20116                *a += (v as f64) * (v as f64);
20117            }
20118        }
20119    }
20120    if dump {
20121        // Cap the capture: the covariance needs a few thousand rows, and a
20122        // whole prefill of every layer would be gigabytes for no extra rank.
20123        let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
20124            .ok()
20125            .and_then(|v| v.parse().ok())
20126            .unwrap_or(4096);
20127        let mut rows = m.act_rows.borrow_mut();
20128        if rows.len() < cap * hidden {
20129            let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
20130            rows.extend_from_slice(&xs[..take * hidden]);
20131        }
20132    }
20133}
20134
20135/// Send-able cursor over a Vec-of-Vecs: each pool worker writes only its
20136/// own slots (disjoint by construction in the caller).
20137#[derive(Clone, Copy)]
20138struct SendVecs(*mut Vec<f32>);
20139unsafe impl Send for SendVecs {}
20140unsafe impl Sync for SendVecs {}
20141impl SendVecs {
20142    #[inline]
20143    fn at(self, i: usize) -> *mut Vec<f32> {
20144        unsafe { self.0.add(i) }
20145    }
20146}
20147
20148fn moe_ffn_batch(
20149    m: &MoeFfn,
20150    xs: &[f32],
20151    b: usize,
20152    hidden: usize,
20153    pool: Option<&Pool>,
20154    allowed: Option<&[bool]>,
20155) -> Vec<f32> {
20156    accumulate_act(m, xs, b);
20157    let ne = m.experts.len();
20158    let mut logits = vec![0.0f32; b * ne];
20159    match &m.resonance {
20160        Some(r) => {
20161            let hdim = xs.len() / b.max(1);
20162            for bi in 0..b {
20163                r.scores(
20164                    &xs[bi * hdim..(bi + 1) * hdim],
20165                    &mut logits[bi * ne..(bi + 1) * ne],
20166                );
20167            }
20168        }
20169        None => m.router.matmat(xs, b, &mut logits, pool),
20170    }
20171
20172    // Assignments: expert → [(position, weight)] — same routing as
20173    // moe_ffn, per position (see `moe_route`).
20174    let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
20175    {
20176        let mut st = m.stats.borrow_mut();
20177        if st.len() < ne {
20178            st.resize(ne, 0);
20179        }
20180        for bi in 0..b {
20181            let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
20182            for &e in &idx {
20183                st[e] += 1;
20184                assign[e].push((bi, p[e] / wsum));
20185            }
20186        }
20187    }
20188
20189    let mut out = vec![0.0f32; b * hidden];
20190    let cols = m.experts[0].gate_proj.cols();
20191    let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
20192        let sb = list.len();
20193        let mut sub = vec![0.0f32; sb * cols];
20194        for (k, &(bi, _)) in list.iter().enumerate() {
20195            sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
20196        }
20197        let eo = dense_ffn_batch(d, &sub, sb, pool, None);
20198        for (k, &(bi, w)) in list.iter().enumerate() {
20199            for i in 0..hidden {
20200                out[bi * hidden + i] += w * eo[k * hidden + i];
20201            }
20202        }
20203    };
20204    // Routed experts: the panels are TINY (b·top_k spread over every
20205    // expert — a few positions each), so a pool dispatch per expert is
20206    // pure barrier cost. Invert the parallelism: workers take WHOLE
20207    // experts (serial math inside), then one deterministic scatter in
20208    // expert order — the exact accumulation order the serial loop had.
20209    let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
20210    if pool.is_some() && active.len() >= 8 {
20211        let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
20212        {
20213            let panel_ptr = SendVecs(panels.as_mut_ptr());
20214            // Capture only the expert table: `m` itself carries RefCell
20215            // stats and must not cross the pool boundary.
20216            let experts = &m.experts;
20217            let (active_r, assign_r) = (&active, &assign);
20218            let inherit_cpu = crate::gpu::inherit_cpu_scope();
20219            let run = |start: usize, end: usize| {
20220                let _cpu_scope = inherit_cpu();
20221                // Native Metal: the per-op device helpers (`q4tp_ffn`,
20222                // `q8_matmat`, …) stage through size-keyed shared io
20223                // buffers and are not re-entrant — two workers whose experts
20224                // drew the same row count wrote one buffer at once, so a
20225                // Mellum2.1 prompt came out different on every run. The
20226                // workers keep whole experts on the CPU there; the device
20227                // prefill is the MoE chunk graph.
20228                #[cfg(target_os = "macos")]
20229                let _metal_cpu = crate::gpu::enter_cpu_scope();
20230                for ai in start..end {
20231                    let e = active_r[ai];
20232                    let list = &assign_r[e];
20233                    let sb = list.len();
20234                    let mut sub = vec![0.0f32; sb * cols];
20235                    for (k, &(bi, _)) in list.iter().enumerate() {
20236                        sub[k * cols..(k + 1) * cols]
20237                            .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
20238                    }
20239                    // SAFETY: each worker owns a disjoint panels[ai].
20240                    unsafe {
20241                        *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
20242                    }
20243                }
20244            };
20245            match pool {
20246                Some(p) => p.run_rows(active.len(), &run),
20247                None => run(0, active.len()),
20248            }
20249        }
20250        for (ai, &e) in active.iter().enumerate() {
20251            for (k, &(bi, w)) in assign[e].iter().enumerate() {
20252                let eo = &panels[ai][k * hidden..(k + 1) * hidden];
20253                for i in 0..hidden {
20254                    out[bi * hidden + i] += w * eo[i];
20255                }
20256            }
20257        }
20258    } else {
20259        for &e in &active {
20260            run_expert(&m.experts[e], &assign[e], &mut out);
20261        }
20262    }
20263    if let Some((se, gate)) = &m.shared {
20264        let all: Vec<(usize, f32)> = if let Some(gate) = gate {
20265            let mut gl = vec![0.0f32; b];
20266            gate.matmat(xs, b, &mut gl, pool);
20267            (0..b)
20268                .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
20269                .collect()
20270        } else {
20271            (0..b).map(|bi| (bi, 1.0)).collect()
20272        };
20273        run_expert(se, &all, &mut out);
20274    }
20275    out
20276}
20277
20278/// Decode-exact multi-token MoE — the MiMo speculative verify's FFN. Row
20279/// `r` of the result is bit-identical to `moe_ffn(m, x_r)` on the CPU
20280/// (`moe_ffn_cpu` → `moe_ffn_cpu_batched`): router matvec per row, the same
20281/// routing, the same int8 gate/up/SiLU and down terms
20282/// (`QTensor::moe_gate_up_rows` / `moe_down_rows`), and the row's experts
20283/// summed in ITS route order from 0. What the rows share is the weight
20284/// traffic: each routed expert is read once for every row that picked it.
20285/// (`moe_ffn_batch`, the prompt path, groups the same way but sums in
20286/// expert-index order and runs blocked kernels on wide groups — close, not
20287/// bit-equal to decode.) Any layer the kernels do not cover, or a device
20288/// that could answer `moe_ffn` itself, walks `moe_ffn` row by row.
20289fn moe_ffn_rows_exact(
20290    m: &MoeFfn,
20291    xs: &[f32],
20292    b: usize,
20293    hidden: usize,
20294    pool: Option<&Pool>,
20295) -> Vec<f32> {
20296    let mut out = vec![0.0f32; b * hidden];
20297    let per_row = |out: &mut [f32]| {
20298        for r in 0..b {
20299            let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
20300            out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
20301        }
20302    };
20303    let covered = !crate::gpu::enabled_here()
20304        && moe_batch_enabled()
20305        && m.shared.is_none()
20306        && m.resonance.is_none()
20307        && FFN_PROBE.with(|pr| pr.borrow().is_none())
20308        && m.experts.iter().all(|d| d.act == Act::Silu);
20309    if !covered {
20310        per_row(&mut out);
20311        return out;
20312    }
20313    let ne = m.experts.len();
20314    // Routing, row by row, exactly as `moe_ffn`.
20315    let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
20316    for r in 0..b {
20317        let x = &xs[r * hidden..(r + 1) * hidden];
20318        accumulate_act(m, x, 1);
20319        let mut logits = vec![0.0f32; ne];
20320        m.router.matvec(x, &mut logits, pool);
20321        let (idx, p, wsum) = moe_route(&logits, m, None);
20322        {
20323            let mut st = m.stats.borrow_mut();
20324            if st.len() < ne {
20325                st.resize(ne, 0);
20326            }
20327            for &e in &idx {
20328                st[e] += 1;
20329            }
20330        }
20331        let w: Vec<f32> = idx
20332            .iter()
20333            .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
20334            .collect();
20335        routes.push((idx, w));
20336    }
20337    if routes.iter().any(|(idx, _)| idx.is_empty()) {
20338        per_row(&mut out);
20339        return out;
20340    }
20341    // Group the (row, expert) picks by expert, in first-seen order.
20342    let mut experts: Vec<usize> = Vec::new();
20343    let mut groups: Vec<Vec<usize>> = Vec::new();
20344    for (r, (idx, _)) in routes.iter().enumerate() {
20345        for &e in idx {
20346            match experts.iter().position(|&x| x == e) {
20347                Some(g) => groups[g].push(r),
20348                None => {
20349                    experts.push(e);
20350                    groups.push(vec![r]);
20351                }
20352            }
20353        }
20354    }
20355    let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
20356    let inter = m.experts[experts[0]].gate_proj.rows();
20357    let pairs: Vec<(&QTensor, &QTensor)> = experts
20358        .iter()
20359        .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
20360        .collect();
20361    let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
20362    if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
20363        per_row(&mut out);
20364        return out;
20365    }
20366    let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
20367    let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
20368    let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
20369    if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
20370        per_row(&mut out);
20371        return out;
20372    }
20373    // Where each (row, expert) term landed in the flat pair list.
20374    let mut slot = std::collections::HashMap::with_capacity(n_pairs);
20375    let mut p = 0usize;
20376    for (g, &e) in experts.iter().enumerate() {
20377        for &r in &groups[g] {
20378            slot.insert((r, e), p);
20379            p += 1;
20380        }
20381    }
20382    for (r, (idx, w)) in routes.iter().enumerate() {
20383        let terms: Vec<(&[f32], f32)> = idx
20384            .iter()
20385            .zip(w)
20386            .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
20387            .collect();
20388        let row = &mut out[r * hidden..(r + 1) * hidden];
20389        for (i, dst) in row.iter_mut().enumerate() {
20390            // `moe_down_many`'s per-row sum: from 0, in route order.
20391            let mut acc = 0f32;
20392            for (d, we) in &terms {
20393                acc += we * d[i];
20394            }
20395            *dst = acc;
20396        }
20397    }
20398    out
20399}
20400
20401/// Gemma's final-logit soft-capping, `c·tanh(x/c)`, over a whole vocab row —
20402/// element for element the serial expression, split across the pool: at a
20403/// 262k vocab the serial `tanh` pass was ~5 ms of a 36 ms decode token.
20404pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
20405    match pool {
20406        Some(p) if v.len() >= 1 << 15 => {
20407            let base = v.as_mut_ptr() as usize;
20408            p.run_rows(v.len(), &|s, e| {
20409                // SAFETY: run_rows hands out disjoint [s, e) ranges of `v`,
20410                // which outlives the (blocking) dispatch.
20411                let sl =
20412                    unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
20413                for x in sl {
20414                    *x = c * (*x / c).tanh();
20415                }
20416            });
20417        }
20418        _ => {
20419            for x in v.iter_mut() {
20420                *x = c * (*x / c).tanh();
20421            }
20422        }
20423    }
20424}
20425
20426thread_local! {
20427    /// gate/up activation scratch for the dense FFN paths (single uses
20428    /// two slots, the fused pair all four) — these were fresh
20429    /// intermediate-size Vecs on every layer of every token.
20430    static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
20431        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
20432}
20433
20434/// Dense SwiGLU FFN through QTensor matvecs (any storage).
20435fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20436    // Per-token sparsity, when the file was built for it: gate first,
20437    // then only the chosen neurons' up/down rows leave the mmap.
20438    if gate_topk() > 0
20439        && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
20440    {
20441        return out;
20442    }
20443    // Whole-FFN GPU submit (этап 4.2 increment): gate → silu·up → down
20444    // chained in ONE command buffer with the intermediate activations
20445    // resident on the device — 3 per-op polls become 1 per layer. The
20446    // moe_block backend already implements exactly this chain; a dense
20447    // FFN is one expert with weight 1. Runtime probe: the chain still
20448    // pays one submit+poll per layer — alternate it against the pure-CPU
20449    // FFN and keep whichever is faster on this machine.
20450    // q1 FFNs offload at any practical size: the q1 CPU kernel is
20451    // compute-bound, so the UMA threshold logic does not apply — the
20452    // probe measures and decides either way.
20453    // The fused GPU block has no descriptor-aware Prism path: it would either
20454    // consume an unrotated activation or decline after inspecting the mixed
20455    // q2tp/q4tp tensors.  Do not let that structural refusal enter the FFN
20456    // probe's CPU_ONLY scope; the ordinary body below dispatches each matrix
20457    // through QTensor::matvec, which owns the signed FWHT + affine q2tp route.
20458    let prism_body = d.gate_proj.has_prism_contract()
20459        || d.up_proj.has_prism_contract()
20460        || d.down_proj.has_prism_contract();
20461    if !prism_body
20462        && crate::gpu::enabled_here()
20463        && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
20464    {
20465        let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
20466            crate::gpu::ProbeArm::Gpu
20467        } else {
20468            crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
20469        };
20470        match arm {
20471            crate::gpu::ProbeArm::Gpu => {
20472                let t0 = std::time::Instant::now();
20473                if let Some(out) = dense_ffn_gpu(d, x, pool) {
20474                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
20475                    return out;
20476                }
20477                // Declined: no timing exists, so say so. Silence here is
20478                // what left `ffn` undecided for 9000 calls and cost a
20479                // failed device attempt on half of them.
20480                crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
20481            }
20482            crate::gpu::ProbeArm::CpuTimed => {
20483                let t0 = std::time::Instant::now();
20484                let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20485                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
20486                return out;
20487            }
20488            crate::gpu::ProbeArm::Cpu => {
20489                return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20490            }
20491        }
20492    }
20493    dense_ffn_cpu(d, x, pool)
20494}
20495
20496/// The pure-CPU dense-FFN body (also the fallback of every GPU refusal).
20497fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20498    let inter = d.gate_proj.rows();
20499    FFN_SCRATCH.with(|s| {
20500        let mut s = s.borrow_mut();
20501        let [g, u, ..] = &mut *s;
20502        g.resize(inter, 0.0);
20503        // Fused gate+up+silu: one dispatch, no separate silu pass.
20504        // Falls back to matvec_many + silu loop for unsupported dtypes.
20505        if gate_topk() > 0 {
20506            // Gate first, select, and only then pay for `up`: the
20507            // measurement arm computes both and zeroes the losers, which
20508            // is the same arithmetic.
20509            u.resize(inter, 0.0);
20510            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20511            for i in 0..inter {
20512                g[i] = Act::Silu.combine(g[i], 1.0);
20513            }
20514            keep_top_k(g, gate_topk());
20515            for i in 0..inter {
20516                g[i] *= u[i];
20517            }
20518        } else if d.act == Act::Silu && {
20519            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20520            QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
20521        } {
20522            // g now holds silu(gate)·up directly.
20523        } else {
20524            u.resize(inter, 0.0);
20525            // Multi-matrix job: gate+up under one pool dispatch.
20526            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20527            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20528            let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
20529            for i in 0..inter {
20530                g[i] = d.act.combine(g[i], u[i]);
20531            }
20532        }
20533        // DTG-MA bake probe (Patent 2): accumulate this layer's
20534        // per-neuron activation mass while a probe pass is active.
20535        // `CMF_FFN_PROBE_TOPK=k` switches the statistic from mass to a
20536        // HIT COUNT — how many tokens rank the neuron in their own top
20537        // k. Mass asks "how loud is this neuron overall", the count
20538        // asks "how often does this task actually need it", and the two
20539        // rank neurons differently whenever a few tokens are loud.
20540        FFN_PROBE.with(|pr| {
20541            if let Some(acc) = pr.borrow_mut().as_mut() {
20542                let li = crate::gpu::cur_layer();
20543                if li >= 0 {
20544                    if let Some(row) = acc.get_mut(li as usize) {
20545                        match probe_topk() {
20546                            0 if probe_sq() => {
20547                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20548                                    *a += (v as f64) * (v as f64);
20549                                }
20550                            }
20551                            0 if probe_signed() => {
20552                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20553                                    *a += v as f64;
20554                                }
20555                            }
20556                            0 => {
20557                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20558                                    *a += (v as f64).abs();
20559                                }
20560                            }
20561                            k => {
20562                                let n = g.len();
20563                                let k = k.min(n);
20564                                let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
20565                                let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
20566                                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20567                                });
20568                                let thr = *kth;
20569                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20570                                    if v.abs() >= thr {
20571                                        *a += 1.0;
20572                                    }
20573                                }
20574                            }
20575                        }
20576                    }
20577                }
20578            }
20579        });
20580        if oracle_topk() > 0 {
20581            keep_top_k(g, oracle_topk());
20582        }
20583        {
20584            let li = crate::gpu::cur_layer();
20585            if li >= 0 {
20586                adump_row(li as usize, g);
20587            }
20588        }
20589        let mut out = attention::take_buf(d.down_proj.rows());
20590        let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
20591        d.down_proj.matvec(g, &mut out, pool);
20592        out
20593    })
20594}
20595
20596/// Online accumulators for the AWNP refit of a narrowed FFN.
20597///
20598/// The refit needs `Gss = A_SᵀA_S` and `YA = YᵀA_S` per layer, where `A_S`
20599/// are the calibration activations of the KEPT neurons and `Y` the full
20600/// FFN output. Both are small enough to hold; the thing that is not is
20601/// the activations they are built from — a 27B layer would dump a
20602/// gigabyte per thousand tokens. So they are accumulated as the
20603/// calibration runs and written once at the end.
20604///
20605/// `CMF_FFN_REFIT=<dir>` holds `support.<L>.u32` (a u32 count then the
20606/// kept indices) for every layer to accumulate; `CMF_FFN_REFIT_FROM/TO`
20607/// bound the layer span so the accumulators fit in RAM.
20608pub struct RefitAcc {
20609    pub support: Vec<u32>,
20610    pub gss: Vec<f32>,
20611    pub ya: Vec<f32>,
20612    pub hidden: usize,
20613    pub tokens: u64,
20614    /// Activations staged transposed ([ns, t] and [hidden, t]) until the
20615    /// batch is worth a GEMM. The product costs `ns²` to move and add
20616    /// REGARDLESS of how many tokens went into it, so folding 16 chunks
20617    /// into one call cuts that cost 16× — it was 15 TB of traffic per
20618    /// calibration pass at one call per 256 tokens.
20619    pub buf_g: Vec<f32>,
20620    pub buf_o: Vec<f32>,
20621    pub buf_t: usize,
20622}
20623
20624/// The product buffer is SHARED across layers — one 473 MB allocation,
20625/// not one per layer (that was 30 GB of nothing on a 64-layer model).
20626/// It lives under the same lock as the accumulators.
20627type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
20628
20629static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
20630    std::sync::OnceLock::new();
20631
20632/// Is an FFN probe accumulator installed on this thread? The fused GPU
20633/// FFN must decline while one is, or the probe silently measures zero.
20634fn ffn_probe_active() -> bool {
20635    FFN_PROBE.with(|p| p.borrow().is_some())
20636}
20637
20638fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
20639    REFIT
20640        .get_or_init(|| {
20641            std::env::var("CMF_FFN_REFIT").ok().map(|d| {
20642                (
20643                    d,
20644                    std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
20645                )
20646            })
20647        })
20648        .as_ref()
20649}
20650
20651/// Accumulate one prefill panel into the layer's refit statistics.
20652fn refit_accumulate(
20653    li: usize,
20654    g: &[f32],
20655    b: usize,
20656    inter: usize,
20657    out: &[f32],
20658    hidden: usize,
20659    pool: Option<&Pool>,
20660) {
20661    let Some((dir, map)) = refit_dir() else {
20662        return;
20663    };
20664    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20665    let (from, to) = *SPAN.get_or_init(|| {
20666        let g = |k: &str, d: usize| {
20667            std::env::var(k)
20668                .ok()
20669                .and_then(|v| v.parse().ok())
20670                .unwrap_or(d)
20671        };
20672        (
20673            g("CMF_FFN_REFIT_FROM", 0),
20674            g("CMF_FFN_REFIT_TO", usize::MAX),
20675        )
20676    });
20677    if li < from || li > to {
20678        return;
20679    }
20680    let mut guard = map.lock().unwrap();
20681    let (map, shared) = &mut *guard;
20682    let acc = match map.entry(li) {
20683        std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
20684        std::collections::hash_map::Entry::Vacant(e) => {
20685            let path = format!("{dir}/support.{li}.u32");
20686            let Ok(bytes) = std::fs::read(&path) else {
20687                eprintln!("refit: no {path} — layer {li} skipped");
20688                return;
20689            };
20690            let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
20691            let support: Vec<u32> = bytes[4..4 + n * 4]
20692                .chunks_exact(4)
20693                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
20694                .collect();
20695            eprintln!(
20696                "refit: layer {li} support {n} ({:.0} MB of accumulator)",
20697                (n * n + hidden * n) as f64 * 4.0 / 1e6
20698            );
20699            e.insert(RefitAcc {
20700                gss: vec![0.0; n * n],
20701                ya: vec![0.0; hidden * n],
20702                buf_g: Vec::new(),
20703                buf_o: Vec::new(),
20704                buf_t: 0,
20705                support,
20706                hidden,
20707                tokens: 0,
20708            })
20709        }
20710    };
20711    let ns = acc.support.len();
20712    // Stage this chunk transposed; the GEMM fires once the batch is full.
20713    let cap = refit_batch();
20714    if acc.buf_g.is_empty() {
20715        acc.buf_g = vec![0.0; ns * cap];
20716        acc.buf_o = vec![0.0; hidden * cap];
20717    }
20718    let take = b.min(cap - acc.buf_t);
20719    for t in 0..take {
20720        let col = acc.buf_t + t;
20721        for (j, &n) in acc.support.iter().enumerate() {
20722            acc.buf_g[j * cap + col] = g[t * inter + n as usize];
20723        }
20724        for h in 0..hidden {
20725            acc.buf_o[h * cap + col] = out[t * hidden + h];
20726        }
20727    }
20728    acc.buf_t += take;
20729    acc.tokens += take as u64;
20730    if acc.buf_t < cap {
20731        return;
20732    }
20733    let bt = acc.buf_t;
20734    acc.buf_t = 0;
20735    // The GEMM WRITES its C (it zeroes the accumulators it uses), so the
20736    // chunk product lands in scratch and is added on — the one thing that
20737    // silently turns a Gram over 13 000 tokens into a Gram over 256.
20738    // Both products are `C[n, m] += X[n, b] · Yᵀ[b, m]` with X and Y
20739    // stored row-major [·, b] — exactly `gemm_nt_f32`'s shape, so the
20740    // card does them when it is up (this is the whole calibration's
20741    // cost: O(|S|²) per token, 2.9 PFLOP for a 27B pass). The tiled CPU
20742    // loop stays as the fallback. Neither accumulates, so the product
20743    // lands in scratch and is added on.
20744    let RefitAcc {
20745        gss,
20746        ya,
20747        buf_g,
20748        buf_o,
20749        ..
20750    } = acc;
20751    let need = (ns * ns).max(hidden * ns);
20752    if shared.len() < need {
20753        shared.resize(need, 0.0);
20754    }
20755    let scratch = &mut shared[..];
20756    let _ = bt;
20757    if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
20758        add_into(gss, &scratch[..ns * ns], pool);
20759        if crate::gpu::gemm_nt_f32_transient(
20760            buf_o,
20761            buf_g,
20762            &mut scratch[..hidden * ns],
20763            hidden,
20764            cap,
20765            ns,
20766        ) {
20767            add_into(ya, &scratch[..hidden * ns], pool);
20768        } else {
20769            accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20770        }
20771    } else {
20772        accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
20773        accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20774    }
20775    // No zeroing: the batch is always filled exactly (cap is a multiple
20776    // of the prefill chunk), and a memset of 178 MB a layer would cost
20777    // more than the GEMM.
20778}
20779
20780/// `CMF_FFN_REFIT_BATCH` — tokens staged before each GEMM (default 4096).
20781fn refit_batch() -> usize {
20782    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20783    *B.get_or_init(|| {
20784        std::env::var("CMF_FFN_REFIT_BATCH")
20785            .ok()
20786            .and_then(|v| v.parse().ok())
20787            .unwrap_or(4096)
20788    })
20789}
20790
20791/// `c[m, n] += Σ_t left[m, t]·right[n, t]` — both operands transposed,
20792/// the CPU fallback for the staged batch.
20793fn accum_outer_t(
20794    c: &mut [f32],
20795    m: usize,
20796    n: usize,
20797    b: usize,
20798    left: &[f32],
20799    right: &[f32],
20800    pool: Option<&Pool>,
20801) {
20802    let ptr = SendMut(c.as_mut_ptr());
20803    let body = |i: usize| {
20804        let ptr = &ptr;
20805        let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
20806        for t in 0..b {
20807            let a = left[i * b + t];
20808            if a == 0.0 {
20809                continue;
20810            }
20811            for (j, o) in row.iter_mut().enumerate() {
20812                *o += a * right[j * b + t];
20813            }
20814        }
20815    };
20816    match pool {
20817        Some(p) if m > 1 => p.run_rows(m, &|s, e| {
20818            for i in s..e {
20819                body(i);
20820            }
20821        }),
20822        _ => {
20823            for i in 0..m {
20824                body(i);
20825            }
20826        }
20827    }
20828}
20829
20830/// `dst += src`, spread over the pool — at 118 M floats a layer this is
20831/// not a loop to leave on one core.
20832fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
20833    let n = dst.len().min(src.len());
20834    match pool {
20835        Some(p) if n >= 1 << 16 => {
20836            let ptr = SendMut(dst.as_mut_ptr());
20837            let f = |s: usize, e: usize| {
20838                let ptr = &ptr;
20839                for blk in s..e {
20840                    let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
20841                    for i in a..b {
20842                        unsafe { *ptr.0.add(i) += src[i] };
20843                    }
20844                }
20845            };
20846            p.run_rows(n.div_ceil(4096), &f);
20847        }
20848        _ => {
20849            for (d, v) in dst.iter_mut().zip(&src[..n]) {
20850                *d += *v;
20851            }
20852        }
20853    }
20854}
20855
20856/// `c[m, n] += Σ_t left[t, m]·right[t, n]`, with `left` stored [m, t] and
20857/// `right` [t, n]. Tiled over the rows of `c` so a tile stays in cache
20858/// while each token's `right` row streams past it once, and parallel
20859/// over tiles.
20860fn accum_outer(
20861    c: &mut [f32],
20862    m: usize,
20863    n: usize,
20864    b: usize,
20865    left: &[f32],
20866    right: &[f32],
20867    pool: Option<&Pool>,
20868) {
20869    const TILE: usize = 32;
20870    let tiles = m.div_ceil(TILE);
20871    let cp = SendMut(c.as_mut_ptr());
20872    let body = |ti: usize| {
20873        let cp = &cp;
20874        let i0 = ti * TILE;
20875        let i1 = (i0 + TILE).min(m);
20876        for t in 0..b {
20877            let r = &right[t * n..t * n + n];
20878            for i in i0..i1 {
20879                let a = left[i * b + t];
20880                if a == 0.0 {
20881                    continue;
20882                }
20883                // SAFETY: tiles partition c's rows; workers never overlap.
20884                let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
20885                for (o, v) in row.iter_mut().zip(r) {
20886                    *o += a * *v;
20887                }
20888            }
20889        }
20890    };
20891    match pool {
20892        Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
20893            for ti in s..e {
20894                body(ti);
20895            }
20896        }),
20897        _ => {
20898            for ti in 0..tiles {
20899                body(ti);
20900            }
20901        }
20902    }
20903}
20904
20905/// Write what the calibration accumulated: `gss.<L>.f32` and `ya.<L>.f32`.
20906pub fn refit_flush() -> usize {
20907    let Some((dir, map)) = refit_dir() else {
20908        return 0;
20909    };
20910    let guard = map.lock().unwrap();
20911    let mut n = 0;
20912    for (li, acc) in guard.0.iter() {
20913        // A silently truncated write here is a Gram that reshapes to
20914        // nothing an hour later — say it out loud instead.
20915        let w = |name: &str, v: &[f32]| {
20916            let path = format!("{dir}/{name}.{li}.f32");
20917            let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
20918            match std::fs::write(&path, &bytes) {
20919                Ok(()) => {}
20920                Err(e) => eprintln!(
20921                    "refit: FAILED to write {path} ({} MB): {e}",
20922                    bytes.len() / 1_000_000
20923                ),
20924            }
20925        };
20926        w("gss", &acc.gss);
20927        w("ya", &acc.ya);
20928        println!(
20929            "refit L{li}: {} support, {} tokens, hidden {}",
20930            acc.support.len(),
20931            acc.tokens,
20932            acc.hidden
20933        );
20934        n += 1;
20935    }
20936    n
20937}
20938
20939/// `CMF_FFN_ADUMP=<prefix>` — append every probed token's FFN activation
20940/// row to `<prefix>.<layer>.f16`. The co-activation record: which
20941/// neurons fire together, which is what a tube has to group if a token
20942/// is ever going to open one tube instead of sixteen.
20943fn adump_row(li: usize, g: &[f32]) {
20944    use std::io::Write as _;
20945    static FILES: std::sync::OnceLock<
20946        Option<(
20947            String,
20948            std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
20949        )>,
20950    > = std::sync::OnceLock::new();
20951    let Some((prefix, map)) = FILES
20952        .get_or_init(|| {
20953            std::env::var("CMF_FFN_ADUMP")
20954                .ok()
20955                .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
20956        })
20957        .as_ref()
20958    else {
20959        return;
20960    };
20961    // `CMF_FFN_ADUMP_FROM/_TO` narrow the dump to a layer span, so a big
20962    // calibration run fits on disk in a few passes instead of one.
20963    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20964    let (from, to) = *SPAN.get_or_init(|| {
20965        let g = |k: &str, d: usize| {
20966            std::env::var(k)
20967                .ok()
20968                .and_then(|v| v.parse().ok())
20969                .unwrap_or(d)
20970        };
20971        (
20972            g("CMF_FFN_ADUMP_FROM", 0),
20973            g("CMF_FFN_ADUMP_TO", usize::MAX),
20974        )
20975    });
20976    if li < from || li > to {
20977        return;
20978    }
20979    let mut map = map.lock().unwrap();
20980    let f = map.entry(li).or_insert_with(|| {
20981        std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
20982    });
20983    let mut bytes = Vec::with_capacity(g.len() * 2);
20984    for v in g {
20985        bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
20986    }
20987    let _ = f.write_all(&bytes);
20988}
20989
20990/// `CMF_FFN_ORACLE_TOPK` — keep only the k largest |silu(g)·u| of each
20991/// token and zero the rest. Not a serving mode: it is the CEILING of
20992/// contextual sparsity — what a per-token router would be chasing —
20993/// measured by cheating, since the selection reads the very activations
20994/// it would have to predict.
20995fn oracle_topk() -> usize {
20996    if exact_mtp_calibration_active() {
20997        return 0;
20998    }
20999    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21000    *K.get_or_init(|| {
21001        std::env::var("CMF_FFN_ORACLE_TOPK")
21002            .ok()
21003            .and_then(|v| v.parse().ok())
21004            .unwrap_or(0)
21005    })
21006}
21007
21008/// `CMF_FFN_GATE_TOPK` — the REALIZABLE cousin of the oracle: rank the
21009/// neurons by their gate alone (which the kernel has computed anyway
21010/// before it reads `up`), keep the k best, and drop the rest. Every
21011/// dropped neuron's `up` row and `down` column stay unread, so this is
21012/// the sparsity a serving path can actually take without a router.
21013fn gate_topk() -> usize {
21014    if exact_mtp_calibration_active() {
21015        return 0;
21016    }
21017    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21018    *K.get_or_init(|| {
21019        std::env::var("CMF_FFN_GATE_TOPK")
21020            .ok()
21021            .and_then(|v| v.parse().ok())
21022            .unwrap_or(0)
21023    })
21024}
21025
21026/// `CMF_FFN_GATE_BLOCK` — select in blocks of B neurons instead of one
21027/// by one. A scattered per-neuron choice cannot be read efficiently (a
21028/// row at a time, no prefetch runway); a block of 32 is a contiguous
21029/// 32-row slab of `up` and of the transposed `down`, which the ordinary
21030/// kernels stream. The question the measurement answers is what the
21031/// block costs in quality.
21032fn gate_block() -> usize {
21033    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21034    *B.get_or_init(|| {
21035        std::env::var("CMF_FFN_GATE_BLOCK")
21036            .ok()
21037            .and_then(|v| v.parse().ok())
21038            .unwrap_or(1)
21039    })
21040}
21041
21042/// Zero all but the `k` largest BLOCKS (by summed square) of a row.
21043fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
21044    let n = g.len();
21045    let nb = n.div_ceil(block);
21046    let kb = (keep_n.div_ceil(block)).clamp(1, nb);
21047    if kb >= nb {
21048        return;
21049    }
21050    let mut score: Vec<f32> = (0..nb)
21051        .map(|b| {
21052            g[b * block..((b + 1) * block).min(n)]
21053                .iter()
21054                .map(|v| v * v)
21055                .sum::<f32>()
21056        })
21057        .collect();
21058    let mut ord = score.clone();
21059    let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
21060        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21061    });
21062    let thr = *kth;
21063    for b in 0..nb {
21064        if score[b] < thr {
21065            g[b * block..((b + 1) * block).min(n)].fill(0.0);
21066        }
21067    }
21068    score.clear();
21069}
21070
21071/// Zero all but the `k` largest magnitudes of one token's activation row.
21072fn keep_top_k(g: &mut [f32], k: usize) {
21073    if gate_block() > 1 {
21074        return keep_top_blocks(g, k, gate_block());
21075    }
21076    let n = g.len();
21077    if k == 0 || k >= n {
21078        return;
21079    }
21080    let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
21081    let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
21082        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21083    });
21084    let thr = *kth;
21085    for v in g.iter_mut() {
21086        if v.abs() < thr {
21087            *v = 0.0;
21088        }
21089    }
21090}
21091
21092/// `CMF_FFN_PROBE_SQ` — accumulate Σa², so the dump divided by the token
21093/// count and square-rooted is the RMS activation trace Patent 12 weights
21094/// its matrices by.
21095fn probe_sq() -> bool {
21096    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21097    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
21098}
21099
21100/// `CMF_FFN_PROBE_SIGNED` — accumulate the SIGNED activation sum
21101/// instead of its magnitude: what a dropped neuron contributes ON
21102/// AVERAGE, which is the bias a narrowed FFN can add back for free.
21103fn probe_signed() -> bool {
21104    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21105    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
21106}
21107
21108/// `CMF_FFN_MEANFILL=<file>` — a masked-out neuron contributes its MEAN
21109/// activation instead of zero (`u32 layers, u32 inter, f32[…]`, the mass
21110/// dump layout, holding per-neuron means). Dropping a neuron outright
21111/// also drops its average contribution, which shifts the layer output by
21112/// a constant; filling the mean back is one add per layer and costs no
21113/// bytes off the bus. This is the measurement arm — in a tube file the
21114/// same correction ships as a per-task bias vector.
21115fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
21116    static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
21117    M.get_or_init(|| {
21118        let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
21119        let b = std::fs::read(&p).ok()?;
21120        let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
21121        let vals: Vec<f32> = b[8..]
21122            .chunks_exact(4)
21123            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
21124            .collect();
21125        eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
21126        Some((inter, vals))
21127    })
21128    .as_ref()
21129}
21130
21131/// `CMF_FFN_PROBE_TOPK` — 0 (default) = accumulate mass, k>0 = count
21132/// how often a neuron lands in a token's top k.
21133fn probe_topk() -> usize {
21134    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21135    *K.get_or_init(|| {
21136        std::env::var("CMF_FFN_PROBE_TOPK")
21137            .ok()
21138            .and_then(|v| v.parse().ok())
21139            .unwrap_or(0)
21140    })
21141}
21142
21143thread_local! {
21144    /// DTG-MA activation probe: per-layer per-neuron Σ|silu(g)·u|
21145    /// accumulator, alive only during `Pipeline::probe_ffn_mass`.
21146    static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
21147        const { std::cell::RefCell::new(None) };
21148}
21149
21150/// Per-token structured sparsity, paid for in bytes.
21151///
21152/// The gate is the cheapest third of an FFN and it already says which
21153/// neurons matter: `silu(gate)` near zero means the neuron contributes
21154/// nothing whatever `up` says. So compute every gate, keep the `k`
21155/// loudest, and read ONLY those neurons' `up` rows and `down` rows —
21156/// the latter needs `down_proj` stored transposed, otherwise a neuron's
21157/// down weights are a strided column and "reading only those" costs a
21158/// full cache line each.
21159///
21160/// Returns `None` when the file has no transposed `down` (the caller
21161/// then runs the ordinary dense path).
21162fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
21163    // The scatter path reads individual rows/columns and cannot express the
21164    // per-matrix signed FWHT boundary.  Let the descriptor-aware dense path
21165    // handle Prism files rather than silently running an unrotated sparse
21166    // approximation.
21167    if d.gate_proj.has_prism_contract()
21168        || d.up_proj.has_prism_contract()
21169        || d.down_proj.has_prism_contract()
21170    {
21171        return None;
21172    }
21173    let dt = d.down_t.as_ref()?;
21174    let inter = d.gate_proj.rows();
21175    let hidden = dt.cols();
21176    if k == 0 || k >= inter || d.act != Act::Silu {
21177        return None;
21178    }
21179    DYN_SCRATCH.with(|sc| {
21180        let mut sc = sc.borrow_mut();
21181        let DynScratch {
21182            g,
21183            mag,
21184            live,
21185            parts,
21186        } = &mut *sc;
21187        g.resize(inter, 0.0);
21188        d.gate_proj.matvec(x, g, pool);
21189        for v in g.iter_mut() {
21190            *v = inference::silu(*v);
21191        }
21192        // The k-th largest |silu(gate)| is the threshold; ties keep more,
21193        // which is the safe side.
21194        mag.clear();
21195        mag.extend(g.iter().map(|v| v.abs()));
21196        let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
21197            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21198        });
21199        let thr = *kth;
21200        live.clear();
21201        live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
21202        let mut out = vec![0.0f32; hidden];
21203        match pool {
21204            Some(p) if live.len() >= 64 => {
21205                let nw = p.n_workers() + 1;
21206                parts.clear();
21207                parts.resize(nw * hidden, 0.0);
21208                let ptr = SendMut(parts.as_mut_ptr());
21209                let n = live.len();
21210                let live_ref: &[u32] = live;
21211                let g_ref: &[f32] = g;
21212                p.run(&|w, workers| {
21213                    let chunk = n.div_ceil(workers);
21214                    let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
21215                    if s >= e {
21216                        return;
21217                    }
21218                    WORKER_SCRATCH.with(|ws| {
21219                        let mut ws = ws.borrow_mut();
21220                        let [scratch, acc] = &mut *ws;
21221                        scratch.resize(hidden.max(x.len()), 0.0);
21222                        acc.clear();
21223                        acc.resize(hidden, 0.0);
21224                        for (o, &nrm) in live_ref[s..e].iter().enumerate() {
21225                            // One neuron of runway: the next row's lines
21226                            // start moving while this one is multiplied.
21227                            if let Some(&nx) = live_ref[s..e].get(o + 1) {
21228                                d.up_proj.prefetch_row(nx as usize);
21229                                dt.prefetch_row(nx as usize);
21230                            }
21231                            let idx = nrm as usize;
21232                            let up = d.up_proj.row_dot(idx, x, scratch);
21233                            let a = g_ref[idx] * up;
21234                            if a != 0.0 {
21235                                dt.add_row_scaled(idx, a, acc, scratch);
21236                            }
21237                        }
21238                        for (j, v) in acc.iter().enumerate() {
21239                            unsafe { *ptr.at(w * hidden + j) = *v };
21240                        }
21241                    });
21242                });
21243                for w in 0..nw {
21244                    for (j, o) in out.iter_mut().enumerate() {
21245                        *o += parts[w * hidden + j];
21246                    }
21247                }
21248            }
21249            _ => {
21250                WORKER_SCRATCH.with(|ws| {
21251                    let mut ws = ws.borrow_mut();
21252                    let [scratch, _acc] = &mut *ws;
21253                    scratch.resize(hidden.max(x.len()), 0.0);
21254                    for &nrm in live.iter() {
21255                        let idx = nrm as usize;
21256                        let up = d.up_proj.row_dot(idx, x, scratch);
21257                        let a = g[idx] * up;
21258                        if a != 0.0 {
21259                            dt.add_row_scaled(idx, a, &mut out, scratch);
21260                        }
21261                    }
21262                });
21263            }
21264        }
21265        Some(out)
21266    })
21267}
21268
21269/// Caller-side scratch of the dynamic path — one allocation per thread,
21270/// not one per layer per token (that alone cost a third of the decode).
21271struct DynScratch {
21272    g: Vec<f32>,
21273    mag: Vec<f32>,
21274    live: Vec<u32>,
21275    parts: Vec<f32>,
21276}
21277
21278thread_local! {
21279    static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
21280        std::cell::RefCell::new(DynScratch {
21281            g: Vec::new(),
21282            mag: Vec::new(),
21283            live: Vec::new(),
21284            parts: Vec::new(),
21285        })
21286    };
21287    /// Pool-worker scratch: the row buffer and this worker's partial sum.
21288    static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
21289        const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
21290}
21291
21292/// `dense_ffn_cpu` with a per-visit mask landing on the activations —
21293/// the masked-inference fast path's decode arm. Full fused quant
21294/// compute, closed neurons zeroed before down: arithmetically the
21295/// pruned network, no dequant, no weight bytes touched.
21296fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
21297    let inter = d.gate_proj.rows();
21298    FFN_SCRATCH.with(|s| {
21299        let mut s = s.borrow_mut();
21300        let [g, u, ..] = &mut *s;
21301        g.resize(inter, 0.0);
21302        if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
21303            // g holds silu(gate)·up.
21304        } else {
21305            u.resize(inter, 0.0);
21306            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
21307            for i in 0..inter {
21308                g[i] = d.act.combine(g[i], u[i]);
21309            }
21310        }
21311        zero_masked_cols(g, 1, inter, mask_row);
21312        let mut out = attention::take_buf(d.down_proj.rows());
21313        d.down_proj.matvec(g, &mut out, pool);
21314        out
21315    })
21316}
21317
21318/// Dense FFN as one GPU submission via the MoE block path (single
21319/// expert, weight 1.0): gate → silu·up → down chained in one command
21320/// buffer, intermediate activations device-resident. None → weights
21321/// not q8-mapped in the primary shard / over the VRAM budget / backend
21322/// refusal → honest CPU path.
21323fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
21324    if d.gate_proj.has_prism_contract()
21325        || d.up_proj.has_prism_contract()
21326        || d.down_proj.has_prism_contract()
21327    {
21328        return None;
21329    }
21330    // The GPU block applies the standard graph activations plus the exact
21331    // MiniMax-M3 OAI contract; any other activation stays on the CPU.
21332    d.act.moe_job_graph_contract()?;
21333    // Threshold: tiny FFNs are not worth a submission (q1 excepted —
21334    // see the caller's gate).
21335    if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
21336        return None;
21337    }
21338    let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
21339    let mut model_ref = None;
21340    moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
21341    let model = model_ref?;
21342    let hidden = jobs[0].down.1;
21343    let mut out = attention::take_buf(hidden);
21344    if crate::gpu::moe_block(&model, &jobs, &mut out) {
21345        Some(out)
21346    } else {
21347        let mut out = out;
21348        attention::recycle_buf(&mut out);
21349        None
21350    }
21351}
21352
21353/// q8-mapped primary-shard tensor parts for a GPU job: q8_2f carries
21354/// its column field, q8_row runs with empty col slices (the backend
21355/// skips the multiply). Shared by the MoE block and the dense-FFN
21356/// single-job path.
21357#[allow(clippy::type_complexity)]
21358#[allow(clippy::type_complexity)]
21359pub(crate) fn moe_parts(
21360    t: &QTensor,
21361) -> Option<(
21362    &std::sync::Arc<cortiq_core::CmfModel>,
21363    usize,
21364    usize,
21365    usize,
21366    &[f32],
21367    &[f32],
21368    bool,
21369    bool,
21370    bool,
21371)> {
21372    match t {
21373        QTensor::Mapped {
21374            model,
21375            idx,
21376            dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
21377            rows,
21378            cols,
21379            row_scale,
21380            col_field,
21381            ..
21382        } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
21383            model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
21384        )),
21385        // q1: tile-embedded scales — empty rs/col slices, raw xs.
21386        QTensor::Mapped {
21387            model,
21388            idx,
21389            dtype: cortiq_core::TensorDtype::Q1,
21390            rows,
21391            cols,
21392            ..
21393        } => Some((
21394            model,
21395            *idx,
21396            *rows,
21397            *cols,
21398            &[][..],
21399            &[][..],
21400            true,
21401            false,
21402            false,
21403        )),
21404        // q4_tiled: 18-byte tiles with embedded f16 scales — raw xs.
21405        QTensor::Mapped {
21406            model,
21407            idx,
21408            dtype: cortiq_core::TensorDtype::Q4Tiled,
21409            rows,
21410            cols,
21411            ..
21412        } => Some((
21413            model,
21414            *idx,
21415            *rows,
21416            *cols,
21417            &[][..],
21418            &[][..],
21419            false,
21420            true,
21421            false,
21422        )),
21423        // q4tp: same raw-xs contract, different stride and scale plane.
21424        QTensor::Mapped {
21425            model,
21426            idx,
21427            dtype: cortiq_core::TensorDtype::Q4TiledP,
21428            rows,
21429            cols,
21430            ..
21431        } => Some((
21432            model,
21433            *idx,
21434            *rows,
21435            *cols,
21436            &[][..],
21437            &[][..],
21438            false,
21439            true,
21440            false,
21441        )),
21442        // q2tp: the 2-bit expert plane of the mixed profile — q4 family
21443        // for stride bookkeeping, flagged q2 so the trio validation can
21444        // demand a q4tp down.
21445        QTensor::Mapped {
21446            model,
21447            idx,
21448            dtype: cortiq_core::TensorDtype::Q2TiledP,
21449            rows,
21450            cols,
21451            ..
21452        } => Some((
21453            model,
21454            *idx,
21455            *rows,
21456            *cols,
21457            &[][..],
21458            &[][..],
21459            false,
21460            true,
21461            true,
21462        )),
21463        _ => None,
21464    }
21465}
21466
21467/// The routed experts of a MoE layer as the Metal chunk prefill takes
21468/// them: f32 router rows and every expert's q4tp (gate, up, down) tensor
21469/// indices, uniform SiLU shapes. Only the plain routed form — no shared
21470/// expert, no per-expert scale, no resonance or τ router, no router input
21471/// norm; the batched host walk keeps the rest. The routing itself stays the
21472/// host's (`moe_route`), so mask and selection bias need nothing here.
21473#[cfg(target_os = "macos")]
21474#[allow(clippy::type_complexity)]
21475fn chunk_moe_parts(
21476    m: &MoeFfn,
21477    hidden: usize,
21478) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
21479    if m.shared.is_some()
21480        || m.per_expert_scale.is_some()
21481        || m.resonance.is_some()
21482        || m.route_tau.is_some()
21483        || m.router_input_norm
21484        || m.experts.is_empty()
21485        || m.top_k == 0
21486        || std::env::var("CMF_RMS_TRACE").is_ok()
21487        || std::env::var("CMF_ACT_DUMP").is_ok()
21488        || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
21489    {
21490        return None;
21491    }
21492    let (rf, rr, rc) = m.router.f32_parts()?;
21493    if rr != m.experts.len() || rc != hidden {
21494        return None;
21495    }
21496    let inter = m.experts[0].gate_proj.rows();
21497    // All-q8_2f experts (Mellum2.1 q8_2f) ride the `moe_q8` GEMMs;
21498    // `CMF_METAL_MOE_Q8=0` keeps them on the host walk.
21499    let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
21500    let experts = m
21501        .experts
21502        .iter()
21503        .map(|e| {
21504            if e.act != Act::Silu
21505                || !e.segs.is_empty()
21506                || e.gate_proj.rows() != inter
21507                || e.up_proj.rows() != inter
21508                || e.down_proj.cols() != inter
21509                || e.gate_proj.cols() != hidden
21510                || e.down_proj.rows() != hidden
21511            {
21512                return None;
21513            }
21514            if q8 {
21515                let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21516                return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21517            }
21518            Some((
21519                e.gate_proj.mapped_q4tp()?.1,
21520                e.up_proj.mapped_q4tp()?.1,
21521                e.down_proj.mapped_q4tp()?.1,
21522            ))
21523        })
21524        .collect::<Option<Vec<_>>>()?;
21525    Some((rf, experts, q8))
21526}
21527
21528/// Map a MoE onto the Metal token graph's contract: f32 router, a
21529/// shared expert (gated — Qwen — or ungated at weight 1 — DeepSeek-V3 /
21530/// HunYuan hy_v3), softmax or sigmoid scores with an optional selection
21531/// bias and routed scale, experts uniformly q4tp (or the mixed profile:
21532/// q2tp gate/up over a q4tp down). τ routers, masks, per-expert scales
21533/// and Gemma's router-input norm refuse here — those semantics stay on
21534/// the CPU path.
21535/// Map Flint's released ragged MoE into the native page graph.  This stays
21536/// separate from `metal_moe_graph_parts`: its six routed slots are Q2TP and
21537/// 512-wide, while its ungated shared SwiGLU is Q4TP and 1024-wide.  Feeding
21538/// the shared tuple into the uniform jobs descriptor would make the shader
21539/// use the routed stride on a different payload.
21540#[cfg(target_os = "macos")]
21541fn metal_flint_moe_graph_parts(
21542    m: &MoeFfn,
21543    hidden: usize,
21544    scratch_key: usize,
21545) -> Option<crate::gpu::GpuFlintMoe<'_>> {
21546    use std::sync::Arc;
21547
21548    const HIDDEN: usize = 2_048;
21549    const ROUTED_INTER: usize = 512;
21550    const SHARED_INTER: usize = 1_024;
21551    const EXPERTS: usize = 256;
21552    const TOP_K: usize = 6;
21553
21554    if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
21555        return None;
21556    }
21557    let routed0 = m.experts.first()?;
21558    let (model, _) = routed0.gate_proj.mapped_q2tp()?;
21559    let (router, router_rows, router_cols) = m.router.f32_parts()?;
21560    if router_rows != EXPERTS || router_cols != HIDDEN {
21561        return None;
21562    }
21563    if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
21564        return None;
21565    }
21566    let same_q2 = |t: &QTensor| -> Option<usize> {
21567        let (owner, idx) = t.mapped_q2tp()?;
21568        Arc::ptr_eq(&owner, &model).then_some(idx)
21569    };
21570    let same_q4 = |t: &QTensor| -> Option<usize> {
21571        let (owner, idx) = t.mapped_q4tp()?;
21572        Arc::ptr_eq(&owner, &model).then_some(idx)
21573    };
21574    let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21575        if e.act != Act::Silu
21576            || !e.segs.is_empty()
21577            || e.gate_proj.rows() != ROUTED_INTER
21578            || e.gate_proj.cols() != HIDDEN
21579            || e.up_proj.rows() != ROUTED_INTER
21580            || e.up_proj.cols() != HIDDEN
21581            || e.down_proj.rows() != HIDDEN
21582            || e.down_proj.cols() != ROUTED_INTER
21583        {
21584            return None;
21585        }
21586        Some((
21587            same_q2(&e.gate_proj)?,
21588            same_q2(&e.up_proj)?,
21589            same_q4(&e.down_proj)?,
21590        ))
21591    };
21592    let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21593        if e.act != Act::Silu
21594            || !e.segs.is_empty()
21595            || e.gate_proj.rows() != SHARED_INTER
21596            || e.gate_proj.cols() != HIDDEN
21597            || e.up_proj.rows() != SHARED_INTER
21598            || e.up_proj.cols() != HIDDEN
21599            || e.down_proj.rows() != HIDDEN
21600            || e.down_proj.cols() != SHARED_INTER
21601        {
21602            return None;
21603        }
21604        Some((
21605            same_q4(&e.gate_proj)?,
21606            same_q4(&e.up_proj)?,
21607            same_q4(&e.down_proj)?,
21608        ))
21609    };
21610    let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
21611    if experts.len() != EXPERTS {
21612        return None;
21613    }
21614    let (shared_ffn, shared_gate) = m.shared.as_ref()?;
21615    if shared_gate.is_some() {
21616        return None;
21617    }
21618    Some(crate::gpu::GpuFlintMoe {
21619        router,
21620        bias: m.expert_bias.as_deref(),
21621        experts,
21622        scratch_key,
21623        shared: shared(shared_ffn)?,
21624        top_k: m.top_k,
21625        norm_topk: m.norm_topk_prob,
21626        route_scale: m.routed_scaling,
21627        sigmoid: m.router_sigmoid,
21628        sigmoid_denom_eps: m.sigmoid_denom_eps,
21629    })
21630}
21631
21632#[cfg(target_os = "macos")]
21633fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
21634    if m.router_input_norm
21635        || m.route_tau.is_some()
21636        || m.mask.is_some()
21637        || m.per_expert_scale.is_some()
21638        || m.experts.is_empty()
21639        || m.top_k == 0
21640        || m.resonance.is_some()
21641    {
21642        return None;
21643    }
21644    // Without a shared expert the select kernel fills the k routed jobs
21645    // only (flag 32). Measured on Mellum2.1's softmax router; the sigmoid /
21646    // biased routers without one (LFM2-MoE) keep the CPU path until they
21647    // are measured too.
21648    let (sh, sg) = match &m.shared {
21649        Some((sh, sg)) => (Some(sh), sg.as_ref()),
21650        None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
21651        None => return None,
21652    };
21653    let (rf, rr, rc) = m.router.f32_parts()?;
21654    if rr != m.experts.len() || rc != hidden {
21655        return None;
21656    }
21657    let shared_gated = sg.is_some();
21658    let sf = match sg {
21659        Some(sg) => {
21660            let (sf, sr, sc) = sg.f32_parts()?;
21661            if sr * sc != hidden {
21662                return None;
21663            }
21664            sf
21665        }
21666        // Ungated: the router's first row stands in for the gate matvec
21667        // (its logit is never read — the kernel pins weight 1).
21668        None => &rf[..hidden],
21669    };
21670    if let Some(b) = &m.expert_bias {
21671        if b.len() != m.experts.len() {
21672            return None;
21673        }
21674    }
21675    let inter = m.experts[0].gate_proj.rows();
21676    // The first expert's gate decides the profile; every trio (shared
21677    // included) must agree — the jobs ladder flips ONE kernel for all.
21678    let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
21679    // All-q8_2f routed experts (Mellum2.1 q8_2f: no shared expert, the
21680    // contract they were measured on) take the `moe_q8` kernels;
21681    // `CMF_METAL_MOE_Q8=0` keeps them off the graph (per-op path).
21682    let q8 = crate::gpu_metal::moe_q8::enabled()
21683        && m.shared.is_none()
21684        && m.experts[0].gate_proj.q8_2f_parts().is_some();
21685    let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21686        if e.act != Act::Silu
21687            || e.gate_proj.rows() != inter
21688            || e.gate_proj.cols() != hidden
21689            || e.up_proj.rows() != inter
21690            || e.up_proj.cols() != hidden
21691            || e.down_proj.rows() != hidden
21692            || e.down_proj.cols() != inter
21693        {
21694            return None;
21695        }
21696        if q8 {
21697            let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21698            return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21699        }
21700        let pick = |t: &QTensor| -> Option<usize> {
21701            if gu_q2 {
21702                t.mapped_q2tp().map(|(_, i)| i)
21703            } else {
21704                t.mapped_q4tp().map(|(_, i)| i)
21705            }
21706        };
21707        Some((
21708            pick(&e.gate_proj)?,
21709            pick(&e.up_proj)?,
21710            e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
21711        ))
21712    };
21713    let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
21714    let shared = match sh {
21715        Some(sh) => Some(trio(sh)?),
21716        None => None,
21717    };
21718    Some(crate::gpu::GpuMoe {
21719        router: rf,
21720        sgate: sf,
21721        experts,
21722        shared,
21723        n_exp: m.experts.len(),
21724        top_k: m.top_k,
21725        inter,
21726        norm_topk: m.norm_topk_prob,
21727        route_scale: m.routed_scaling,
21728        gu_q2,
21729        q8,
21730        sigmoid: m.router_sigmoid,
21731        sigmoid_denom_eps: m.sigmoid_denom_eps,
21732        bias: m.expert_bias.as_deref(),
21733        shared_gated,
21734    })
21735}
21736
21737/// `CMF_FORCE_IDS=<file>` (diagnostics): decode step n commits the n-th
21738/// whitespace-separated token id of the file instead of sampling, while the
21739/// logits are still computed (and dumped by `CMF_LOGIT_DUMP_ALL`) — one
21740/// backend teacher-forced along another's greedy sequence, so top-1
21741/// agreement can be counted past a near-tie divergence.
21742fn forced_ids_at(step: usize) -> Option<u32> {
21743    static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
21744    IDS.get_or_init(|| {
21745        std::env::var("CMF_FORCE_IDS")
21746            .ok()
21747            .and_then(|p| std::fs::read_to_string(p).ok())
21748            .map(|s| {
21749                s.split_whitespace()
21750                    .filter_map(|t| t.parse().ok())
21751                    .collect()
21752            })
21753            .unwrap_or_default()
21754    })
21755    .get(step)
21756    .copied()
21757}
21758
21759/// Build one gate/up/down GPU job from three tensors. `moe_push_job` is the
21760/// DenseFfn-shaped caller; architectures that keep their experts in their own
21761/// structs (DeepSeek-V4) come here directly.
21762pub(crate) fn moe_push_job_parts<'a>(
21763    gate: &'a QTensor,
21764    up: &'a QTensor,
21765    down: &'a QTensor,
21766    x: &[f32],
21767    w: f32,
21768    swiglu_limit: f32,
21769    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21770    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21771) -> Option<()> {
21772    use crate::qtensor::prescale;
21773    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
21774    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
21775    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
21776    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21777        return None; // mixed-dtype trio — honest CPU path
21778    }
21779    // The 2-bit profile is gate/up q2tp over a PLAIN q4tp down; any other
21780    // 2-bit arrangement stays on the CPU.
21781    if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
21782        return None;
21783    }
21784    if !gq2 && dq2 {
21785        return None;
21786    }
21787    model_ref.get_or_insert_with(|| gm.clone());
21788    let dt = |cf: &[f32]| {
21789        if cf.is_empty() {
21790            cortiq_core::TensorDtype::Q8Row
21791        } else {
21792            cortiq_core::TensorDtype::Q8_2f
21793        }
21794    };
21795    jobs.push(crate::gpu::MoeJob {
21796        gate: (gi, gr, gc, grs),
21797        up: (ui, ur, uc, urs),
21798        down: (di, dr, dc, drs),
21799        xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
21800        xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
21801        down_col: dcf,
21802        w,
21803        q1: gq1,
21804        q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
21805        q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
21806        gu_q2: gq2,
21807        swiglu_limit,
21808        act: 0,
21809    });
21810    Some(())
21811}
21812
21813/// Build one gate/up/down GPU job (see `moe_parts`).
21814fn moe_push_job<'a>(
21815    d: &'a DenseFfn,
21816    x: &[f32],
21817    w: f32,
21818    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21819    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21820) -> Option<()> {
21821    use crate::qtensor::prescale;
21822    // The block's act(gate)·up pass takes an exact device contract.  For
21823    // ordinary SiLU/GELU this is a zero clamp; MiniMax M3 contributes its
21824    // dedicated OAI marker and frozen 7.0 source limit.  Anything without a
21825    // device contract stays on the CPU.
21826    let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
21827    let act = graph_act.code();
21828    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
21829    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
21830    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
21831    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21832        return None; // mixed-dtype trio — honest CPU path
21833    }
21834    if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
21835        return None;
21836    }
21837    if !gq2 && dq2 {
21838        return None;
21839    }
21840    model_ref.get_or_insert_with(|| gm.clone());
21841    let gdt = if gcf.is_empty() {
21842        cortiq_core::TensorDtype::Q8Row
21843    } else {
21844        cortiq_core::TensorDtype::Q8_2f
21845    };
21846    let udt = if ucf.is_empty() {
21847        cortiq_core::TensorDtype::Q8Row
21848    } else {
21849        cortiq_core::TensorDtype::Q8_2f
21850    };
21851    jobs.push(crate::gpu::MoeJob {
21852        gate: (gi, gr, gc, grs),
21853        up: (ui, ur, uc, urs),
21854        down: (di, dr, dc, drs),
21855        xs_gate: prescale(x, gcf, gdt).into_owned(),
21856        xs_up: prescale(x, ucf, udt).into_owned(),
21857        down_col: dcf,
21858        w,
21859        q1: gq1,
21860        q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
21861        q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
21862        gu_q2: gq2,
21863        swiglu_limit,
21864        act,
21865    });
21866    Some(())
21867}
21868
21869/// Sparse dense-FFN directly on QUANTIZED weights (mask × mmap): reads
21870/// ONLY the active neurons' gate/up rows and down columns from the mmap
21871/// — no full-matrix dequant, no f32 model copy. This is what lets a
21872/// masked big model run at quantized RSS (the historical mask path
21873/// forced the whole model to f32). Semantics identical to the f32
21874/// sparse path within quant tolerance.
21875fn sparse_ffn_quant(
21876    d: &DenseFfn,
21877    x: &[f32],
21878    active: &[u16],
21879    hidden: usize,
21880    pool: Option<&Pool>,
21881) -> Vec<f32> {
21882    let n = active.len();
21883    let inter = d.gate_proj.rows();
21884    let mut act = vec![0.0f32; n];
21885    // Scratch is needed if EITHER projection is group-packed (q4/vbit);
21886    // gate/up normally share a dtype but sizing on both is robust.
21887    let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
21888    let compute = |ai: usize| -> f32 {
21889        let idx = active[ai] as usize;
21890        if idx >= inter {
21891            return 0.0; // defensive parity with the f32 sparse path
21892        }
21893        let mut s = if need_scratch {
21894            vec![0.0f32; hidden]
21895        } else {
21896            Vec::new()
21897        };
21898        let gate = d.gate_proj.row_dot(idx, x, &mut s);
21899        let up = d.up_proj.row_dot(idx, x, &mut s);
21900        d.act.combine(gate, up)
21901    };
21902    match pool {
21903        Some(p) if n >= 256 => {
21904            let ptr = SendMut(act.as_mut_ptr());
21905            p.run(&|widx, nw| {
21906                let chunk = n.div_ceil(nw);
21907                let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
21908                for ai in s..e {
21909                    unsafe { *ptr.at(ai) = compute(ai) };
21910                }
21911            });
21912        }
21913        _ => {
21914            for (ai, a) in act.iter_mut().enumerate() {
21915                *a = compute(ai);
21916            }
21917        }
21918    }
21919    // Scatter through active down columns (reads only those columns).
21920    let mut out = vec![0.0f32; hidden];
21921    for (ai, &idx) in active.iter().enumerate() {
21922        let w = act[ai];
21923        if w.abs() >= 1e-12 && (idx as usize) < inter {
21924            d.down_proj.add_col_scaled(idx as usize, w, &mut out);
21925        }
21926    }
21927    out
21928}
21929
21930/// Test-only re-export of the private sparse-quant FFN (mask × mmap gate).
21931#[doc(hidden)]
21932pub fn sparse_ffn_quant_for_test(
21933    d: &DenseFfn,
21934    x: &[f32],
21935    active: &[u16],
21936    hidden: usize,
21937) -> Vec<f32> {
21938    sparse_ffn_quant(d, x, active, hidden, None)
21939}
21940
21941/// Dequantize a DenseFfn's three matrices to f32 (transient; only the
21942/// q4/vbit-masked fallback uses it — the memory-lean path is
21943/// sparse_ffn_quant). Reuses row_f32 row-by-row.
21944fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
21945    let deq = |t: &QTensor| -> Vec<f32> {
21946        let (rows, cols) = (t.rows(), t.cols());
21947        let mut out = vec![0.0f32; rows * cols];
21948        for r in 0..rows {
21949            t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
21950        }
21951        out
21952    };
21953    (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
21954}
21955
21956/// Pointer wrapper for the worker-pool scatter (same pattern as qtensor).
21957struct SendMut(*mut f32);
21958unsafe impl Send for SendMut {}
21959unsafe impl Sync for SendMut {}
21960impl SendMut {
21961    #[inline]
21962    // Deliberate unsynchronized scatter: pool workers write disjoint indices
21963    // in parallel, so returning `&mut` from `&self` is intentional here.
21964    #[allow(clippy::mut_from_ref)]
21965    unsafe fn at(&self, i: usize) -> &mut f32 {
21966        unsafe { &mut *self.0.add(i) }
21967    }
21968}
21969
21970/// Router → (selected experts in torch.topk order, per-expert score
21971/// vector, normalizer). The final weight of expert `e` is `p[e] / wsum`.
21972///
21973/// Two regimes share this. Qwen: softmax over ALL experts, top-k of the
21974/// probabilities, optional renorm — `router_sigmoid=false`, no bias,
21975/// scale 1 → bit-identical to the historical path. LFM2-MoE /
21976/// DeepSeek-V3 `noaux_tc`: per-expert sigmoid scores, an optional
21977/// selection bias (top-k CHOICE only; weights stay unbiased), an
21978/// architecture-specified renorm floor and a routed scale.  The ordinary
21979/// loader contract is `1e-6`; Flint and MiniMax M3.1 preserve their
21980/// reference runtime's selected-score `sum + 1e-20`.
21981pub(crate) fn moe_route(
21982    logits: &[f32],
21983    m: &MoeFfn,
21984    allowed: Option<&[bool]>,
21985) -> (Vec<usize>, Vec<f32>, f32) {
21986    moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
21987}
21988
21989/// Router implementation with an explicit sigmoid renormalization floor.
21990///
21991/// Flint and MiniMax M3.1 reference runtimes compute
21992/// `sum(selected_scores) + 1e-20` in FP32. Using the generic 1e-6 floor is
21993/// not a harmless tolerance difference when all logits are very negative: it
21994/// collapses the routed branch toward zero instead of normalizing the selected
21995/// experts. Keeping the epsilon parameter here avoids changing the established
21996/// Qwen/LFM2 contract while allowing each architecture to preserve its own
21997/// numerical semantics.
21998pub(crate) fn moe_route_with_eps(
21999    logits: &[f32],
22000    m: &MoeFfn,
22001    allowed: Option<&[bool]>,
22002    sigmoid_denom_eps: f32,
22003) -> (Vec<usize>, Vec<f32>, f32) {
22004    let ne = logits.len();
22005    // Expert restriction: the static env mask (CMF_MOE_MASK) AND the
22006    // active task mask's expert fields (spec §5) both narrow the
22007    // candidate set; selection happens over the admitted experts only.
22008    // With norm_topk the kept weights renormalize below; without it
22009    // the excluded mass is honestly dropped.
22010    let admit = |e: usize| {
22011        m.mask.as_ref().is_none_or(|mk| mk[e])
22012            && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
22013    };
22014    // The resonance router (spec §9.5.1) selects by the RAW score: the
22015    // trainer (`resonance_winner`) and the resident graph
22016    // (`embryo_core_route_pick`) take the first maximum of the scores
22017    // and run the winner with weight 1.0. Selecting through the softmax
22018    // instead is not the same decision: `exp(l − max)` rounds two scores
22019    // closer than 2^-25 (possible below |score| 0.25) to the same 1.0, and
22020    // the lower index would take a token whose score is strictly smaller
22021    // — the trainer's trace and the graph would disagree with this path.
22022    // `−∞` (outside the shell) never wins; with no finite admitted expert
22023    // the generic path below degrades to uniform. The winner's
22024    // probability is 1.0 by construction (a one-hot `p`), so its
22025    // renormalized weight is `routed_scaling` on both norm_topk settings.
22026    if m.resonance.is_some() && m.top_k == 1 {
22027        let mut best: Option<usize> = None;
22028        for e in (0..ne).filter(|&e| admit(e)) {
22029            let l = logits[e];
22030            if l == f32::NEG_INFINITY || l.is_nan() {
22031                continue;
22032            }
22033            if best.is_none_or(|b| l > logits[b]) {
22034                best = Some(e);
22035            }
22036        }
22037        if let Some(b) = best {
22038            let mut p = vec![0.0f32; ne];
22039            p[b] = 1.0;
22040            return (vec![b], p, 1.0 / m.routed_scaling);
22041        }
22042    }
22043    // A `−∞` logit (a grown expert outside its shell, `Resonance::scores`)
22044    // takes probability 0 on both paths: sigmoid(−∞) = 0, exp(−∞ − max) = 0
22045    // — top-1 is the best FINITE expert, its renormalized weight exactly
22046    // 1.0. Every expert at −∞ cannot happen (trunk experts have no shell);
22047    // should it, the softmax would be NaN, so it degrades to uniform.
22048    let p: Vec<f32> = if m.router_sigmoid {
22049        logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
22050    } else {
22051        let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
22052        if mx == f32::NEG_INFINITY {
22053            vec![1.0 / ne.max(1) as f32; ne]
22054        } else {
22055            let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
22056            let s: f32 = e.iter().sum();
22057            for v in &mut e {
22058                *v /= s;
22059            }
22060            e
22061        }
22062    };
22063    let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
22064    // Descending by selection score, lower index wins ties (torch.topk).
22065    match &m.expert_bias {
22066        Some(b) => idx.sort_unstable_by(|&x, &y| {
22067            (p[y] + b[y])
22068                .partial_cmp(&(p[x] + b[x]))
22069                .unwrap()
22070                .then(x.cmp(&y))
22071        }),
22072        None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
22073    }
22074    idx.truncate(m.top_k);
22075    // Adaptive τ-routing: trim the tail experts once the kept mass is
22076    // enough. wsum below renormalizes over the KEPT set, so the output
22077    // stays a proper weighted average.
22078    if let Some(tau) = m.route_tau {
22079        let total: f32 = idx.iter().map(|&e| p[e]).sum();
22080        if total > 0.0 {
22081            let mut acc = 0.0f32;
22082            let mut keep = idx.len();
22083            for (i, &e) in idx.iter().enumerate() {
22084                acc += p[e];
22085                if acc >= tau * total {
22086                    keep = i + 1;
22087                    break;
22088                }
22089            }
22090            idx.truncate(keep);
22091        }
22092    }
22093    let wsum: f32 = if m.norm_topk_prob {
22094        let s: f32 = idx.iter().map(|&e| p[e]).sum();
22095        // Sigmoid routers use their architecture's reference floor; the
22096        // softmax path's probs already sum near 1, so it stays exactly as
22097        // before.
22098        (if m.router_sigmoid {
22099            s + sigmoid_denom_eps
22100        } else {
22101            s
22102        }) / m.routed_scaling
22103    } else {
22104        1.0 / m.routed_scaling
22105    };
22106    (idx, p, wsum)
22107}
22108
22109/// See the call site: one `layer:e1,e2,…` line per routed token.
22110fn moe_trace(idx: &[usize]) {
22111    moe_trace_at(crate::gpu::cur_layer() as i32, idx)
22112}
22113
22114/// The same, for callers that know their layer (DSV4 owns its layers and
22115/// never sets the pipeline's current-layer marker).
22116pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
22117    use std::io::Write;
22118    static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
22119        std::sync::OnceLock::new();
22120    let Some(f) = F.get_or_init(|| {
22121        let p = std::env::var("CMF_MOE_TRACE").ok()?;
22122        Some(std::sync::Mutex::new(
22123            std::fs::OpenOptions::new()
22124                .create(true)
22125                .append(true)
22126                .open(p)
22127                .ok()?,
22128        ))
22129    }) else {
22130        return;
22131    };
22132    let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
22133    let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
22134}
22135
22136/// MoE FFN: router → top-k experts (see `moe_route`). Only selected
22137/// experts' pages are touched in mmap.
22138pub(crate) fn moe_ffn(
22139    m: &MoeFfn,
22140    x: &[f32],
22141    pool: Option<&Pool>,
22142    allowed: Option<&[bool]>,
22143) -> Vec<f32> {
22144    let r = moe_ffn_route(m, x, pool, allowed);
22145    moe_ffn_experts(m, x, &r, pool)
22146}
22147
22148/// One token's host route through a MoE layer: the chosen experts in
22149/// selection order, the per-expert scores and the normalizer (see
22150/// `moe_route`), plus the raw router logits.
22151pub(crate) struct MoeRoute {
22152    pub idx: Vec<usize>,
22153    pub p: Vec<f32>,
22154    pub wsum: f32,
22155    pub logits: Vec<f32>,
22156}
22157
22158/// The routing half of `moe_ffn`, shared by every executor of the chosen
22159/// experts (the host/per-op path below and the MiMo dynamic device cache,
22160/// `crate::mimo_moe`): activation accounting, router logits, `moe_route`,
22161/// the selection statistics and the `CMF_MOE_TRACE` line — so switching
22162/// executors can never change which experts a token gets.
22163pub(crate) fn moe_ffn_route(
22164    m: &MoeFfn,
22165    x: &[f32],
22166    pool: Option<&Pool>,
22167    allowed: Option<&[bool]>,
22168) -> MoeRoute {
22169    accumulate_act(m, x, 1);
22170    let ne = m.experts.len();
22171    let mut logits = vec![0.0f32; ne];
22172    match &m.resonance {
22173        Some(r) => r.scores(x, &mut logits),
22174        None => m.router.matvec(x, &mut logits, pool),
22175    }
22176    let (idx, p, wsum) = moe_route(&logits, m, allowed);
22177    {
22178        let mut st = m.stats.borrow_mut();
22179        if st.len() < ne {
22180            st.resize(ne, 0);
22181        }
22182        for &e in &idx {
22183            st[e] += 1;
22184        }
22185    }
22186    // `CMF_MOE_TRACE=<file>`: append one line per (layer, token) with the
22187    // selected expert ids. The cumulative `stats` above answer "which
22188    // experts are popular"; a residency design needs the question they
22189    // cannot answer — whether CONSECUTIVE tokens reuse experts (the
22190    // temporal locality an LRU cache lives on, FreeToken §4).
22191    moe_trace(&idx);
22192    MoeRoute {
22193        idx,
22194        p,
22195        wsum,
22196        logits,
22197    }
22198}
22199
22200/// Flint's released mixed MoE profile has a native-Metal selected-expert
22201/// pager with a different numerical contract from the generic CPU probe:
22202/// the pager keeps activations in f32, whereas the fast host arm normally
22203/// uses its A8W8 activation path.  Timing those two approximations against
22204/// one another can make the probe reject the exact pager even when it is the
22205/// faster exact-f32 implementation.  Keep this gate deliberately narrower
22206/// than the pager itself; the pager revalidates every selected tuple before
22207/// it binds a no-copy view and an honest refusal still takes the CPU path.
22208#[cfg(target_os = "macos")]
22209fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
22210    const HIDDEN: usize = 2_048;
22211    const ROUTED_INTER: usize = 512;
22212    const SHARED_INTER: usize = 1_024;
22213    const EXPERTS: usize = 256;
22214    const TOP_K: usize = 6;
22215
22216    let Some(routed) = m.experts.first() else {
22217        return false;
22218    };
22219    let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
22220        return false;
22221    };
22222    let Some((shared, shared_gate)) = &m.shared else {
22223        return false;
22224    };
22225    if shared_gate.is_some()
22226        || model.header.arch.arch_name != "flint"
22227        || model.header.arch.prism_hadamard.is_some()
22228        || model.header.arch.hidden_size != HIDDEN
22229        || m.experts.len() != EXPERTS
22230        || m.top_k != TOP_K
22231        || !m.router_sigmoid
22232        || !m.norm_topk_prob
22233        || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
22234        || m.routed_scaling.to_bits() != 1.0f32.to_bits()
22235        || m.route_tau.is_some()
22236        || m.mask.is_some()
22237        || m.per_expert_scale.is_some()
22238        || m.router_input_norm
22239        || m.resonance.is_some()
22240        || !m.grown.is_empty()
22241    {
22242        return false;
22243    }
22244
22245    let routed_q2 = routed.act == Act::Silu
22246        && routed.segs.is_empty()
22247        && routed.gate_proj.rows() == ROUTED_INTER
22248        && routed.gate_proj.cols() == HIDDEN
22249        && routed.up_proj.rows() == ROUTED_INTER
22250        && routed.up_proj.cols() == HIDDEN
22251        && routed.down_proj.rows() == HIDDEN
22252        && routed.down_proj.cols() == ROUTED_INTER
22253        && routed.up_proj.mapped_q2tp().is_some()
22254        && routed.down_proj.mapped_q4tp().is_some();
22255    let shared_q4 = shared.act == Act::Silu
22256        && shared.segs.is_empty()
22257        && shared.gate_proj.rows() == SHARED_INTER
22258        && shared.gate_proj.cols() == HIDDEN
22259        && shared.up_proj.rows() == SHARED_INTER
22260        && shared.up_proj.cols() == HIDDEN
22261        && shared.down_proj.rows() == HIDDEN
22262        && shared.down_proj.cols() == SHARED_INTER
22263        && shared.gate_proj.mapped_q4tp().is_some()
22264        && shared.up_proj.mapped_q4tp().is_some()
22265        && shared.down_proj.mapped_q4tp().is_some();
22266    routed_q2 && shared_q4
22267}
22268
22269/// The expert half of `moe_ffn`: run a route's experts on the per-op GPU
22270/// block or the host.
22271pub(crate) fn moe_ffn_experts(
22272    m: &MoeFfn,
22273    x: &[f32],
22274    r: &MoeRoute,
22275    pool: Option<&Pool>,
22276) -> Vec<f32> {
22277    let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
22278    // D5: the whole layer MoE block in one GPU command buffer (experts — the
22279    // same mmap via a no-copy buffer; intermediate activations on the GPU).
22280    // MoE owns a separate probe class from dense FFN: its selected-span pager
22281    // has materially different residency and latency costs.
22282    if crate::gpu::enabled_here() {
22283        #[cfg(target_os = "macos")]
22284        if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
22285            // Do not compare the pager's exact-f32 result with the CPU's
22286            // optional A8W8 activation approximation.  The strict pager is
22287            // the validated Flint path; if a directory/view/device check
22288            // declines, preserve the existing CPU fallback rather than
22289            // leaving the probe in a half-sampled state.
22290            return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
22291                crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
22292            });
22293        }
22294        match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
22295            crate::gpu::ProbeArm::Gpu => {
22296                let t0 = std::time::Instant::now();
22297                if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
22298                    crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
22299                    return out;
22300                }
22301                crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
22302            }
22303            crate::gpu::ProbeArm::CpuTimed => {
22304                let t0 = std::time::Instant::now();
22305                let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
22306                crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
22307                return out;
22308            }
22309            crate::gpu::ProbeArm::Cpu => {
22310                return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
22311            }
22312        }
22313    }
22314    moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
22315}
22316
22317/// One MiniCPM CPU-tail MoE through its opt-in dynamic bank, or through the
22318/// same-route host fallback.  The route is formed exactly once so statistics,
22319/// tracing, and model semantics remain owned by `moe_ffn_route`.
22320fn moe_ffn_minicpm_tail(
22321    slot: &mut crate::minicpm_tail::MiniCpmTail,
22322    li: usize,
22323    total_layers: usize,
22324    m: &MoeFfn,
22325    x: &[f32],
22326    pool: Option<&Pool>,
22327) -> Vec<f32> {
22328    let route = moe_ffn_route(m, x, pool, None);
22329    match slot.forward(li, m, x, &route, pool, total_layers) {
22330        Some(out) => out,
22331        // A cache refusal must be observationally identical to the generic
22332        // CPU-tail path. The surrounding host-tail scope already disables the
22333        // per-op arena, so do not introduce a float-activation policy change
22334        // merely because this explicit opt-in cache declined a model/layout.
22335        None => moe_ffn_experts(m, x, &route, pool),
22336    }
22337}
22338
22339/// One MoE token through the MiMo expert bank (`crate::mimo_moe`), or —
22340/// when the bank does not serve it — through the host path with the SAME
22341/// route, so the routing statistics and `CMF_MOE_TRACE` see it once.
22342fn moe_ffn_banked(
22343    slot: &mut crate::mimo_moe::Slot,
22344    li: usize,
22345    m: &MoeFfn,
22346    x: &[f32],
22347    pool: Option<&Pool>,
22348) -> Vec<f32> {
22349    let t0 = std::time::Instant::now();
22350    let r = moe_ffn_route(m, x, pool, None);
22351    slot.note_route(t0.elapsed().as_nanos() as u64);
22352    match slot.forward(li, m, x, &r, pool) {
22353        Some(out) => out,
22354        None => crate::qtensor::float_activations_scope(|| {
22355            crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
22356        }),
22357    }
22358}
22359
22360/// Verify rows share a bank frame; routing and fallback are decode's.
22361fn moe_ffn_banked_rows(
22362    slot: &mut crate::mimo_moe::Slot,
22363    li: usize,
22364    m: &MoeFfn,
22365    xs: &[f32],
22366    b: usize,
22367    hidden: usize,
22368    pool: Option<&Pool>,
22369) -> Vec<f32> {
22370    let t0 = std::time::Instant::now();
22371    let routes: Vec<_> = xs
22372        .chunks_exact(hidden)
22373        .map(|x| moe_ffn_route(m, x, pool, None))
22374        .collect();
22375    slot.note_route(t0.elapsed().as_nanos() as u64);
22376    if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
22377        return out;
22378    }
22379    let mut out = Vec::with_capacity(b * hidden);
22380    for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
22381        let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
22382            // A failed bank must not stream missing experts into the arena.
22383            crate::qtensor::float_activations_scope(|| {
22384                crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
22385            })
22386        });
22387        out.extend(row);
22388    }
22389    out
22390}
22391
22392/// One-shot report of whether the whole-token wgpu graph actually formed.
22393/// A refusal silently reverts to the per-op path, which is how a model can
22394/// look "GPU-accelerated" while every layer walks the host.  A device prefix
22395/// is tracked separately because it still pays a host boundary for the tail.
22396fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
22397    use std::sync::atomic::{AtomicBool, Ordering};
22398    if built {
22399        GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
22400        if total_layers > 0 && layers_run < total_layers {
22401            GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
22402        } else {
22403            GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
22404        }
22405    } else {
22406        GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
22407    }
22408    static SAID: AtomicBool = AtomicBool::new(false);
22409    if !SAID.swap(true, Ordering::Relaxed) {
22410        if built {
22411            tracing::info!("wgpu whole-token graph: ACTIVE");
22412        } else {
22413            tracing::warn!("wgpu whole-token graph refused — per-op path");
22414        }
22415    }
22416}
22417
22418/// Whole-token graph outcomes, process-wide: a benchmark that claims a
22419/// GPU number while MISS climbs is measuring the CPU — the honest-bench
22420/// contract makes that an error, not a footnote.
22421pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22422pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22423/// Graph calls that returned a hidden after running only a leading device
22424/// prefix.  These are valid hybrid executions but must not be reported as a
22425/// full GPU graph in benchmark evidence.
22426pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22427/// Graph calls that covered the complete requested layer span.
22428pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22429
22430/// Native Metal TokenGraph completion counters. These are incremented only
22431/// after checked command-buffer completion and successful readback, so a
22432/// fused-head NLL report can prove the route rather than infer it from env.
22433pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22434pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22435pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
22436    std::sync::atomic::AtomicU64::new(0);
22437pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22438pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22439/// Ordinary native-Metal rows-prefill admissions and completed rows.  These
22440/// counters are separate from TokenGraph token/head counts so a batch NLL
22441/// receipt cannot accidentally claim serial execution as batched.
22442pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
22443    std::sync::atomic::AtomicU64::new(0);
22444pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22445pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
22446    std::sync::atomic::AtomicU64::new(0);
22447pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
22448    std::sync::atomic::AtomicU64::new(0);
22449
22450/// `CMF_MOE_BATCH=0` restores the per-expert serial loop — the A/B lever
22451/// for the batched kernel, and how its bit-identity is checked.
22452fn moe_batch_enabled() -> bool {
22453    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
22454    *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
22455}
22456
22457/// Two-dispatch CPU MoE: every routed expert (and the shared one) fused
22458/// into one gate/up/SiLU dispatch and one down dispatch, instead of two
22459/// pool barriers per expert. Bit-identical to the serial loop below —
22460/// see `moe_gate_up_many` / `moe_down_many`. `None` = the batched kernel
22461/// does not cover this layer, walk the serial path.
22462fn moe_ffn_cpu_batched(
22463    m: &MoeFfn,
22464    x: &[f32],
22465    idx: &[usize],
22466    p: &[f32],
22467    wsum: f32,
22468    pool: Option<&Pool>,
22469) -> Option<Vec<f32>> {
22470    if idx.is_empty() || !moe_batch_enabled() {
22471        return None;
22472    }
22473    // The bake probe reads per-neuron activation mass out of the
22474    // single-expert path; batching would skip it. Rare and offline —
22475    // hand those runs to the serial loop.
22476    if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
22477        return None;
22478    }
22479    let n = idx.len() + usize::from(m.shared.is_some());
22480    let mut pairs = Vec::with_capacity(n);
22481    let mut downs = Vec::with_capacity(n);
22482    let mut ws = Vec::with_capacity(n);
22483    for &e in idx {
22484        let d = &m.experts[e];
22485        if d.act != Act::Silu {
22486            return None;
22487        }
22488        pairs.push((&d.gate_proj, &d.up_proj));
22489        downs.push(&d.down_proj);
22490        ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
22491    }
22492    // The shared expert goes last, matching the serial loop's order —
22493    // the f32 accumulation order is part of the bit-identity claim.
22494    if let Some((se, gate)) = &m.shared {
22495        if se.act != Act::Silu {
22496            return None;
22497        }
22498        let g = gate.as_ref().map_or(1.0, |gate| {
22499            let mut gl = [0.0f32; 1];
22500            gate.matvec(x, &mut gl, pool);
22501            1.0 / (1.0 + (-gl[0]).exp())
22502        });
22503        pairs.push((&se.gate_proj, &se.up_proj));
22504        downs.push(&se.down_proj);
22505        ws.push(g);
22506    }
22507    let inter = pairs[0].0.rows();
22508    let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
22509    if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
22510        return None;
22511    }
22512    let mut out = attention::take_buf(x.len());
22513    if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
22514        attention::recycle_buf(&mut out);
22515        return None;
22516    }
22517    Some(out)
22518}
22519
22520/// Exact CPU completion for the routed experts a dynamic device cache did
22521/// not contain. The weights are already the router's final normalized mix.
22522/// Keeping this independent of `MoeFfn` makes the job `Sync`: its routing
22523/// statistics live in a `RefCell`, while the immutable expert tensors can be
22524/// evaluated safely in parallel with the GPU's resident subset.
22525/// Pooled-tail stage timers, reported per token by the bench JSON
22526/// (`m3_span_ms_per_tok`, `m3_cold_ms_per_tok`, `m3_arm_ms_per_tok`).
22527pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22528pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22529pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22530pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22531pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22532
22533pub(crate) fn moe_cold_experts_cpu(
22534    experts: &[(&DenseFfn, f32)],
22535    x: &[f32],
22536    pool: Option<&Pool>,
22537) -> Vec<f32> {
22538    let mut out = attention::take_buf(x.len());
22539    if experts.is_empty() {
22540        return out;
22541    }
22542    let pairs: Vec<_> = experts
22543        .iter()
22544        .map(|(e, _)| (&e.gate_proj, &e.up_proj))
22545        .collect();
22546    let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
22547    let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
22548    let inter = experts[0].0.gate_proj.rows();
22549    let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
22550    // The batched gate/up kernels implement PLAIN SiLU only (their legacy
22551    // helper passes limit 0 and no activation code). MiniMax's SwiGLU-OAI
22552    // (alpha 1.702, shifted up) must take the per-expert path that reads
22553    // each tensor's own activation — otherwise every cold expert computes
22554    // silu and the residual walks away from the reference (measured: the
22555    // pooled tail shipped cos 0.047 hidden while the resident path was
22556    // bit-close).
22557    let silu_only = experts
22558        .iter()
22559        .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
22560    if silu_only
22561        && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
22562        && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
22563    {
22564        return out;
22565    }
22566    out.fill(0.0);
22567    for &(expert, weight) in experts {
22568        let mut one = dense_ffn(expert, x, pool);
22569        for (o, v) in out.iter_mut().zip(&one) {
22570            *o += weight * v;
22571        }
22572        attention::recycle_buf(&mut one);
22573    }
22574    out
22575}
22576
22577/// Cold part of a short bank batch. Share each expert's weight stream
22578/// across its tokens, but reduce contributions in each token's route order.
22579/// On an unsupported CPU/layout, retain the single-token cold kernels.
22580pub(crate) fn moe_cold_experts_rows_cpu(
22581    jobs: &[Vec<(&DenseFfn, f32)>],
22582    xs: &[f32],
22583    hidden: usize,
22584    pool: Option<&Pool>,
22585) -> Vec<f32> {
22586    let mut out = vec![0.0; xs.len()];
22587    let mut experts: Vec<&DenseFfn> = Vec::new();
22588    let mut groups: Vec<Vec<usize>> = Vec::new();
22589    let mut terms = vec![Vec::new(); jobs.len()];
22590    for (r, row) in jobs.iter().enumerate() {
22591        for &(e, w) in row {
22592            let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
22593                Some(g) => g,
22594                None => {
22595                    experts.push(e);
22596                    groups.push(Vec::new());
22597                    groups.len() - 1
22598                }
22599            };
22600            terms[r].push((g, groups[g].len(), w));
22601            groups[g].push(r);
22602        }
22603    }
22604    if experts.is_empty() {
22605        return out;
22606    }
22607    let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
22608    let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
22609    let lens: Vec<_> = groups.iter().map(Vec::len).collect();
22610    let count: usize = lens.iter().sum();
22611    let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
22612    let mut ds = vec![vec![0.0; hidden]; count];
22613    if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
22614        && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
22615    {
22616        let mut offset = 0;
22617        let offsets: Vec<_> = lens
22618            .iter()
22619            .map(|&n| {
22620                let start = offset;
22621                offset += n;
22622                start
22623            })
22624            .collect();
22625        for (r, terms) in terms.iter().enumerate() {
22626            for &(g, slot, w) in terms {
22627                for (o, &v) in out[r * hidden..(r + 1) * hidden]
22628                    .iter_mut()
22629                    .zip(&ds[offsets[g] + slot])
22630                {
22631                    *o += w * v;
22632                }
22633            }
22634        }
22635    } else {
22636        for (r, jobs) in jobs.iter().enumerate() {
22637            if !jobs.is_empty() {
22638                let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
22639                out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
22640                attention::recycle_buf(&mut row);
22641            }
22642        }
22643    }
22644    out
22645}
22646
22647/// The pure-CPU MoE expert loop (also the fallback of every GPU refusal).
22648fn moe_ffn_cpu(
22649    m: &MoeFfn,
22650    x: &[f32],
22651    idx: &[usize],
22652    p: &[f32],
22653    wsum: f32,
22654    pool: Option<&Pool>,
22655) -> Vec<f32> {
22656    if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
22657        return out;
22658    }
22659    let mut out = attention::take_buf(x.len());
22660    for &e in idx {
22661        let mut eo = dense_ffn(&m.experts[e], x, pool);
22662        let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
22663        for i in 0..out.len() {
22664            out[i] += w * eo[i];
22665        }
22666        attention::recycle_buf(&mut eo);
22667    }
22668    if let Some((se, gate)) = &m.shared {
22669        let mut so = dense_ffn(se, x, pool);
22670        let g = gate.as_ref().map_or(1.0, |gate| {
22671            let mut gl = [0.0f32; 1];
22672            gate.matvec(x, &mut gl, pool);
22673            1.0 / (1.0 + (-gl[0]).exp())
22674        });
22675        for i in 0..out.len() {
22676            out[i] += g * so[i];
22677        }
22678        attention::recycle_buf(&mut so);
22679    }
22680    out
22681}
22682
22683/// DeepSeek-V2 MLA forward, expand-to-MHA form (see `AttnKind::Mla`):
22684/// per token the latent expands to every head's K/V and the ordinary
22685/// cache + grouped attend do the rest. K head layout is [rope | nope]
22686/// (rotary_dim = qk_rope rotates the shared rope key and each q head's
22687/// prefix); V rows are zero-padded to the K head_dim inside the cache
22688/// and the pad is sliced off before O. Attention importance is not
22689/// accumulated for MLA yet (no eviction interplay).
22690#[allow(clippy::too_many_arguments)]
22691pub(crate) fn mla_attention(
22692    w: &MlaWeights,
22693    normed: &[f32],
22694    cache: &mut crate::kv_cache::LayerKvCache,
22695    position: usize,
22696    inv_freq: &[f32],
22697    rope_scale: f32,
22698    eps: f64,
22699    pool: Option<&Pool>,
22700) -> Vec<f32> {
22701    let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
22702    let hd = dr + dn;
22703    let mut q = vec![0.0f32; nh * hd];
22704    match (&w.q_a, &w.q_a_norm) {
22705        (Some(qa), Some(qn)) => {
22706            let mut t = vec![0.0f32; qa.rows()];
22707            qa.matvec(normed, &mut t, pool);
22708            let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
22709            w.q_proj.matvec(&tn, &mut q, pool);
22710        }
22711        _ => w.q_proj.matvec(normed, &mut q, pool),
22712    }
22713    let mut ca = vec![0.0f32; lora + dr];
22714    w.kv_a.matvec(normed, &mut ca, pool);
22715    let (c_lat, k_rope) = ca.split_at_mut(lora);
22716    let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
22717    let mut kvb = vec![0.0f32; nh * (dn + dv)];
22718    w.kv_b.matvec(&latn, &mut kvb, pool);
22719    if !w.nope {
22720        attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
22721    }
22722    for h in 0..nh {
22723        if !w.nope {
22724            attention::rope_rotate_scaled(
22725                &mut q[h * hd..h * hd + dr],
22726                position,
22727                inv_freq,
22728                rope_scale,
22729            );
22730        }
22731    }
22732    let mut k = vec![0.0f32; nh * hd];
22733    let mut v = vec![0.0f32; nh * hd];
22734    for h in 0..nh {
22735        k[h * hd..h * hd + dr].copy_from_slice(k_rope);
22736        k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
22737        v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
22738    }
22739    cache.append(&k, &v, &vec![true; nh]);
22740    let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
22741    attention::recycle_buf(&mut imp);
22742    let mut ov = vec![0.0f32; nh * dv];
22743    for h in 0..nh {
22744        ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
22745    }
22746    let mut out = vec![0.0f32; w.o_proj.rows()];
22747    w.o_proj.matvec(&ov, &mut out, pool);
22748    out
22749}
22750
22751/// Gemma-4 dual-branch FFN (spec: see `FfnKind::DenseMoe`). The dense
22752/// branch reads the pre-FFN-normed activation; the router and the
22753/// expert branch read the RAW residual — the router through a
22754/// scale-less rms norm (its constant gain is folded into the weights),
22755/// the experts through `pre_norm_2`. CPU path; GPU graphs refuse the
22756/// layer kind honestly.
22757fn dense_moe_ffn(
22758    dm: &DenseMoeFfn,
22759    x_normed: &[f32],
22760    h_raw: &[f32],
22761    eps: f64,
22762    norm_style: NormStyle,
22763    pool: Option<&Pool>,
22764) -> Vec<f32> {
22765    let mut d = dense_ffn(&dm.dense, x_normed, pool);
22766    d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
22767    let m = &dm.moe;
22768    let ne = m.experts.len();
22769    let mut logits = vec![0.0f32; ne];
22770    if m.router_input_norm {
22771        let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
22772        let inv = 1.0 / (ss + eps as f32).sqrt();
22773        let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
22774        m.router.matvec(&xr, &mut logits, pool);
22775    } else {
22776        m.router.matvec(h_raw, &mut logits, pool);
22777    }
22778    let (idx, p, wsum) = moe_route(&logits, m, None);
22779    {
22780        let mut st = m.stats.borrow_mut();
22781        if st.len() < ne {
22782            st.resize(ne, 0);
22783        }
22784        for &e in &idx {
22785            st[e] += 1;
22786        }
22787    }
22788    let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
22789    let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
22790    let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
22791    for (di, mi) in d.iter_mut().zip(&mo) {
22792        *di += mi;
22793    }
22794    d
22795}
22796
22797/// Building the MoE-layer GPU jobs: all selected experts (+shared) must
22798/// be q8_2f-Mapped from the primary mapping; otherwise None → CPU path.
22799/// One-shot report of why the MoE GPU block refused. A silent `?` here
22800/// sends every expert to the CPU with nothing in the logs to say so —
22801/// which is exactly how a q4tp MoE model looked "GPU-accelerated" while
22802/// running entirely on the host.
22803fn moe_gpu_refused(why: &'static str) {
22804    use std::sync::atomic::{AtomicBool, Ordering};
22805    static SAID: AtomicBool = AtomicBool::new(false);
22806    if !SAID.swap(true, Ordering::Relaxed) {
22807        tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
22808    }
22809}
22810
22811fn moe_ffn_gpu(
22812    m: &MoeFfn,
22813    x: &[f32],
22814    idx: &[usize],
22815    p: &[f32],
22816    wsum: f32,
22817    pool: Option<&Pool>,
22818) -> Option<Vec<f32>> {
22819    use crate::gpu::MoeJob;
22820
22821    let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
22822    let mut model_ref = None;
22823    for &e in idx {
22824        if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
22825            moe_gpu_refused("push_job(expert)");
22826            return None;
22827        }
22828    }
22829    if let Some((se, gate)) = &m.shared {
22830        let g = gate.as_ref().map_or(1.0, |gate| {
22831            let mut gl = [0.0f32; 1];
22832            gate.matvec(x, &mut gl, pool);
22833            1.0 / (1.0 + (-gl[0]).exp())
22834        });
22835        if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
22836            moe_gpu_refused("push_job(shared)");
22837            return None;
22838        }
22839    }
22840    let Some(model) = model_ref else {
22841        moe_gpu_refused("no model_ref");
22842        return None;
22843    };
22844    let hidden = jobs[0].down.1;
22845    let mut out = vec![0.0f32; hidden];
22846    if crate::gpu::moe_block(&model, &jobs, &mut out) {
22847        Some(out)
22848    } else {
22849        moe_gpu_refused("gpu::moe_block");
22850        None
22851    }
22852}
22853
22854/// Single-position FFN dispatch.
22855fn ffn_forward(
22856    ffn: &FfnKind,
22857    x: &[f32],
22858    pool: Option<&Pool>,
22859    experts_allowed: Option<&[bool]>,
22860) -> Vec<f32> {
22861    match ffn {
22862        FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
22863        FfnKind::Dense(d) => dense_ffn(d, x, pool),
22864        FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
22865        // Dual-branch layers need the raw residual — their callers
22866        // dispatch dense_moe_ffn directly; the auxiliary paths that land
22867        // here (MTP draft, o1 replay) do not co-occur with gemma-4 MoE.
22868        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22869    }
22870}
22871
22872/// Fused two-position FFN: gate/up/down streamed once (dense). MoE
22873/// falls back to two singles — expert sets differ per position, there
22874/// is nothing to fuse.
22875fn ffn_forward_pair(
22876    ffn: &FfnKind,
22877    x1: &[f32],
22878    x2: &[f32],
22879    pool: Option<&Pool>,
22880    experts_allowed: Option<&[bool]>,
22881) -> (Vec<f32>, Vec<f32>) {
22882    let d = match ffn {
22883        // A tube layer has nothing to fuse across the pair — the tubes
22884        // are separate matrices; two singles are the honest path.
22885        FfnKind::Dense(d) if !d.segs.is_empty() => {
22886            return (
22887                tube_ffn(d, x1, 1, pool, None),
22888                tube_ffn(d, x2, 1, pool, None),
22889            );
22890        }
22891        FfnKind::Dense(d) => d,
22892        FfnKind::Moe(m) => {
22893            return (
22894                moe_ffn(m, x1, pool, experts_allowed),
22895                moe_ffn(m, x2, pool, experts_allowed),
22896            );
22897        }
22898        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22899    };
22900    let inter = d.gate_proj.rows();
22901    FFN_SCRATCH.with(|s| {
22902        let mut s = s.borrow_mut();
22903        let [g1, g2, u1, u2] = &mut *s;
22904        g1.resize(inter, 0.0);
22905        g2.resize(inter, 0.0);
22906        u1.resize(inter, 0.0);
22907        u2.resize(inter, 0.0);
22908        // Multi-matrix pair job: gate+up under one pool dispatch
22909        // (o1s = lane-1 outputs across tensors, o2s = lane-2).
22910        QTensor::matvec2_many(
22911            [&d.gate_proj, &d.up_proj],
22912            x1,
22913            x2,
22914            [g1.as_mut_slice(), u1.as_mut_slice()],
22915            [g2.as_mut_slice(), u2.as_mut_slice()],
22916            pool,
22917        );
22918        for i in 0..inter {
22919            g1[i] = d.act.combine(g1[i], u1[i]);
22920            g2[i] = d.act.combine(g2[i], u2[i]);
22921        }
22922        let mut o1 = attention::take_buf(d.down_proj.rows());
22923        let mut o2 = attention::take_buf(d.down_proj.rows());
22924        d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
22925        (o1, o2)
22926    })
22927}
22928
22929#[cfg(test)]
22930mod tests {
22931    use std::ffi::OsString;
22932    use std::sync::Mutex;
22933
22934    // Environment switches are process-global. Keep the speculative-path
22935    // tests serialized and restore every prior value even during unwinding.
22936    static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
22937
22938    struct ScopedTestEnv {
22939        previous: Vec<(&'static str, Option<OsString>)>,
22940    }
22941
22942    impl ScopedTestEnv {
22943        fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
22944            let previous = vars
22945                .iter()
22946                .map(|(name, _)| (*name, std::env::var_os(name)))
22947                .collect();
22948            for &(name, value) in vars {
22949                unsafe {
22950                    match value {
22951                        Some(value) => std::env::set_var(name, value),
22952                        None => std::env::remove_var(name),
22953                    }
22954                }
22955            }
22956            Self { previous }
22957        }
22958    }
22959
22960    impl Drop for ScopedTestEnv {
22961        fn drop(&mut self) {
22962            for (name, value) in self.previous.drain(..).rev() {
22963                unsafe {
22964                    match value {
22965                        Some(value) => std::env::set_var(name, value),
22966                        None => std::env::remove_var(name),
22967                    }
22968                }
22969            }
22970        }
22971    }
22972
22973    #[test]
22974    fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
22975        use super::pad_mtp_draft_logits;
22976
22977        let mut logits = vec![-3.0, -1.0, -2.0];
22978        pad_mtp_draft_logits(&mut logits, 6);
22979        assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
22980        assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
22981        assert_eq!(
22982            logits
22983                .iter()
22984                .enumerate()
22985                .max_by(|(_, a), (_, b)| a.total_cmp(b))
22986                .map(|(i, _)| i),
22987            Some(1),
22988            "an unevaluated row cannot win an all-negative shortlist"
22989        );
22990    }
22991
22992    #[test]
22993    fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
22994        use super::{Pipeline, SpecMon, SpecTrial};
22995
22996        let mut mon = SpecMon::default();
22997        let start = std::time::Instant::now();
22998        mon.round(1.0, 1);
22999        let first = Pipeline::spec_trial_round(
23000            SpecTrial::Spec {
23001                t0: start,
23002                gen0: 0,
23003                rounds: 0,
23004            },
23005            &mut mon,
23006            1,
23007        );
23008        assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
23009
23010        mon.round(1.0, 1);
23011        let second = Pipeline::spec_trial_round(first, &mut mon, 2);
23012        assert!(
23013            matches!(second, SpecTrial::Plain { gen0: 2, .. }),
23014            "two verifier-tip-only rounds must stop the losing MTP trial"
23015        );
23016
23017        // One successful draft resets the streak, so a viable head retains
23018        // the normal five-round measurement budget.
23019        let mut viable = SpecMon::default();
23020        viable.round(1.0, 1);
23021        viable.round(1.0, 2);
23022        let continued = Pipeline::spec_trial_round(
23023            SpecTrial::Spec {
23024                t0: start,
23025                gen0: 0,
23026                rounds: 1,
23027            },
23028            &mut viable,
23029            3,
23030        );
23031        assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
23032    }
23033
23034    #[test]
23035    fn fresh_mtp_request_does_not_inherit_full_head_streak() {
23036        let mut p = spark_test_pipeline(None);
23037        let head_rows = 248_070;
23038        p.draft_full_streak = 7;
23039        assert_eq!(p.draft_head_rows(head_rows), head_rows);
23040
23041        p.reset_fresh_mtp_draft_state();
23042        assert_eq!(
23043            p.draft_head_rows(head_rows),
23044            super::Pipeline::draft_vocab_rows(head_rows),
23045            "a fresh request returns to its configured shortlist"
23046        );
23047
23048        // A high-id first token of the NEW request still restores the
23049        // full-head protection immediately.
23050        p.note_draft_id(u32::MAX);
23051        assert_eq!(p.draft_head_rows(head_rows), head_rows);
23052    }
23053
23054    #[test]
23055    fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
23056        use super::graph_spec_wanted_by_policy;
23057
23058        // A capable, otherwise-default-eligible native Metal model stays on
23059        // the serial path until the user deliberately asks for the
23060        // experimental verifier.
23061        assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
23062        assert!(!graph_spec_wanted_by_policy(
23063            Some("anything-but-1"),
23064            true,
23065            false,
23066            false,
23067            true,
23068        ));
23069        assert!(graph_spec_wanted_by_policy(
23070            Some("1"),
23071            false,
23072            true,
23073            false,
23074            true,
23075        ));
23076        assert!(!graph_spec_wanted_by_policy(
23077            Some("0"),
23078            true,
23079            false,
23080            false,
23081            true,
23082        ));
23083
23084        // Do not regress the established discrete/wgpu policy while closing
23085        // the native-Metal quality gate.
23086        assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
23087        assert!(graph_spec_wanted_by_policy(
23088            Some("legacy-force"),
23089            false,
23090            true,
23091            true,
23092            false,
23093        ));
23094    }
23095
23096    #[test]
23097    fn encoder_hidden_states_are_normalized_and_reusable() {
23098        let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
23099        let ids = [1, 7, 11, 19];
23100
23101        let first = p.encode_hidden_states(&ids).expect("encoder forward");
23102        let second = p
23103            .encode_hidden_states(&ids)
23104            .expect("reusable encoder forward");
23105        assert_eq!(first.len(), ids.len() * 8);
23106        assert!(first.iter().all(|v| v.is_finite()));
23107        assert_eq!(
23108            first, second,
23109            "encoder forward must start from a fresh state"
23110        );
23111
23112        // An encoder call must not leave stale KV/GDN state behind for the
23113        // regular generative API sharing the same Pipeline implementation.
23114        assert!(p.forward_ids(&ids, None).is_ok());
23115    }
23116
23117    /// The 0.7.6 prefill-chunk rule: a plain dense stack wholly on a
23118    /// discrete card reads the prompt in wide chunks on x86; every other
23119    /// case keeps the width it had (the GDN-hybrid, MoE and DeepSeek paths
23120    /// were tuned on hardware not measured for this change).
23121    #[test]
23122    fn prefill_chunk_rule_widens_only_dense_on_discrete() {
23123        use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
23124        let dense_card = ChunkStackFacts {
23125            plain_dense: true,
23126            discrete: true,
23127            gpu_on: true,
23128            ..Default::default()
23129        };
23130        assert!(dense_card.dense_on_discrete());
23131        // The bug: a dense Llama on a Vulkan RTX 3090 got 48.
23132        assert_eq!(
23133            prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
23134            DISCRETE_DENSE_PREFILL_CHUNK
23135        );
23136        assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
23137        for (label, facts) in [
23138            (
23139                "GDN hybrid / MoE / DeepSeek stack",
23140                ChunkStackFacts {
23141                    plain_dense: false,
23142                    ..dense_card
23143                },
23144            ),
23145            (
23146                "integrated GPU",
23147                ChunkStackFacts {
23148                    discrete: false,
23149                    ..dense_card
23150                },
23151            ),
23152            (
23153                "CPU only",
23154                ChunkStackFacts {
23155                    gpu_on: false,
23156                    discrete: false,
23157                    ..dense_card
23158                },
23159            ),
23160            (
23161                "capacity split",
23162                ChunkStackFacts {
23163                    capacity_split: true,
23164                    ..dense_card
23165                },
23166            ),
23167            (
23168                "multi-GPU plan",
23169                ChunkStackFacts {
23170                    multi_gpu: true,
23171                    ..dense_card
23172                },
23173            ),
23174            (
23175                "O(1) layers",
23176                ChunkStackFacts {
23177                    o1: true,
23178                    ..dense_card
23179                },
23180            ),
23181        ] {
23182            assert!(!facts.dense_on_discrete(), "{label}");
23183            assert_eq!(
23184                prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
23185                48,
23186                "{label} keeps the historical x86 chunk"
23187            );
23188        }
23189        // Other hosts are untouched whatever the model.
23190        for dense in [false, true] {
23191            assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
23192            assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
23193        }
23194        // CMF_PREFILL_CHUNK still wins everywhere (and is clamped to ≥ 1).
23195        for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
23196            for dense in [false, true] {
23197                assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
23198                assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
23199            }
23200        }
23201    }
23202
23203    #[test]
23204    fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
23205        use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
23206        let full = |host_rows, device_rows| ReuseLayer {
23207            full: true,
23208            host_rows,
23209            device_rows,
23210            device_state: false,
23211        };
23212        // Turn 1: 300-token prompt prefilled on the host, 40 tokens decoded
23213        // by the wgpu graph into the device mirror only. Turn 2 reuses 339.
23214        assert_eq!(
23215            kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
23216            ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
23217        );
23218        // CPU / Metal: the host owner already holds every forwarded row.
23219        assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
23220        // A mirror past the prefix is fine for the host (it gets rewound).
23221        assert_eq!(
23222            kv_reuse_plan(339, &[full(339, Some(345))]),
23223            ReusePlan::Ready
23224        );
23225        // GPU prefix / CPU tail: only the device layers lag.
23226        assert_eq!(
23227            kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
23228            ReusePlan::Pull(vec![(0, 300, 339)])
23229        );
23230        // The device cannot supply the missing rows: never continue.
23231        assert_eq!(
23232            kv_reuse_plan(339, &[full(300, Some(320))]),
23233            ReusePlan::Fresh
23234        );
23235        assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
23236        assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
23237        // A recurrent state advanced on the device cannot be handed to a
23238        // host prefill (it is not rewindable and the host copy is stale).
23239        let conv = |device_state| ReuseLayer {
23240            full: false,
23241            host_rows: 0,
23242            device_rows: None,
23243            device_state,
23244        };
23245        assert_eq!(
23246            kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
23247            ReusePlan::Fresh
23248        );
23249        assert_eq!(
23250            kv_reuse_plan(339, &[conv(false), full(339, None)]),
23251            ReusePlan::Ready
23252        );
23253    }
23254
23255    #[test]
23256    fn nll_graph_policy_scopes_only_the_fused_head() {
23257        for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
23258            // A Vulkan/Wgpu hidden-only graph remains the quality route.
23259            ("vulkan graph", true, true, false, true, false),
23260            // Native Metal adds the strict fused graph-head contract.
23261            ("native Metal graph", true, true, true, true, true),
23262            // Masked NLL and the explicit non-graph fallback remain unchanged.
23263            ("masked", false, true, false, false, false),
23264            ("graph disabled", true, false, true, false, false),
23265        ] {
23266            let (graph_quality, graph_head_required) =
23267                super::nll_graph_policy(unmasked, prefer_graph, native_metal);
23268            assert_eq!(graph_quality, want_graph, "{label}: graph quality");
23269            assert_eq!(graph_head_required, want_head, "{label}: fused head");
23270        }
23271    }
23272
23273    #[test]
23274    fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
23275        assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
23276        assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
23277        assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
23278        assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
23279        assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
23280    }
23281
23282    #[test]
23283    fn cancel_flag_stops_generation() {
23284        let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
23285        // Set before the call: the prefill loops honour it, the run
23286        // returns immediately with the cancelled reason and no tokens.
23287        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23288        let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
23289        assert_eq!(r.finish_reason, "cancelled");
23290        assert!(
23291            r.token_ids.is_empty(),
23292            "no tokens after cancel: {:?}",
23293            r.token_ids
23294        );
23295        assert_eq!(p.kv_cache.seq_len(), 0);
23296        assert!(p.kv_history.is_empty());
23297        assert!(!p.graph_want_logits);
23298        assert!(p.graph_logits.is_none());
23299        // Flag auto-cleared: the next call generates normally.
23300        let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
23301        assert_ne!(r2.finish_reason, "cancelled");
23302    }
23303    use super::*;
23304
23305    #[test]
23306    fn minimax_m3_arch_selects_checked_oai_activation_only() {
23307        // Keep this fixture minimal: `Act::from_arch_full` needs only the
23308        // architecture id and activation name, while serde supplies stable
23309        // defaults for unrelated CMF fields.
23310        let arch = |arch_name: &str, hidden_act: &str| {
23311            serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
23312                "arch_name": arch_name,
23313                "hidden_size": 8,
23314                "intermediate_size": 16,
23315                "num_layers": 1,
23316                "num_attention_heads": 1,
23317                "num_kv_heads": 1,
23318                "head_dim": 8,
23319                "vocab_size": 16,
23320                "layer_types": ["FullAttention"],
23321                "rms_norm_eps": 1e-6,
23322                "max_position_embeddings": 16,
23323                "hidden_act": hidden_act
23324            }))
23325            .expect("minimal architecture")
23326        };
23327        let minimax = arch("minimax_m3_vl", "swigluoai");
23328        assert_eq!(
23329            Act::from_arch_full(&minimax),
23330            Act::SwiGluOai {
23331                alpha: 1.702,
23332                limit: 7.0,
23333            }
23334        );
23335
23336        // The same spelling must not turn into MiniMax math on a generic
23337        // model.  Only the source-validated M3 architecture owns it.
23338        let unrelated = arch("qwen2", "swigluoai");
23339        assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
23340
23341        // Conversely, an M3-tagged header with a normal activation keeps the
23342        // normal path; this prevents the architecture id from overriding the
23343        // explicit serialized contract.
23344        let non_oai = arch("minimax_m3_vl", "gelu");
23345        assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
23346    }
23347
23348    #[test]
23349    fn minimax_swiglu_oai_act_matches_scalar_contract() {
23350        let act = Act::swiglu_oai(1.702, 7.0);
23351        let got = act.combine(10.0, -10.0);
23352        let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
23353        assert!((got - expected).abs() < 1e-6);
23354        assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
23355        assert_eq!(
23356            act.wgpu_graph_act(),
23357            Some(crate::gpu::GraphAct::SwiGluOaiM3),
23358            "the WGPU-only arm accepts only the frozen M3 constants"
23359        );
23360        assert_eq!(
23361            Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
23362            None,
23363            "nearby OAI constants must not borrow MiniMax's kernel"
23364        );
23365        assert_eq!(
23366            act.moe_job_graph_contract(),
23367            Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
23368            "the direct MoE path carries the frozen clamp with the marker"
23369        );
23370        assert_eq!(
23371            Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
23372            None,
23373            "a nearby clamp must not borrow MiniMax's device contract"
23374        );
23375
23376        // `apply` is the gate half only; `combine` owns the up clamp and
23377        // +1 offset. The two must not accidentally collapse to ordinary SiLU.
23378        let gate_only = act.apply(-10.0);
23379        let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
23380        assert!((gate_only - expected_gate).abs() < 1e-10);
23381    }
23382
23383    #[test]
23384    fn explicit_sliding_schedule_selects_local_rope_profile() {
23385        // Mellum alternates three local 1,024-token layers with one global
23386        // YaRN layer.  An explicit schedule deliberately overrides the
23387        // cadence sentinel in `swa`, so test both RoPE table and scale
23388        // dispatch rather than merely checking that the metadata loaded.
23389        let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
23390        p.swa = Some((1024, usize::MAX));
23391        p.sliding_layers = Some(vec![true, true, true, false]);
23392        p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
23393        p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
23394        p.rope_scale = 1.277_258_9;
23395        p.rope_scale_local = 1.0;
23396
23397        for li in 0..3 {
23398            assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
23399            assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
23400            assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
23401        }
23402        assert_eq!(p.layer_window(3), None);
23403        assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
23404        assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
23405    }
23406
23407    /// sparse_ffn_quant must equal a dense FFN where inactive neurons are
23408    /// zeroed (mask × mmap correctness). On F32 tensors this is EXACT —
23409    /// it validates the row_dot / add_col_scaled / scatter indexing, the
23410    /// bug-prone part. The q8 branches reuse the golden-tested linear
23411    /// The per-token sparse path reads a transposed `down`; it must
23412    /// agree with the arm that computes everything and zeroes the
23413    /// losers, or the speed measurement is measuring a different model.
23414    #[test]
23415    fn dynamic_ffn_equals_the_zeroing_arm() {
23416        let (hidden, inter) = (8usize, 32usize);
23417        let synth = |n: usize, salt: usize| -> Vec<f32> {
23418            (0..n)
23419                .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
23420                .collect()
23421        };
23422        let down = synth(hidden * inter, 3);
23423        let mut down_t = vec![0.0f32; inter * hidden];
23424        for r in 0..hidden {
23425            for c in 0..inter {
23426                down_t[c * hidden + r] = down[r * inter + c];
23427            }
23428        }
23429        let d = DenseFfn {
23430            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23431            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23432            down_proj: QTensor::from_f32(down.clone(), hidden, inter),
23433            act: Act::Silu,
23434            down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
23435            segs: Vec::new(),
23436        };
23437        let x = synth(hidden, 11);
23438        let k = 12usize;
23439        let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
23440        // Reference: full compute, keep the k loudest |silu(gate)|.
23441        let mut g = vec![0.0f32; inter];
23442        d.gate_proj.matvec(&x, &mut g, None);
23443        let mut u = vec![0.0f32; inter];
23444        d.up_proj.matvec(&x, &mut u, None);
23445        for v in g.iter_mut() {
23446            *v = inference::silu(*v);
23447        }
23448        keep_top_k(&mut g, k);
23449        for i in 0..inter {
23450            g[i] *= u[i];
23451        }
23452        let mut want = vec![0.0f32; hidden];
23453        d.down_proj.matvec(&g, &mut want, None);
23454        for (a, b) in want.iter().zip(&got) {
23455            assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
23456        }
23457    }
23458
23459    /// A tube layer is the same layer, re-cut. With every tube open the
23460    /// answer must equal the dense FFN over the concatenated neurons
23461    /// (the permutation is an identity on the layer's function); with a
23462    /// tube closed it must equal the dense FFN with those neurons
23463    /// zeroed — the mask semantics, now paid for in bytes not read.
23464    #[test]
23465    fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
23466        let (hidden, core, tube) = (8usize, 12usize, 8usize);
23467        let inter = core + tube;
23468        let synth = |n: usize, salt: usize| -> Vec<f32> {
23469            (0..n)
23470                .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
23471                .collect()
23472        };
23473        let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
23474        let d_all = synth(hidden * inter, 3);
23475        // The dense layer, and the same weights cut into core + tube.
23476        let dense = DenseFfn {
23477            gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
23478            up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
23479            down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
23480            act: Act::Silu,
23481            down_t: None,
23482            segs: Vec::new(),
23483        };
23484        let rows =
23485            |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
23486        let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
23487            let mut o = Vec::with_capacity(hidden * (b - a));
23488            for r in 0..hidden {
23489                o.extend_from_slice(&v[r * inter + a..r * inter + b]);
23490            }
23491            o
23492        };
23493        let tubed = DenseFfn {
23494            down_t: None,
23495            gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
23496            up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
23497            down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
23498            act: Act::Silu,
23499            segs: vec![FfnSeg {
23500                gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
23501                up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
23502                down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
23503                start: core,
23504                width: tube,
23505            }],
23506        };
23507        let x = synth(hidden, 7);
23508        let want = dense_ffn(&dense, &x, None);
23509        let got = tube_ffn(&tubed, &x, 1, None, None);
23510        for (a, b) in want.iter().zip(&got) {
23511            assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
23512        }
23513        // Closed tube: bits on for the core, off for the tube.
23514        let mut bits = vec![0u8; inter.div_ceil(8)];
23515        for n in 0..core {
23516            bits[n / 8] |= 1 << (n % 8);
23517        }
23518        let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23519        let masked = dense_ffn_masked(&dense, &x, None, &bits);
23520        for (a, b) in masked.iter().zip(&closed) {
23521            assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
23522        }
23523        // The batched arm must agree with the single-position one.
23524        let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23525        for (a, b) in closed.iter().zip(&batch) {
23526            assert_eq!(a, b, "batch arm disagrees with decode arm");
23527        }
23528    }
23529
23530    /// scale, structurally identical to the matvec kernels.
23531    #[test]
23532    fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
23533        let (hidden, inter) = (16usize, 40usize);
23534        let synth = |n: usize, salt: usize| -> Vec<f32> {
23535            (0..n)
23536                .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
23537                .collect()
23538        };
23539        let d = DenseFfn {
23540            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23541            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23542            down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
23543            act: Act::Silu,
23544            down_t: None,
23545            segs: Vec::new(),
23546        };
23547        let x = synth(hidden, 9);
23548        // Active = every 3rd neuron.
23549        let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
23550
23551        let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
23552
23553        // Reference: full dense FFN but g[i]=0 for inactive neurons.
23554        let mut g = vec![0.0f32; inter];
23555        d.gate_proj.matvec(&x, &mut g, None);
23556        let mut u = vec![0.0f32; inter];
23557        d.up_proj.matvec(&x, &mut u, None);
23558        let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
23559        for i in 0..inter {
23560            g[i] = if act_set.contains(&(i as u16)) {
23561                inference::silu(g[i]) * u[i]
23562            } else {
23563                0.0
23564            };
23565        }
23566        let mut reference = vec![0.0f32; hidden];
23567        d.down_proj.matvec(&g, &mut reference, None);
23568
23569        let max_d = sparse
23570            .iter()
23571            .zip(&reference)
23572            .map(|(a, b)| (a - b).abs())
23573            .fold(0.0f32, f32::max);
23574        assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
23575    }
23576
23577    /// Attach a synthetic MTP head. `inter` deliberately stays independent
23578    /// from the trunk width so calibration tests can catch a trunk-proxy bug.
23579    fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
23580        let (h, inter, heads, kv, hd) = (
23581            p.hidden_size,
23582            inter,
23583            p.num_heads,
23584            p.num_kv_heads,
23585            p.head_dim,
23586        );
23587        let synth = |n: usize, salt: usize| -> Vec<f32> {
23588            (0..n)
23589                .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
23590                .collect()
23591        };
23592        let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
23593            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23594        };
23595        p.mtp = Some(MtpModule {
23596            enorm: vec![1.0; h],
23597            hnorm: vec![1.0; h],
23598            eh_proj: qt(h, 2 * h, 301),
23599            layer: LayerWeights {
23600                input_norm: vec![1.0; h],
23601                post_norm: vec![1.0; h],
23602                attn_out_norm: None,
23603                ffn_out_norm: None,
23604                layer_scale: None,
23605                ffn: FfnKind::Dense(DenseFfn {
23606                    gate_proj: qt(inter, h, 315),
23607                    up_proj: qt(inter, h, 316),
23608                    down_proj: qt(h, inter, 317),
23609                    act: Act::Silu,
23610                    down_t: None,
23611                    segs: Vec::new(),
23612                }),
23613                attn: AttnKind::Full {
23614                    bias: None,
23615                    wq: qt(heads * hd, h, 311),
23616                    wk: qt(kv * hd, h, 312),
23617                    wv: qt(kv * hd, h, 313),
23618                    wo: qt(h, heads * hd, 314),
23619                    q_norm: None,
23620                    k_norm: None,
23621                    output_gate: false,
23622                    softplus_gate: None,
23623                },
23624            },
23625            final_norm: vec![1.0; h],
23626            kv: crate::kv_cache::LayerKvCache::new(kv, hd),
23627        });
23628    }
23629
23630    /// Attach a synthetic MTP head with the trunk's conventional width.
23631    fn attach_test_mtp(p: &mut Pipeline) {
23632        attach_test_mtp_width(p, p.intermediate_size);
23633    }
23634
23635    #[test]
23636    fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
23637        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
23638        // This differs from the trunk FFN width (16). A result shaped like
23639        // the trunk probe would be a silent, dangerous compaction bug.
23640        attach_test_mtp_width(&mut p, 11);
23641        let ids = [3, 9, 4, 12, 7, 2];
23642        let trunk = p.probe_ffn_mass(&ids);
23643        assert_eq!(trunk[0].len(), p.intermediate_size);
23644
23645        let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
23646        assert_eq!(got.len(), 1);
23647        assert_eq!(got[0].mass.len(), 11);
23648        assert_eq!(got[0].squared_mass.len(), 11);
23649        assert_eq!(got[0].observed_positions, ids.len() - 1);
23650        assert!(got[0].mass.iter().any(|&v| v > 0.0));
23651        assert!(got[0]
23652            .mass
23653            .iter()
23654            .zip(&got[0].squared_mass)
23655            .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23656        assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
23657        assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
23658
23659        // Force an error *after* the MTP attention has appended a draft KV
23660        // row. This proves the error path clears detached draft state, not
23661        // merely the easy invalid-token preflight branch.
23662        let h = p.hidden_size;
23663        let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
23664            panic!("synthetic MTP must be dense")
23665        };
23666        dense.segs.push(FfnSeg {
23667            gate: QTensor::from_f32(vec![0.0; h], 1, h),
23668            up: QTensor::from_f32(vec![0.0; h], 1, h),
23669            down: QTensor::from_f32(vec![0.0; h], h, 1),
23670            start: dense.gate_proj.rows(),
23671            width: 1,
23672        });
23673        let err = p.probe_mtp_ffn(&ids);
23674        assert!(err
23675            .unwrap_err()
23676            .contains("tube-segmented draft FFNs are not supported"));
23677        assert_eq!(p.kv_cache.seq_len(), 0);
23678        assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
23679    }
23680
23681    #[test]
23682    fn speculative_equals_vanilla_greedy() {
23683        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23684        // Exercise the experimental host pair deterministically; the native
23685        // and wgpu batch graphs have independent coverage.
23686        let _env = ScopedTestEnv::set(&[
23687            ("CMF_GPU", Some("0")),
23688            ("CMF_GPU_WGPU_GRAPH", Some("0")),
23689            ("CMF_MTP_PAIR", Some("1")),
23690        ]);
23691        let run = |spec: bool| {
23692            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23693            p.sampler_config.temperature = 0.0;
23694            attach_test_mtp(&mut p);
23695            p.speculative = spec;
23696            let r = p.generate("abcdef", 12, None, None).unwrap();
23697            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23698        };
23699        let (vanilla, d0, _) = run(false);
23700        let (spec, d1, a1) = run(true);
23701        assert_eq!(d0, 0, "vanilla path must not draft");
23702        assert!(d1 > 0, "speculative path must draft");
23703        assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
23704        assert_eq!(
23705            vanilla, spec,
23706            "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
23707        );
23708    }
23709
23710    #[test]
23711    fn speculative_accepts_constant_oracle() {
23712        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23713        let _env = ScopedTestEnv::set(&[
23714            ("CMF_GPU", Some("0")),
23715            ("CMF_GPU_WGPU_GRAPH", Some("0")),
23716            ("CMF_MTP_PAIR", Some("1")),
23717        ]);
23718        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23719        p.sampler_config.temperature = 0.0;
23720        p.sampler_config.repetition_penalty = 1.0;
23721        // Constant lm_head → every logit equal → both the main model and
23722        // the draft head argmax to token 0: acceptance must be 100%.
23723        p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
23724        attach_test_mtp(&mut p);
23725        p.speculative = true;
23726        let r = p.generate("abcd", 10, None, None).unwrap();
23727        assert!(r.mtp_drafted > 0);
23728        assert_eq!(
23729            r.mtp_accepted, r.mtp_drafted,
23730            "constant logits → every draft accepted"
23731        );
23732        // Ties resolve to the same token in both the main and draft
23733        // heads — the sequence is one repeated token.
23734        assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
23735    }
23736
23737    #[test]
23738    fn host_fused_mtp_pair_is_opt_in() {
23739        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23740        let _env = ScopedTestEnv::set(&[
23741            ("CMF_GPU", Some("0")),
23742            ("CMF_GPU_WGPU_GRAPH", Some("0")),
23743            ("CMF_MTP_PAIR", None),
23744        ]);
23745        let run = |spec: bool| {
23746            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23747            p.sampler_config.temperature = 0.0;
23748            attach_test_mtp(&mut p);
23749            p.speculative = spec;
23750            let r = p.generate("abcdef", 12, None, None).unwrap();
23751            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23752        };
23753        let (plain, d0, _) = run(false);
23754        let (guarded, d1, a1) = run(true);
23755        assert_eq!(d0, 0);
23756        assert_eq!(d1, 0, "host MTP pair must remain opt-in");
23757        assert_eq!(a1, 0);
23758        assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
23759    }
23760
23761    #[test]
23762    fn empty_prompt_is_an_error_not_a_panic() {
23763        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23764        let r = p.generate("", 4, None, None);
23765        assert!(r.is_err(), "empty prompt must be a clean error");
23766    }
23767
23768    #[test]
23769    fn every_token_enters_kv_exactly_once() {
23770        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23771        // Greedy so no RNG variance; byte tokenizer → 3 prompt tokens.
23772        p.sampler_config.temperature = 0.0;
23773        let r = p.generate("abc", 2, None, None).unwrap();
23774        assert_eq!(r.prompt_tokens, 3);
23775        // prompt(3) + first sampled token forwarded before second logits:
23776        // step0 samples from prefill hidden (no extra forward), then
23777        // forwards t1 → cache 4; step1 samples, loop ends (max_tokens).
23778        assert_eq!(
23779            p.kv_cache.seq_len(),
23780            3 + r.tokens_generated - 1,
23781            "each token must be cached exactly once (v1 cached the last prompt token twice)"
23782        );
23783    }
23784
23785    #[test]
23786    fn generation_is_reproducible_with_seed() {
23787        let run = || {
23788            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23789            p.generate("hello", 8, None, None).unwrap().token_ids
23790        };
23791        assert_eq!(run(), run());
23792    }
23793
23794    #[test]
23795    fn resetting_sampler_restarts_the_seeded_stream() {
23796        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23797        let config = SamplerConfig {
23798            seed: Some(1234),
23799            ..SamplerConfig::default()
23800        };
23801        p.set_sampler_config(config.clone());
23802        let first = p.generate("hello", 8, None, None).unwrap().token_ids;
23803        p.set_sampler_config(config);
23804        let second = p.generate("hello", 8, None, None).unwrap().token_ids;
23805        assert_eq!(first, second);
23806    }
23807
23808    #[test]
23809    fn eviction_bounds_the_cache() {
23810        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23811        p.kv_cache.max_seq_len = 6;
23812        p.sampler_config.temperature = 0.0;
23813        let _ = p.generate("abcd", 12, None, None).unwrap();
23814        assert!(
23815            p.kv_cache.seq_len() <= 6 + 1,
23816            "cache must stay bounded by max_seq_len (got {})",
23817            p.kv_cache.seq_len()
23818        );
23819    }
23820
23821    #[test]
23822    fn confidence_matches_tokens_and_is_a_probability() {
23823        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23824        p.sampler_config.temperature = 0.0;
23825        p.sampler_config.repetition_penalty = 1.0;
23826        let r = p.generate("abcd", 10, None, None).unwrap();
23827        assert_eq!(
23828            r.token_confidence.len(),
23829            r.token_ids.len(),
23830            "one confidence per emitted token"
23831        );
23832        for &c in &r.token_confidence {
23833            assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
23834        }
23835        // top1_prob is a valid softmax probability.
23836        let logits = [1.0f32, 3.0, 0.5, 3.0];
23837        let p0 = top1_prob_t(&logits, 1, 1.0);
23838        let p1 = top1_prob_t(&logits, 3, 1.0);
23839        assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
23840        assert!(p0 > 0.0 && p0 < 1.0);
23841        // Calibration temperature > 1 softens an over-confident peak.
23842        let sharp = top1_prob_t(&logits, 1, 1.0);
23843        let soft = top1_prob_t(&logits, 1, 2.0);
23844        assert!(soft < sharp, "higher temperature lowers peak confidence");
23845    }
23846
23847    #[test]
23848    fn trace_is_opt_in_and_parallels_the_output() {
23849        // Off by default: the runtime is silent unless observation asked.
23850        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23851        p.sampler_config.temperature = 0.0;
23852        p.sampler_config.repetition_penalty = 1.0;
23853        let r = p.generate("abcd", 10, None, None).unwrap();
23854        assert!(r.traces.is_empty(), "trace must be empty unless enabled");
23855
23856        // On: exactly one row per emitted token, aligned with the output.
23857        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23858        p.sampler_config.temperature = 0.0;
23859        p.sampler_config.repetition_penalty = 1.0;
23860        p.set_trace(true);
23861        let r = p.generate("abcd", 10, None, None).unwrap();
23862        assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
23863        for (i, tr) in r.traces.iter().enumerate() {
23864            assert_eq!(tr.t, i, "trace index is sequential");
23865            assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
23866            assert_eq!(
23867                tr.confidence, r.token_confidence[i],
23868                "trace confidence matches the confidence channel"
23869            );
23870            // No dynamic router in this pipeline → no skill, no coherence.
23871            assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
23872        }
23873    }
23874
23875    #[test]
23876    fn explain_prefill_logits_match_greedy_first_token() {
23877        // `cortiq explain` shows the next-token distribution from
23878        // prefill_next_logits; its argmax must equal what greedy generate
23879        // actually emits first — otherwise explain would lie.
23880        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23881        p.sampler_config.temperature = 0.0;
23882        p.sampler_config.repetition_penalty = 1.0;
23883        let ids = p.tokenizer.encode("abcd");
23884        let logits = p.prefill_next_logits(&ids, None);
23885        let argmax = logits
23886            .iter()
23887            .enumerate()
23888            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
23889            .unwrap()
23890            .0 as u32;
23891        let r = p.generate("abcd", 1, None, None).unwrap();
23892        assert_eq!(
23893            argmax, r.token_ids[0],
23894            "explain preview must match greedy emit"
23895        );
23896    }
23897
23898    #[test]
23899    fn laguna_shared_expert_is_unconditionally_added() {
23900        let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
23901        let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
23902        let zero_dense = || DenseFfn {
23903            gate_proj: matrix(vec![0.0; 4]),
23904            up_proj: matrix(vec![0.0; 4]),
23905            down_proj: matrix(vec![0.0; 4]),
23906            act: Act::Silu,
23907            down_t: None,
23908            segs: Vec::new(),
23909        };
23910        let shared = DenseFfn {
23911            gate_proj: identity(),
23912            up_proj: identity(),
23913            down_proj: identity(),
23914            act: Act::Silu,
23915            down_t: None,
23916            segs: Vec::new(),
23917        };
23918        let x = [1.0, 2.0];
23919        let expected = dense_ffn(&shared, &x, None);
23920        let moe = MoeFfn {
23921            router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
23922            experts: vec![zero_dense()],
23923            top_k: 1,
23924            norm_topk_prob: true,
23925            router_sigmoid: true,
23926            sigmoid_denom_eps: 1e-6,
23927            expert_bias: None,
23928            routed_scaling: 1.0,
23929            route_tau: None,
23930            shared: Some((shared, None)),
23931            stats: std::cell::RefCell::new(Vec::new()),
23932            act_sq: std::cell::RefCell::new(Vec::new()),
23933            act_rows: std::cell::RefCell::new(Vec::new()),
23934            mask: None,
23935            per_expert_scale: None,
23936            router_input_norm: false,
23937            resonance: None,
23938            grown: Vec::new(),
23939        };
23940        let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
23941        for (actual, expected) in actual.iter().zip(expected) {
23942            assert!((actual - expected).abs() < 1e-6);
23943        }
23944    }
23945
23946    /// A tiny MiMo-V2-shaped stack (the M3 fixture): layers [full, sliding,
23947    /// sliding, full]; 4 Q heads over 1 (full) / 2 (sliding) KV heads;
23948    /// head_dim 8 with 4-wide V heads; partial rotary 4 at θ 1e7 (full) /
23949    /// 1e4 (sliding); window 3; learned sinks on the sliding layers; layer
23950    /// 0 a dense FFN, layers 1..3 sigmoid-routed MoE with a selection bias
23951    /// (4 experts, top-2, renormalized, no shared expert). Geometry and
23952    /// sinks go through the same `set_attn_geometry` / `set_layer_sinks`
23953    /// the loader calls.
23954    fn mimo_test_pipeline() -> Pipeline {
23955        let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
23956        let kvh = [1usize, 2, 2, 1];
23957        let synth = |n: usize, salt: usize| -> Vec<f32> {
23958            (0..n)
23959                .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23960                .collect()
23961        };
23962        let qt = |rows: usize, cols: usize, salt: usize| {
23963            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23964        };
23965        let dense = |inter: usize, salt: usize| DenseFfn {
23966            gate_proj: qt(inter, hs, salt),
23967            up_proj: qt(inter, hs, salt + 1),
23968            down_proj: qt(hs, inter, salt + 2),
23969            act: Act::Silu,
23970            down_t: None,
23971            segs: Vec::new(),
23972        };
23973        let layers: Vec<LayerWeights> = (0..4)
23974            .map(|li| LayerWeights {
23975                input_norm: vec![1.0; hs],
23976                post_norm: vec![1.0; hs],
23977                attn_out_norm: None,
23978                ffn_out_norm: None,
23979                layer_scale: None,
23980                ffn: if li == 0 {
23981                    FfnKind::Dense(dense(inter, 50))
23982                } else {
23983                    FfnKind::Moe(MoeFfn {
23984                        router: qt(4, hs, 60 + li),
23985                        experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
23986                        top_k: 2,
23987                        norm_topk_prob: true,
23988                        router_sigmoid: true,
23989                        sigmoid_denom_eps: 1e-6,
23990                        expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
23991                        routed_scaling: 1.0,
23992                        route_tau: None,
23993                        shared: None,
23994                        stats: std::cell::RefCell::new(Vec::new()),
23995                        act_sq: std::cell::RefCell::new(Vec::new()),
23996                        act_rows: std::cell::RefCell::new(Vec::new()),
23997                        mask: None,
23998                        per_expert_scale: None,
23999                        router_input_norm: false,
24000                        resonance: None,
24001                        grown: Vec::new(),
24002                    })
24003                },
24004                attn: AttnKind::Full {
24005                    wq: qt(nh * hd, hs, li * 10 + 1),
24006                    wk: qt(kvh[li] * hd, hs, li * 10 + 2),
24007                    wv: qt(kvh[li] * vd, hs, li * 10 + 3),
24008                    wo: qt(hs, nh * vd, li * 10 + 4),
24009                    q_norm: None,
24010                    k_norm: None,
24011                    output_gate: false,
24012                    softplus_gate: None,
24013                    bias: None,
24014                },
24015            })
24016            .collect();
24017        let mut p = Pipeline::new(
24018            Tokenizer::byte_level(),
24019            PipelineWeights {
24020                embed_tokens: qt(vocab, hs, 100),
24021                layers,
24022                lm_head: qt(vocab, hs, 200),
24023                final_norm: vec![1.0; hs],
24024            },
24025            hs,
24026            inter,
24027            nh,
24028            1, // header num_kv_heads (the full layers')
24029            hd,
24030            4,
24031            4,
24032            false,
24033            vocab,
24034            1e-6,
24035            1e7,
24036            NormStyle::Qwen,
24037            4096,
24038            SamplerConfig {
24039                seed: Some(7),
24040                ..Default::default()
24041            },
24042        );
24043        // Diagnostics stay off whatever the test environment exports.
24044        p.layer_dump = None;
24045        p.set_rotary(4, 1e7);
24046        p.sliding_layers = Some(vec![false, true, true, false]);
24047        p.swa = Some((3, usize::MAX));
24048        p.rotary_dim_local = Some(4);
24049        p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
24050        p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
24051        p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
24052        p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
24053        p
24054    }
24055
24056    #[test]
24057    fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
24058        let mut p = mimo_test_pipeline();
24059        p.speculative = false;
24060        p.ignore_eos = true;
24061        p.sampler_config.temperature = 0.0;
24062        p.sampler_config.repetition_penalty = 1.0;
24063        let a = vec![3, 5, 7, 9, 11, 13];
24064        let b = vec![4, 8, 12, 16, 20, 24];
24065        let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
24066        let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
24067        // Same placeholder IDs as an earlier request are not a cache key
24068        // for different media. The actual rows, not a re-embedding of a,
24069        // must determine the continuation.
24070        let actual = p
24071            .generate_from_embeds(&a, &rows, 8, None, None)
24072            .unwrap()
24073            .token_ids;
24074        assert_eq!(actual, expected);
24075        assert!(p.kv_history.is_empty());
24076        let mut extended = a.clone();
24077        extended.push(17);
24078        let after_media = p
24079            .generate_from_ids(&extended, 8, None, None)
24080            .unwrap()
24081            .token_ids;
24082        p.reset_session();
24083        let fresh = p
24084            .generate_from_ids(&extended, 8, None, None)
24085            .unwrap()
24086            .token_ids;
24087        assert_eq!(after_media, fresh);
24088        assert!(
24089            p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
24090                .is_err()
24091        );
24092        // Force a real token-prefix reuse opportunity into the media call.
24093        // Those labels are unchanged, but their embeddings now describe a
24094        // different source sequence and every KV row must be rebuilt.
24095        p.reset_session();
24096        p.generate_from_ids(&a, 1, None, None).unwrap();
24097        let mut media_ids = p.kv_history.clone();
24098        assert!(!media_ids.is_empty());
24099        media_ids.extend_from_slice(&[19, 21, 23]);
24100        let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
24101        let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
24102        let mut oracle = mimo_test_pipeline();
24103        oracle.speculative = false;
24104        oracle.ignore_eos = true;
24105        oracle.sampler_config.temperature = 0.0;
24106        oracle.sampler_config.repetition_penalty = 1.0;
24107        let expected = oracle
24108            .generate_from_ids(&source_ids, 8, None, None)
24109            .unwrap()
24110            .token_ids;
24111        assert_eq!(
24112            p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
24113                .unwrap()
24114                .token_ids,
24115            expected
24116        );
24117        assert!(p.kv_history.is_empty());
24118        let mut bad = rows;
24119        bad[0] = f32::NAN;
24120        assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
24121    }
24122
24123    fn f32_bits(v: &[f32]) -> Vec<u32> {
24124        v.iter().map(|x| x.to_bits()).collect()
24125    }
24126
24127    /// M3 acceptance: on the MiMo-shaped stack the decode walk (one
24128    /// position at a time through `forward_layers`) and the batched
24129    /// prefill (`prefill_batch_span`, whole prompt and split in two
24130    /// chunks) give bit-identical logits at all 12 positions — per-layer
24131    /// KV heads, narrow V, sinks, the window and the biased sigmoid MoE all
24132    /// agree across the two walks.
24133    #[test]
24134    fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
24135        let mut p = mimo_test_pipeline();
24136        let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
24137        assert_eq!(kv, vec![1, 2, 2, 1]);
24138        assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
24139        assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
24140        let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
24141        let hs = p.hidden_size;
24142        let mut decode = Vec::new();
24143        for (pos, &id) in ids.iter().enumerate() {
24144            let e = p.embed_single(id);
24145            let h = p.forward_layers(&e, pos, None);
24146            decode.push(p.logits_from_hidden(&h));
24147        }
24148        for l in &p.kv_cache.layers {
24149            assert_eq!(l.seq_len, 12);
24150            // V rows are padded to head_dim inside the cache.
24151            assert_eq!(l.head_values(0).len(), 12 * 8);
24152        }
24153        assert!(decode.iter().flatten().all(|v| v.is_finite()));
24154
24155        p.clear_sequence_state();
24156        let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24157        for pos in 0..ids.len() {
24158            let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
24159            assert_eq!(
24160                f32_bits(&decode[pos]),
24161                f32_bits(&lg),
24162                "whole prompt, pos {pos}"
24163            );
24164        }
24165
24166        p.clear_sequence_state();
24167        let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
24168        let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
24169        for pos in 0..ids.len() {
24170            let row = if pos < 5 {
24171                &a[pos * hs..(pos + 1) * hs]
24172            } else {
24173                &b[(pos - 5) * hs..(pos - 4) * hs]
24174            };
24175            let lg = p.logits_from_hidden(row);
24176            assert_eq!(
24177                f32_bits(&decode[pos]),
24178                f32_bits(&lg),
24179                "two chunks, pos {pos}"
24180            );
24181        }
24182
24183        // The fixture is not degenerate: the sinks and the window each
24184        // change the answer.
24185        let last = |p: &mut Pipeline| {
24186            p.clear_sequence_state();
24187            let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24188            p.logits_from_hidden(&hb[11 * hs..12 * hs])
24189        };
24190        let base = last(&mut p);
24191        let mut no_sinks = mimo_test_pipeline();
24192        for l in &mut no_sinks.kv_cache.layers {
24193            l.sinks = None;
24194        }
24195        assert_ne!(
24196            f32_bits(&last(&mut no_sinks)),
24197            f32_bits(&base),
24198            "sinks are live"
24199        );
24200        let mut wide = mimo_test_pipeline();
24201        wide.swa = Some((64, usize::MAX));
24202        assert_ne!(
24203            f32_bits(&last(&mut wide)),
24204            f32_bits(&base),
24205            "window is live"
24206        );
24207
24208        // Generation runs end to end on the same stack.
24209        p.clear_sequence_state();
24210        p.ignore_eos = true;
24211        let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
24212        assert_eq!(r.token_ids.len(), 4);
24213    }
24214
24215    /// A Spark-X2.5-shaped stack: layers 0..3 slide (window 6), layer 3
24216    /// is global, every attention carries the head-wise sigmoid g_proj gate.
24217    fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
24218        let (hs, nh) = (16usize, 4usize);
24219        let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
24220        p.layer_dump = None;
24221        p.swa = Some((6, 4));
24222        p.proj_gate_sigmoid = true;
24223        for (li, lw) in p.weights.layers.iter_mut().enumerate() {
24224            if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
24225                let g: Vec<f32> = (0..nh * hs)
24226                    .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
24227                    .collect();
24228                *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
24229            }
24230        }
24231        p.swa_trim = trim;
24232        p
24233    }
24234
24235    /// Trimming the sliding tails changes nothing but memory: the same
24236    /// prompt in uneven chunks, a decode walk, and fused pairs with a
24237    /// 2-row rollback give bit-identical hiddens with and without it,
24238    /// while the trimmed sliding layers stay bounded and the global layer
24239    /// keeps every row.
24240    #[test]
24241    fn swa_trim_pipeline_matches_untrimmed_bitwise() {
24242        let mut a = spark_test_pipeline(None);
24243        let mut b = spark_test_pipeline(Some((2, 4)));
24244        assert_eq!(
24245            (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
24246            vec![Some(6), Some(6), Some(6), None]
24247        );
24248        let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
24249        let mut pos = 0usize;
24250        for &n in [5usize, 13, 7, 16].iter().cycle() {
24251            if pos >= ids.len() {
24252                break;
24253            }
24254            let end = (pos + n).min(ids.len());
24255            let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
24256            let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
24257            assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
24258            pos = end;
24259        }
24260        for _ in 0..23 {
24261            let e = a.embed_single(((pos * 7) % 64) as u32);
24262            let ha = a.forward_layers(&e, pos, None);
24263            let hb = b.forward_layers(&e, pos, None);
24264            assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
24265            for li in 0..3 {
24266                assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
24267            }
24268            pos += 1;
24269        }
24270        for round in 0..9 {
24271            let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
24272            let (a1, a2) = a.forward_pair(&e1, &e2, pos);
24273            let (b1, b2) = b.forward_pair(&e1, &e2, pos);
24274            assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
24275            assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
24276            if round % 2 == 1 {
24277                // A rejected draft: both lanes roll back.
24278                for p in [&mut a, &mut b] {
24279                    for l in &mut p.kv_cache.layers {
24280                        l.truncate_last(2);
24281                    }
24282                }
24283            } else {
24284                pos += 2;
24285            }
24286        }
24287        let e = a.embed_single(9);
24288        let (ha, hb) = (
24289            a.forward_layers(&e, pos, None),
24290            b.forward_layers(&e, pos, None),
24291        );
24292        assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
24293        for li in 0..3 {
24294            let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
24295            assert!(lb.base() > 0, "layer {li} trimmed");
24296            assert_eq!(la.base(), 0);
24297            assert_eq!(lb.pos_len(), la.seq_len);
24298            assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
24299        }
24300        let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
24301        assert_eq!(
24302            (gb.base(), gb.seq_len),
24303            (0, ga.seq_len),
24304            "the global layer keeps all"
24305        );
24306        assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
24307        assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
24308    }
24309
24310    /// The network split's KvFetch hand-off on a trimmed stack: every
24311    /// layer shipped over the wire mid-sequence (the sliding ones as
24312    /// `FullTail`) into a fresh pipeline that never ran a position, which
24313    /// then decodes on — bit for bit what the untrimmed pipeline decodes.
24314    #[test]
24315    fn swa_trim_wire_handoff_continues_bitwise() {
24316        let mut a = spark_test_pipeline(None);
24317        let mut b = spark_test_pipeline(Some((2, 4)));
24318        let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
24319        let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
24320        let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
24321        assert_eq!(f32_bits(&ha), f32_bits(&hb));
24322        let mut pos = ids.len();
24323        for _ in 0..5 {
24324            let e = a.embed_single(((pos * 5) % 64) as u32);
24325            assert_eq!(
24326                f32_bits(&a.forward_layers(&e, pos, None)),
24327                f32_bits(&b.forward_layers(&e, pos, None))
24328            );
24329            pos += 1;
24330        }
24331        let mut c = spark_test_pipeline(Some((2, 4)));
24332        for li in 0..4 {
24333            let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
24334            c.kv_cache.layers[li].import_wire(&bytes).unwrap();
24335        }
24336        assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
24337        assert_eq!(c.kv_cache.seq_len(), pos);
24338        for step in 0..17 {
24339            let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
24340            let ha = a.forward_layers(&e, pos, None);
24341            let hc = c.forward_layers(&e, pos, None);
24342            assert_eq!(
24343                f32_bits(&ha),
24344                f32_bits(&hc),
24345                "after the hand-off, step {step}"
24346            );
24347            pos += 1;
24348        }
24349        assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
24350    }
24351
24352    /// A synthetic MiMo draft stack for `mimo_test_pipeline` (the SWA
24353    /// geometry of its sliding layers: 2 KV heads, head 8 / V 4).  Widths
24354    /// are per-draft-layer so the MTP collector cannot accidentally borrow
24355    /// the trunk's `intermediate_size`.
24356    fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
24357        let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
24358        let synth = |len: usize, salt: usize| -> Vec<f32> {
24359            (0..len)
24360                .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
24361                .collect()
24362        };
24363        let qt = |rows: usize, cols: usize, salt: usize| {
24364            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
24365        };
24366        let layers = widths
24367            .iter()
24368            .copied()
24369            .enumerate()
24370            .map(|(k, inter)| {
24371                let s = 500 + k * 40;
24372                let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
24373                kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
24374                MtpModule {
24375                    enorm: vec![1.0; hs],
24376                    hnorm: vec![1.0; hs],
24377                    eh_proj: qt(hs, 2 * hs, s),
24378                    layer: LayerWeights {
24379                        input_norm: vec![1.0; hs],
24380                        post_norm: vec![1.0; hs],
24381                        attn_out_norm: None,
24382                        ffn_out_norm: None,
24383                        layer_scale: None,
24384                        attn: AttnKind::Full {
24385                            wq: qt(nh * hd, hs, s + 1),
24386                            wk: qt(nkv * hd, hs, s + 2),
24387                            wv: qt(nkv * vd, hs, s + 3),
24388                            wo: qt(hs, nh * vd, s + 4),
24389                            q_norm: None,
24390                            k_norm: None,
24391                            output_gate: false,
24392                            softplus_gate: None,
24393                            bias: None,
24394                        },
24395                        ffn: FfnKind::Dense(DenseFfn {
24396                            gate_proj: qt(inter, hs, s + 5),
24397                            up_proj: qt(inter, hs, s + 6),
24398                            down_proj: qt(hs, inter, s + 7),
24399                            act: Act::Silu,
24400                            down_t: None,
24401                            segs: Vec::new(),
24402                        }),
24403                    },
24404                    final_norm: vec![1.0; hs],
24405                    kv,
24406                }
24407            })
24408            .collect();
24409        mimo_mtp::MimoMtp::from_layers(layers)
24410    }
24411
24412    fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
24413        mimo_test_mtp_widths(&vec![24; n], gain)
24414    }
24415
24416    fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
24417        p.clear_sequence_state();
24418        p.speculative = spec;
24419        p.ignore_eos = true;
24420        p.sampler_config.temperature = 0.0;
24421        p.generate_from_ids(ids, n, None, None).unwrap()
24422    }
24423
24424    #[test]
24425    fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
24426        let mut p = mimo_test_pipeline();
24427        let widths = [7usize, 13, 29];
24428        assert_ne!(widths[0], p.intermediate_size);
24429        assert_ne!(widths[1], p.intermediate_size);
24430        assert_ne!(widths[2], p.intermediate_size);
24431        p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
24432        let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
24433
24434        let trunk = p.probe_ffn_mass(&ids);
24435        assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
24436        let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
24437        assert_eq!(got.len(), widths.len());
24438        for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
24439            assert_eq!(stat.mass.len(), width, "draft layer {k}");
24440            assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
24441            assert_eq!(stat.observed_positions, ids.len() - k - 1);
24442            assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
24443            assert!(stat
24444                .mass
24445                .iter()
24446                .zip(&stat.squared_mass)
24447                .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
24448        }
24449        assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
24450        let st = p.mimo_mtp.as_ref().unwrap();
24451        assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
24452
24453        // As above, reject only after a MiMo draft KV has been appended.
24454        // The detached multi-layer owner must still be reset on that error.
24455        let h = p.hidden_size;
24456        let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
24457            panic!("synthetic MiMo MTP must be dense")
24458        };
24459        dense.segs.push(FfnSeg {
24460            gate: QTensor::from_f32(vec![0.0; h], 1, h),
24461            up: QTensor::from_f32(vec![0.0; h], 1, h),
24462            down: QTensor::from_f32(vec![0.0; h], h, 1),
24463            start: dense.gate_proj.rows(),
24464            width: 1,
24465        });
24466        assert!(p
24467            .probe_mtp_ffn(&ids)
24468            .unwrap_err()
24469            .contains("tube-segmented draft FFNs are not supported"));
24470        assert_eq!(p.kv_cache.seq_len(), 0);
24471        assert!(p
24472            .mimo_mtp
24473            .as_ref()
24474            .unwrap()
24475            .layers
24476            .iter()
24477            .all(|m| m.kv.seq_len == 0));
24478    }
24479
24480    #[test]
24481    fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
24482        fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
24483            p.clear_sequence_state();
24484            let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
24485            let _exact = ExactMtpCalibrationGuard::enter();
24486            let got = crate::gpu::cpu_scope(|| {
24487                let trunk = p.probe_trunk_rows(ids)?;
24488                p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
24489            })
24490            .expect("teacher-forced MiMo probe");
24491            st.reset();
24492            p.mimo_mtp = Some(st);
24493            p.reset_mtp_probe_state();
24494            got
24495        }
24496
24497        for post_norm_hidden in [false, true] {
24498            let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
24499            let make = || {
24500                let mut p = mimo_test_pipeline();
24501                let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
24502                st.post_norm_hidden = post_norm_hidden;
24503                p.mimo_mtp = Some(st);
24504                p
24505            };
24506            let mut full = make();
24507            let expected = collect(&mut full, &ids, usize::MAX);
24508            for rows in [1usize, 7, 128] {
24509                let mut chunked = make();
24510                let got = collect(&mut chunked, &ids, rows);
24511                assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
24512                for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
24513                    assert_eq!(
24514                        got.observed_positions, expected.observed_positions,
24515                        "post={post_norm_hidden}, rows={rows}, layer={layer}"
24516                    );
24517                    assert_eq!(
24518                        got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24519                        expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24520                        "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
24521                    );
24522                    assert_eq!(
24523                        got.squared_mass
24524                            .iter()
24525                            .map(|v| v.to_bits())
24526                            .collect::<Vec<_>>(),
24527                        expected
24528                            .squared_mass
24529                            .iter()
24530                            .map(|v| v.to_bits())
24531                            .collect::<Vec<_>>(),
24532                        "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
24533                    );
24534                }
24535            }
24536        }
24537    }
24538
24539    /// The draft stack's incremental rounds (a few rows per layer, last
24540    /// round's provisional rows dropped) give exactly the teacher-forced
24541    /// table of one causal pass per layer over the whole sequence — the
24542    /// table `tools/mimo_ref.py mtp` computes for variant A: layer k, row
24543    /// j reads (x[j+k+1], norm(h_j)) at RoPE position j.
24544    #[test]
24545    fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
24546        // Both readings of the backbone hidden: pre-final-norm (default)
24547        // and post-final-norm (`CMF_MIMO_MTP_HIDDEN=post`).
24548        for post in [false, true] {
24549            let mut p = mimo_test_pipeline();
24550            // A non-trivial final norm, so the two readings differ.
24551            p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
24552            let mut st0 = mimo_test_mtp(3, 1.0);
24553            st0.post_norm_hidden = post;
24554            p.mimo_mtp = Some(st0);
24555            let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
24556            let hs = p.hidden_size;
24557            let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24558            p.mimo_note_rows(&hb, 0);
24559            let mut st = p.mimo_mtp.take().unwrap();
24560            // Incremental: one round per t through the decode path (later
24561            // tokens from `ids`, the probe's teacher forcing).
24562            let k = 3;
24563            let mut inc = Vec::new();
24564            for t in 0..ids.len() - k - 1 {
24565                inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
24566            }
24567            // Reference: per layer, ONE batched causal pass over all rows
24568            // with fresh caches.
24569            let s = ids.len();
24570            let mut reference = vec![vec![0u32; k]; s - k - 1];
24571            let mut fresh = mimo_test_mtp(3, 1.0);
24572            for (layer, m) in fresh.layers.iter_mut().enumerate() {
24573                let n = s - layer - 1;
24574                let mut cats = vec![0.0f32; n * 2 * hs];
24575                for j in 0..n {
24576                    let e = p.embed_single(ids[j + layer + 1]);
24577                    let raw = &hb[j * hs..(j + 1) * hs];
24578                    let g = if post {
24579                        inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
24580                    } else {
24581                        raw.to_vec()
24582                    };
24583                    let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
24584                    inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
24585                    inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
24586                }
24587                let mut x = vec![0.0f32; n * hs];
24588                m.eh_proj.matmat(&cats, n, &mut x, None);
24589                p.mimo_mtp_block(m, &mut x, n, 0);
24590                for (t, row) in reference.iter_mut().enumerate() {
24591                    let y = inference::rms_norm(
24592                        &x[t * hs..(t + 1) * hs],
24593                        &m.final_norm,
24594                        p.rms_eps,
24595                        p.norm_style,
24596                    );
24597                    row[layer] = sampler::argmax(&p.lm_head_forward(&y));
24598                }
24599            }
24600            assert_eq!(inc, reference, "post_norm_hidden = {post}");
24601            // Not a degenerate table: the drafts vary.
24602            let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
24603            assert!(distinct.len() > 3, "{inc:?}");
24604            // Each layer's cache ends holding rows up to the last round start.
24605            let last_t = ids.len() - k - 2;
24606            for m in &st.layers {
24607                assert_eq!(m.kv.seq_len, last_t + 1);
24608            }
24609        }
24610    }
24611
24612    /// Greedy with the MiMo draft stack is the plain greedy stream, token
24613    /// for token — with the real draft layers (low acceptance) and with a
24614    /// drafter that is right most of the time (exercises accepted prefixes
24615    /// of every length, the KV truncation of the rejected rows and the
24616    /// logits hand-off to the loop top), under the default repetition
24617    /// penalty.
24618    #[test]
24619    fn mimo_speculative_greedy_equals_plain_greedy() {
24620        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
24621        let _env = ScopedTestEnv::set(&[
24622            ("CMF_GPU", Some("0")),
24623            ("CMF_GPU_WGPU_GRAPH", Some("0")),
24624        ]);
24625        let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
24626        let n = 24;
24627        let mut p = mimo_test_pipeline();
24628        let plain = mimo_greedy(&mut p, &ids, n, false);
24629        assert_eq!(plain.mtp_drafted, 0);
24630        assert_eq!(plain.token_ids.len(), n);
24631        let plain_kv = p.kv_cache.layers[0].seq_len;
24632
24633        // Real draft layers.
24634        p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
24635        let spec = mimo_greedy(&mut p, &ids, n, true);
24636        assert!(spec.mtp_drafted > 0, "the round must draft");
24637        assert_eq!(spec.token_ids, plain.token_ids);
24638        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24639
24640        // A drafter reading the true continuation with every fifth token
24641        // wrong: accepted prefixes of 0..=3 all occur.
24642        let mut truth: Vec<u32> = ids.clone();
24643        truth.extend(&plain.token_ids);
24644        let mut noisy = truth.clone();
24645        for (i, t) in noisy.iter_mut().enumerate() {
24646            if i % 5 == 0 {
24647                *t = (*t + 1) % 64;
24648            }
24649        }
24650        let mut st = mimo_test_mtp(3, 1.0);
24651        st.draft_override = Some(noisy);
24652        p.mimo_mtp = Some(st);
24653        let spec = mimo_greedy(&mut p, &ids, n, true);
24654        assert_eq!(spec.token_ids, plain.token_ids);
24655        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24656        let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
24657        assert_eq!(stats.accepted as usize, spec.mtp_accepted);
24658        assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
24659        assert!(
24660            stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
24661            "{:?}",
24662            stats.accept_hist
24663        );
24664        assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
24665
24666        // A perfect drafter: every draft accepted, rounds of K+1 tokens,
24667        // and the budget is never overrun.
24668        let mut st = mimo_test_mtp(3, 1.0);
24669        st.draft_override = Some(truth);
24670        p.mimo_mtp = Some(st);
24671        let spec = mimo_greedy(&mut p, &ids, n, true);
24672        assert_eq!(spec.token_ids, plain.token_ids);
24673        assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
24674        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24675
24676        // CMF_MTP=0 path: the stack is attached but idle.
24677        let off = mimo_greedy(&mut p, &ids, n, false);
24678        assert_eq!(off.token_ids, plain.token_ids);
24679        assert_eq!(off.mtp_drafted, 0);
24680    }
24681
24682    /// The wgpu graphs carry MiMo-V2's attention per layer (KV heads,
24683    /// narrow V, sinks, windows, two RoPE tables): no attention-level
24684    /// decline for it any more, and the geometry each layer hands the
24685    /// graph is exactly what the CPU attention reads for that layer. The
24686    /// descriptive reasons stay (the Metal graphs and the q1 dropin still
24687    /// decline on them), and what the per-layer geometry cannot express
24688    /// keeps a named wgpu decline.
24689
24690    #[test]
24691    fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
24692        let p = mimo_test_pipeline();
24693        assert_eq!(
24694            p.graph_attn_decline_reason(),
24695            Some("per-layer KV head counts")
24696        );
24697        assert_eq!(p.wgpu_graph_attn_decline(), None);
24698        let g0 = p.graph_attn_geom(0).expect("full layer geometry");
24699        assert_eq!(
24700            (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
24701            (1, 4, 4, None, false)
24702        );
24703        assert_eq!(g0.invf, p.inv_freq.as_slice());
24704        let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
24705        assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
24706        assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
24707        assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
24708        assert_ne!(g0.invf, g1.invf, "two RoPE tables");
24709        let g3 = p.graph_attn_geom(3).expect("full layer geometry");
24710        assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
24711
24712        // No wgpu device in this process: the builders run and decline on
24713        // the (f32, unmapped) experts — never with an attention line.
24714        let emb = p.embed_single(3);
24715        let mut lg = Vec::new();
24716        assert!(
24717            p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
24718                .is_none()
24719        );
24720        let mut hid = emb.clone();
24721        assert_eq!(
24722            p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
24723            crate::gpu::BatchGraphOutcome::Declined
24724        );
24725        assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
24726        assert!(p.try_multi_burst(3, 0, 4).is_none());
24727        assert!(
24728            p.graph_declines().is_empty(),
24729            "no attention decline logged: {:?}",
24730            p.graph_declines()
24731        );
24732        // (No assertion on graph_prefill_preferred: with no attention
24733        // decline it follows the device — a test process that brought a
24734        // wgpu adapter up routes this resident MoE through the graph.)
24735
24736        let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
24737        assert_eq!(plain().graph_attn_decline_reason(), None);
24738        assert_eq!(plain().wgpu_graph_attn_decline(), None);
24739        assert!(
24740            plain().graph_attn_geom(0).is_none(),
24741            "uniform models keep the historical arms"
24742        );
24743        let mut q = plain();
24744        q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
24745        assert_eq!(
24746            q.graph_attn_decline_reason(),
24747            Some("learned attention sinks")
24748        );
24749        assert_eq!(
24750            q.graph_attn_geom(1).unwrap().sink,
24751            Some(&[0.25f32, -0.25][..])
24752        );
24753        let mut q = plain();
24754        q.set_attn_geometry(None, Some(2)).unwrap();
24755        assert_eq!(
24756            q.graph_attn_decline_reason(),
24757            Some("V heads narrower than Q/K heads")
24758        );
24759        assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
24760        let mut q = plain();
24761        q.sliding_layers = Some(vec![true, false]);
24762        q.swa = Some((4, usize::MAX));
24763        assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
24764        assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
24765        assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
24766
24767        // YaRN's post-rotation amplitude factor rides the per-layer graph
24768        // geometry. It is intentionally not an angular scale folded into
24769        // `invf`: the WGPU RoPE kernel receives it separately.
24770        let mut q = mimo_test_pipeline();
24771        q.rope_scale = 2.0;
24772        q.rope_scale_local = 1.25;
24773        assert_eq!(q.wgpu_graph_attn_decline(), None);
24774        assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
24775        assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
24776
24777        // A uniform YaRN model cannot yet pass the factor through the fused
24778        // RKV arm; decline safely instead of producing scale-1 output.
24779        let mut q = plain();
24780        q.rope_scale = 2.0;
24781        assert_eq!(
24782            q.wgpu_graph_attn_decline(),
24783            Some("scaled RoPE without per-layer geometry")
24784        );
24785    }
24786
24787    /// The native q1 attention path has a narrower contract than the WGPU
24788    /// per-layer geometry graph, but it must not discard the exact Flint
24789    /// combination of uniform GQA + sliding windows + learned sinks.  The
24790    /// actual Metal kernel is covered independently with a direct sink
24791    /// softmax test; this is the pure admission test, with no model run.
24792    #[cfg(target_os = "macos")]
24793    #[test]
24794    fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
24795        // rd=64 is in the native rope kernel's lane-pair contract; the
24796        // synthetic stack otherwise has the same uniform K/V geometry that
24797        // Flint needs for q1 (unlike the deliberately ragged MiMo fixture).
24798        let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
24799        p.swa = Some((16, usize::MAX));
24800        p.sliding_layers = Some(vec![true, true]);
24801        p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
24802        p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
24803
24804        assert_eq!(
24805            p.graph_attn_decline_reason(),
24806            Some("learned attention sinks")
24807        );
24808        let swa_graph = p.metal_graph_swa();
24809        assert!(swa_graph, "uniform sliding sink geometry must reach q1");
24810        let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
24811            && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
24812        assert!(
24813            native_sink_attend,
24814            "the q1 sink admission must follow the SWA door"
24815        );
24816        // The batched chunk softmax has no value-less sink column, so its
24817        // separate admission gate must keep this prompt on the host path.
24818        assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
24819    }
24820
24821    #[test]
24822    fn mimo_verify_rewind_preserves_lagging_host_caches() {
24823        let mut p = mimo_test_pipeline();
24824        for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
24825            let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
24826            for _ in 0..if li == 0 { 2 } else { 12 } {
24827                layer.append(&row, &row, &[]);
24828            }
24829        }
24830        p.mimo_verify_rewind(9).unwrap();
24831        assert_eq!(p.kv_cache.layers[0].seq_len, 2);
24832        for layer in &p.kv_cache.layers[1..] {
24833            assert_eq!(layer.seq_len, 9);
24834        }
24835    }
24836
24837    /// CMF_LAYER_DUMP: the decode walk and the batched prefill both write
24838    /// every (position, layer) hidden, the two sets agree byte for byte,
24839    /// and the last layer's file is the stack output.
24840    #[test]
24841    fn layer_dump_covers_every_position_and_layer_on_both_walks() {
24842        let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
24843        let _ = std::fs::remove_dir_all(&dir);
24844        let mut p = mimo_test_pipeline();
24845        let hs = p.hidden_size;
24846        let ids = [5u32, 9, 11, 2, 40];
24847        p.layer_dump = Some(dir.join("decode"));
24848        for (pos, &id) in ids.iter().enumerate() {
24849            let e = p.embed_single(id);
24850            let _ = p.forward_layers(&e, pos, None);
24851        }
24852        p.clear_sequence_state();
24853        p.layer_dump = Some(dir.join("prefill"));
24854        let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24855        for pos in 0..ids.len() {
24856            for li in 0..p.num_layers {
24857                let name = format!("p{pos:06}_l{li:02}.f32");
24858                let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
24859                let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
24860                assert_eq!(a.len(), hs * 4, "{name}");
24861                assert_eq!(a, b, "{name}");
24862            }
24863        }
24864        let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
24865        let vals: Vec<f32> = last
24866            .chunks(4)
24867            .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
24868            .collect();
24869        assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
24870        let _ = std::fs::remove_dir_all(&dir);
24871    }
24872
24873    #[test]
24874    fn attn_geometry_and_sinks_are_validated() {
24875        let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
24876        assert!(
24877            p.set_attn_geometry(Some(vec![2]), None).is_err(),
24878            "one entry per layer"
24879        );
24880        assert!(
24881            p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
24882            "3 does not divide 4"
24883        );
24884        assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
24885        assert!(p.set_attn_geometry(None, Some(0)).is_err());
24886        assert!(
24887            p.set_attn_geometry(None, Some(5)).is_err(),
24888            "V wider than the head"
24889        );
24890        p.set_attn_geometry(None, Some(4)).unwrap();
24891        assert_eq!(
24892            p.v_head_dim, None,
24893            "v_head_dim == head_dim is the uniform case"
24894        );
24895        p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
24896        p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
24897        assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
24898        assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
24899        assert!(
24900            p.kv_cache.layers[1].sinks.is_some(),
24901            "a reshape keeps the layer's sinks"
24902        );
24903        assert_eq!(p.layer_geom(1).0, 4);
24904        assert!(
24905            p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
24906            "one sink per Q head"
24907        );
24908        assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
24909        assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
24910    }
24911
24912    /// The O(1) Nyström state replaces a plain full-context softmax; it
24913    /// must never be armed on a sliding, sink or narrow-V layer.
24914    #[test]
24915    fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
24916        let cfg = || {
24917            Some(crate::nystrom::O1Cfg {
24918                layers: crate::nystrom::O1Layers::All,
24919                m: 4,
24920                w: 8,
24921                sink: 2,
24922                rect: crate::nystrom::O1Rect::Aggregate,
24923            })
24924        };
24925        let mut p = mimo_test_pipeline();
24926        p.set_o1(cfg());
24927        assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
24928        let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
24929        q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
24930        q.sliding_layers = Some(vec![false, false, true]);
24931        q.swa = Some((4, usize::MAX));
24932        q.set_o1(cfg());
24933        assert_eq!(q.o1_flags, vec![true, false, false]);
24934    }
24935
24936    #[test]
24937    fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
24938        const B: usize = 19;
24939        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24940        p.set_o1(Some(crate::nystrom::O1Cfg {
24941            layers: crate::nystrom::O1Layers::All,
24942            m: 4,
24943            w: 8,
24944            sink: 2,
24945            rect: crate::nystrom::O1Rect::Aggregate,
24946        }));
24947        p.o1_begin_with_prefix(Some(B));
24948        let ids: Vec<u32> = (0..B as u32).collect();
24949        let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24950
24951        assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
24952        assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
24953        let next = p.embed_single(B as u32);
24954        let _ = p.forward_layers(&next, B, None);
24955        assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
24956    }
24957
24958    #[test]
24959    fn o1_pair_transition_commits_scratch_before_epoch_publication() {
24960        const B: usize = 19;
24961        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24962        // Keep a real recurrent layer ahead of the Full O(1) layer so the
24963        // pair test observes the GDN lane-2 scratch swap at the same
24964        // boundary, rather than only exercising an artificial scratch vec.
24965        let gdn_cfg = crate::linear_core::GdnCfg {
24966            num_v_heads: 2,
24967            num_k_heads: 1,
24968            key_head_dim: 2,
24969            value_head_dim: 4,
24970            conv_kernel: 3,
24971            hidden_size: 8,
24972            rms_eps: 1e-6,
24973            output_gate_sigmoid: false,
24974        };
24975        let synth = |n: usize, salt: usize| -> Vec<f32> {
24976            (0..n)
24977                .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
24978                .collect()
24979        };
24980        let qt = |rows: usize, cols: usize, salt: usize| {
24981            crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
24982        };
24983        let c_dim = gdn_cfg.conv_dim();
24984        let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
24985        p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
24986            in_proj_qkv: qt(c_dim, 8, 1),
24987            in_proj_z: qt(vd, 8, 2),
24988            in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
24989            in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
24990            conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
24991            a_log: vec![0.2, 0.5],
24992            dt_bias: synth(gdn_cfg.num_v_heads, 6),
24993            norm: vec![1.0; gdn_cfg.value_head_dim],
24994            out_proj: qt(8, vd, 7),
24995        });
24996        p.gdn_cfg = Some(gdn_cfg);
24997        p.set_o1(Some(crate::nystrom::O1Cfg {
24998            layers: crate::nystrom::O1Layers::All,
24999            m: 4,
25000            w: 8,
25001            sink: 2,
25002            rect: crate::nystrom::O1Rect::Aggregate,
25003        }));
25004        p.o1_begin_with_prefix(Some(B));
25005        for pos in 0..B - 2 {
25006            let emb = p.embed_single(pos as u32);
25007            let _ = p.forward_layers(&emb, pos, None);
25008        }
25009        let lane1_state = p.kv_cache.layers[0].linear_state.clone();
25010
25011        let e1 = p.embed_single((B - 2) as u32);
25012        let e2 = p.embed_single((B - 1) as u32);
25013        let _ = p.forward_pair(&e1, &e2, B - 2);
25014
25015        assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
25016        assert!(
25017            p.kv_cache
25018                .layers
25019                .iter()
25020                .enumerate()
25021                .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
25022        );
25023        assert!(!p.kv_cache.layers[0].linear_state.is_empty());
25024        assert_ne!(
25025            p.kv_cache.layers[0].linear_state, lane1_state,
25026            "real pair must commit GDN lane 2 before returning"
25027        );
25028        assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
25029        let next = p.embed_single(B as u32);
25030        let _ = p.forward_layers(&next, B, None);
25031        assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
25032    }
25033
25034    #[test]
25035    fn o1_error_observation_stays_terminal_until_reset() {
25036        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25037        p.set_o1(Some(crate::nystrom::O1Cfg {
25038            layers: crate::nystrom::O1Layers::All,
25039            m: 4,
25040            w: 8,
25041            sink: 2,
25042            rect: crate::nystrom::O1Rect::Aggregate,
25043        }));
25044        p.o1_begin();
25045        p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
25046
25047        assert!(p.o1_seal_checked().is_err());
25048        assert!(
25049            p.o1_seal_checked().is_err(),
25050            "retry must see the sticky error"
25051        );
25052        let k = vec![0.2f32; 4];
25053        let v = vec![0.3f32; 4];
25054        p.kv_cache.layers[0].append(&k, &v, &[]);
25055        assert_eq!(p.kv_cache.layers[0].seq_len, 0);
25056
25057        p.reset_session();
25058        p.o1_begin();
25059        p.kv_cache.layers[0].append(&k, &v, &[]);
25060        assert_eq!(p.kv_cache.layers[0].seq_len, 1);
25061    }
25062
25063    #[test]
25064    fn nll_graph_failure_is_terminal_and_request_is_reusable() {
25065        let ids = vec![1u32, 2, 3, 4, 5, 6];
25066        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25067        p.graph_logits = Some(vec![123.0]);
25068        p.graph_want_logits = true;
25069        p.graph_failed
25070            .store(true, std::sync::atomic::Ordering::Relaxed);
25071        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
25072        let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
25073        assert!(err.contains("before NLL"));
25074        assert!(p.graph_logits.is_none());
25075        assert!(!p.graph_want_logits);
25076        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25077        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25078
25079        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25080        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
25081        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
25082        assert_eq!(actual.1, expected.1);
25083        assert!((actual.0 - expected.0).abs() < 1e-9);
25084    }
25085
25086    #[test]
25087    fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
25088        let ids = vec![1u32, 2, 3, 4, 5, 6];
25089        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25090        p.nll_test_fail_at = Some(1);
25091        let err = p
25092            .nll_ids_from(&ids, 0)
25093            .expect_err("one-shot forward failure");
25094        assert!(err.contains("forward") || err.contains("score row"));
25095        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25096        assert!(!p.graph_want_logits);
25097        assert!(p.graph_logits.is_none());
25098        assert!(p.kv_history.is_empty());
25099
25100        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25101        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
25102        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
25103        assert_eq!(actual.1, expected.1);
25104        assert!((actual.0 - expected.0).abs() < 1e-9);
25105    }
25106
25107    #[test]
25108    fn nll_serial_failure_before_first_row_is_reported() {
25109        let ids = vec![1u32, 2, 3, 4];
25110        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25111        p.nll_test_force_serial = true;
25112        p.nll_test_fail_at = Some(0);
25113        let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
25114        assert!(err.contains("serial forward"));
25115        assert!(p.kv_history.is_empty());
25116        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25117        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25118    }
25119
25120    #[test]
25121    fn ffn_probe_failure_discards_recorder_and_state() {
25122        let ids = vec![1u32, 2, 3, 4];
25123        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25124        p.nll_test_fail_at = Some(0);
25125        let err = p
25126            .probe_ffn_mass_batch(&ids)
25127            .expect_err("probe forward failure");
25128        assert!(err.contains("NLL"));
25129        assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
25130        assert!(p.kv_history.is_empty());
25131        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25132    }
25133
25134    #[test]
25135    fn nll_test_controls_are_pipeline_scoped() {
25136        let ids = vec![1u32, 2, 3, 4];
25137        let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25138        let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25139        failing.nll_test_force_serial = true;
25140        failing.nll_test_fail_at = Some(0);
25141
25142        assert!(!failing.can_prefill_batched());
25143        assert!(unaffected.can_prefill_batched());
25144        let expected = unaffected
25145            .nll_ids_from(&ids, 0)
25146            .expect("unaffected pipeline remains usable");
25147        let err = failing
25148            .nll_ids_from(&ids, 0)
25149            .expect_err("failure injection belongs to failing pipeline");
25150        assert!(err.contains("serial forward"));
25151        assert!(failing.nll_test_fail_at.is_none());
25152        assert!(unaffected.can_prefill_batched());
25153        let actual = unaffected
25154            .nll_ids_from(&ids, 0)
25155            .expect("unaffected pipeline remains reusable");
25156        assert_eq!(actual.1, expected.1);
25157        assert!((actual.0 - expected.0).abs() < 1e-9);
25158    }
25159
25160    #[test]
25161    fn forward_ids_failure_channel_is_terminal_and_reusable() {
25162        let ids = vec![1u32, 2, 3, 4, 5, 6];
25163        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25164        p.graph_logits = Some(vec![123.0]);
25165        p.graph_want_logits = true;
25166        p.graph_failed
25167            .store(true, std::sync::atomic::Ordering::Relaxed);
25168        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
25169
25170        let err = p
25171            .forward_ids(&ids, None)
25172            .expect_err("a failed forward must not become a valid head result");
25173        assert!(err.contains("forward_ids setup"));
25174        assert!(p.graph_logits.is_none());
25175        assert!(!p.graph_want_logits);
25176        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25177        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25178        assert_eq!(p.kv_cache.seq_len(), 0);
25179
25180        let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
25181            .forward_ids(&ids, None)
25182            .expect("fresh forward_ids");
25183        let actual = p
25184            .forward_ids(&ids, None)
25185            .expect("pipeline remains reusable after a failed forward");
25186        assert_eq!(actual.len(), expected.len());
25187        assert!(
25188            actual
25189                .iter()
25190                .zip(expected)
25191                .all(|(a, b)| (a - b).abs() < 1e-9)
25192        );
25193        assert_eq!(p.kv_cache.seq_len(), ids.len());
25194    }
25195
25196    #[test]
25197    fn sigmoid_router_floor_is_explicit_per_architecture() {
25198        // Flint and MiniMax M3.1 reference runtimes use +1e-20, while the
25199        // generic LFM2-compatible path uses +1e-6. At low (but representable)
25200        // sigmoid scores, silently sharing the latter changes expert weights by
25201        // orders of magnitude and can make a routed layer look coherent while
25202        // discarding its expert contribution.
25203        let zero = || DenseFfn {
25204            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25205            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25206            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25207            act: Act::Silu,
25208            down_t: None,
25209            segs: Vec::new(),
25210        };
25211        let m = MoeFfn {
25212            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25213            experts: vec![zero(), zero()],
25214            top_k: 1,
25215            norm_topk_prob: true,
25216            router_sigmoid: true,
25217            sigmoid_denom_eps: 1e-20,
25218            expert_bias: None,
25219            routed_scaling: 2.5,
25220            route_tau: None,
25221            shared: None,
25222            stats: std::cell::RefCell::new(Vec::new()),
25223            act_sq: std::cell::RefCell::new(Vec::new()),
25224            act_rows: std::cell::RefCell::new(Vec::new()),
25225            mask: None,
25226            per_expert_scale: None,
25227            router_input_norm: false,
25228            resonance: None,
25229            grown: Vec::new(),
25230        };
25231        let logits = [-20.0f32, -20.0];
25232        let (_, p, flint_wsum) = moe_route(&logits, &m, None);
25233        let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25234        let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
25235        let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
25236        assert!((flint_wsum - reference_expected).abs() < 1e-15);
25237        assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
25238        assert!(legacy_wsum > minimax_wsum * 100.0);
25239    }
25240
25241    #[test]
25242    fn resonance_scores_match_formula_and_stable_tie() {
25243        let r = Resonance {
25244            // Three descriptors, hidden=2, one projection row each.
25245            mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
25246            u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
25247            k: 1,
25248            bias: vec![1.5, 0.5, 0.0],
25249            shell: Vec::new(),
25250        };
25251        let x = [1.0f32, 1.0];
25252        let mut got = vec![0.0; 3];
25253        r.scores(&x, &mut got);
25254        // Expert 0 and 1 are an exact score tie; the CPU top-1 contract uses
25255        // the lower index.  The values also check d² - (U·d)², not just tie
25256        // ordering.
25257        assert!((got[0] - 0.5).abs() < 1e-6);
25258        assert!((got[1] - 0.5).abs() < 1e-6);
25259        assert!(got[2].abs() < 1e-6);
25260        let best = got
25261            .iter()
25262            .enumerate()
25263            .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
25264            .map(|(i, _)| i);
25265        assert_eq!(best, Some(0));
25266        assert!(got.iter().all(|v| v.is_finite()));
25267    }
25268
25269    /// The growth shell (spec §2): a grown expert whose reconstruction
25270    /// error lies outside its shell scores −∞, one inside keeps the exact
25271    /// resonance score, trunk rows (`+inf` shell) are bit-identical to the
25272    /// shell-less computation; the process-wide switch disables it.
25273    #[test]
25274    fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
25275        // hidden = 2, rank 1. Experts 0/1 = trunk (shell +inf); 2 and 3 =
25276        // grown, the same descriptor (μ = (0, 1), u = (1, 1)) with shells
25277        // 6.0 and 0.25. At x' = (3, 0): d = (3, −1), d² = 10, proj =
25278        // (3 − 1)² = 4, err = 6 exactly — on the boundary of expert 2's
25279        // shell (kept: the rule is strict `>`), outside expert 3's.
25280        let plain = Resonance {
25281            mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
25282            u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
25283            k: 1,
25284            bias: vec![1.5, 0.5, 0.0, 0.0],
25285            shell: Vec::new(),
25286        };
25287        let shelled = Resonance {
25288            mu: plain.mu.clone(),
25289            u: plain.u.clone(),
25290            k: 1,
25291            bias: plain.bias.clone(),
25292            shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
25293        };
25294        assert!(!plain.has_shell());
25295        assert!(shelled.has_shell());
25296        let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
25297        let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
25298        set_growth_shell(Some(true));
25299        assert!(growth_shell_enabled());
25300        // x = (1, 1): the grown experts reconstruct it exactly (err 0):
25301        // inside both shells, every row the shell-less bits.
25302        let x = [1.0f32, 1.0];
25303        plain.scores(&x, &mut a);
25304        shelled.scores(&x, &mut b);
25305        assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
25306        assert!(a[2] == 0.0 && a[3] == 0.0);
25307        // x' = (3, 0): expert 3 → −∞, expert 2 (err == shell) and the
25308        // trunk rows keep their exact bits.
25309        let xo = [3.0f32, 0.0];
25310        plain.scores(&xo, &mut a);
25311        shelled.scores(&xo, &mut b);
25312        assert_eq!(a[2], -6.0);
25313        assert_eq!(a[3], -6.0);
25314        assert_eq!(
25315            bits(&a[..3]),
25316            bits(&b[..3]),
25317            "trunk rows + the boundary row"
25318        );
25319        assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
25320        assert_eq!(shelled.effective_shell(4), shelled.shell);
25321        // The switch (`CMF_GROWTH_SHELL=off` / `growth-eval --shell off`):
25322        // all +inf, the shell-less bits everywhere.
25323        set_growth_shell(Some(false));
25324        assert!(!growth_shell_enabled());
25325        assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
25326        shelled.scores(&xo, &mut b);
25327        assert_eq!(bits(&a), bits(&b));
25328        set_growth_shell(None);
25329        // A shell vector shorter than the expert count masks nothing
25330        // beyond it (a legacy layer whose tail has no shell).
25331        let short = Resonance {
25332            shell: vec![f32::INFINITY, f32::INFINITY],
25333            ..shelled
25334        };
25335        set_growth_shell(Some(true));
25336        short.scores(&xo, &mut b);
25337        assert_eq!(bits(&a), bits(&b));
25338        set_growth_shell(None);
25339    }
25340
25341    /// `moe_route` with −∞ logits (a grown expert outside its shell):
25342    /// top-1 is the best finite expert with weight exactly 1.0 on both
25343    /// the softmax and the sigmoid path; all −∞ degrades to uniform.
25344    #[test]
25345    fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
25346        let zero = || DenseFfn {
25347            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25348            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25349            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25350            act: Act::Silu,
25351            down_t: None,
25352            segs: Vec::new(),
25353        };
25354        let moe = |sigmoid: bool| MoeFfn {
25355            router: QTensor::from_f32(vec![0.0; 8], 4, 2),
25356            experts: vec![zero(), zero(), zero(), zero()],
25357            top_k: 1,
25358            norm_topk_prob: true,
25359            router_sigmoid: sigmoid,
25360            sigmoid_denom_eps: 1e-6,
25361            expert_bias: None,
25362            routed_scaling: 1.0,
25363            route_tau: None,
25364            shared: None,
25365            stats: std::cell::RefCell::new(Vec::new()),
25366            act_sq: std::cell::RefCell::new(Vec::new()),
25367            act_rows: std::cell::RefCell::new(Vec::new()),
25368            mask: None,
25369            per_expert_scale: None,
25370            router_input_norm: false,
25371            resonance: None,
25372            grown: Vec::new(),
25373        };
25374        let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
25375        for sigmoid in [false, true] {
25376            let m = moe(sigmoid);
25377            let (idx, p, wsum) = moe_route(&logits, &m, None);
25378            assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
25379            assert_eq!(p[1], 0.0);
25380            assert_eq!(p[3], 0.0);
25381            assert!(p[2] > p[0] && p[0] > 0.0);
25382            assert!(p.iter().all(|v| v.is_finite()));
25383            let w = p[2] / wsum;
25384            if sigmoid {
25385                // The sigmoid renorm keeps its reference floor (+1e-6).
25386                assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
25387            } else {
25388                assert_eq!(
25389                    w, 1.0,
25390                    "softmax: the renormalized top-1 weight is exactly 1"
25391                );
25392            }
25393            // Masked experts stay masked even when they are the only ones
25394            // "admitted" by an allow-list that covers everything.
25395            let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
25396            assert_eq!(idx, vec![2]);
25397        }
25398        // A finite expert always beats −∞ whatever the bias / order.
25399        let m = moe(false);
25400        let (idx, _, _) = moe_route(
25401            &[
25402                f32::NEG_INFINITY,
25403                f32::NEG_INFINITY,
25404                f32::NEG_INFINITY,
25405                -9.0,
25406            ],
25407            &m,
25408            None,
25409        );
25410        assert_eq!(idx, vec![3]);
25411        // Every expert at −∞ (cannot happen on a grown file — trunk rows
25412        // have no shell): uniform, finite, lowest index.
25413        let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
25414        assert_eq!(idx, vec![0]);
25415        assert!(p.iter().all(|&v| v == 0.25));
25416        assert!(wsum.is_finite() && wsum > 0.0);
25417    }
25418
25419    /// MiniMax-M3.1's DEVICE route (the pooled token-graph arm's exact
25420    /// flags: sigmoid scoring via n's high bit, +1e-20 floor,
25421    /// routed_scaling 2.0, selection bias on the CHOICE only) must match
25422    /// the host reference in experts, ORDER and weights — the check that
25423    /// caught the flags/slot collision.
25424    #[cfg(feature = "gpu")]
25425    #[test]
25426    fn minimax_device_route_matches_host_sigmoid_contract() {
25427        match crate::gpu_wgpu::selected_and_up() {
25428            None => {
25429                crate::gpu_wgpu::skip_or_fail(module_path!());
25430                return;
25431            }
25432            // Sibling unit tests in THIS process select wgpu by writing
25433            // `CMF_GPU` (the gpu_wgpu test module does it ~70 times), so on a
25434            // headless runner the variable is already set by the time this
25435            // test reads it — "requested" here is test pollution, not an
25436            // operator's ask, and the adapter genuinely does not exist in
25437            // this process. Same treatment as the gpu_wgpu tests themselves:
25438            // a visible skip, never a silent pass.
25439            Some(false) => {
25440                eprintln!(
25441                    "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25442                    module_path!()
25443                );
25444                return;
25445            }
25446            Some(true) => {}
25447        }
25448        let n = 128usize;
25449        let top_k = 4usize;
25450        let logits: Vec<f32> = (0..n)
25451            .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25452            .collect();
25453        let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25454        let m = MoeFfn {
25455            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25456            experts: Vec::new(),
25457            top_k,
25458            norm_topk_prob: true,
25459            router_sigmoid: true,
25460            sigmoid_denom_eps: 1e-20,
25461            expert_bias: Some(bias.clone()),
25462            routed_scaling: 2.0,
25463            route_tau: None,
25464            shared: None,
25465            stats: std::cell::RefCell::new(Vec::new()),
25466            act_sq: std::cell::RefCell::new(Vec::new()),
25467            act_rows: std::cell::RefCell::new(Vec::new()),
25468            mask: None,
25469            per_expert_scale: None,
25470            router_input_norm: false,
25471            resonance: None,
25472            grown: Vec::new(),
25473        };
25474        let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25475        let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
25476
25477        for shared in [Some(n as u32), None] {
25478            let mut gi = Vec::new();
25479            let mut gw = Vec::new();
25480            assert!(
25481                crate::gpu_wgpu::minimax_moe_route_for_test(
25482                    &logits,
25483                    Some(&bias),
25484                    top_k,
25485                    2.0,
25486                    shared,
25487                    &mut gi,
25488                    &mut gw,
25489                ),
25490                "the device did not come up — the test checked nothing"
25491            );
25492            let routed = if shared.is_some() { top_k } else { gi.len() };
25493            assert_eq!(
25494                &gi[..routed],
25495                &ci[..],
25496                "different experts or order (shared={shared:?})"
25497            );
25498            let d = cw
25499                .iter()
25500                .zip(&gw[..routed])
25501                .map(|(a, b)| (a - b).abs())
25502                .fold(0.0f32, f32::max);
25503            println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
25504            assert!(d < 1e-6, "the weights diverged by {d:.3e}");
25505            if let Some(s) = shared {
25506                assert_eq!(gi[top_k], s as usize, "shared pin slot");
25507                assert_eq!(gw[top_k], 1.0, "shared weight");
25508            }
25509        }
25510    }
25511
25512    /// The pooled arm's COLD hand-back: `(expert, weight)` pairs for the
25513    /// picks the bank could not serve must carry the host's EXACT final
25514    /// weights — the device ships its resident sum, the host adds this on
25515    /// top of the same residual, so a weight drift compounds per layer.
25516    #[cfg(feature = "gpu")]
25517    #[test]
25518    fn minimax_device_cold_weights_match_host_contract() {
25519        match crate::gpu_wgpu::selected_and_up() {
25520            None => {
25521                crate::gpu_wgpu::skip_or_fail(module_path!());
25522                return;
25523            }
25524            // Sibling unit tests in THIS process select wgpu by writing
25525            // `CMF_GPU` (the gpu_wgpu test module does it ~70 times), so on a
25526            // headless runner the variable is already set by the time this
25527            // test reads it — "requested" here is test pollution, not an
25528            // operator's ask, and the adapter genuinely does not exist in
25529            // this process. Same treatment as the gpu_wgpu tests themselves:
25530            // a visible skip, never a silent pass.
25531            Some(false) => {
25532                eprintln!(
25533                    "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25534                    module_path!()
25535                );
25536                return;
25537            }
25538            Some(true) => {}
25539        }
25540        let n = 128usize;
25541        let top_k = 4usize;
25542        let logits: Vec<f32> = (0..n)
25543            .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25544            .collect();
25545        let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25546        let m = MoeFfn {
25547            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25548            experts: Vec::new(),
25549            top_k,
25550            norm_topk_prob: true,
25551            router_sigmoid: true,
25552            sigmoid_denom_eps: 1e-20,
25553            expert_bias: Some(bias.clone()),
25554            routed_scaling: 2.0,
25555            route_tau: None,
25556            shared: None,
25557            stats: std::cell::RefCell::new(Vec::new()),
25558            act_sq: std::cell::RefCell::new(Vec::new()),
25559            act_rows: std::cell::RefCell::new(Vec::new()),
25560            mask: None,
25561            per_expert_scale: None,
25562            router_input_norm: false,
25563            resonance: None,
25564            grown: Vec::new(),
25565        };
25566        let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25567        assert_eq!(ci.len(), top_k);
25568
25569        // Every pick resident → no cold pairs at all.
25570        let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
25571        let mut cold = Vec::new();
25572        let mut idx = Vec::new();
25573        let mut dw = Vec::new();
25574        assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25575            &logits,
25576            Some(&bias),
25577            top_k,
25578            2.0,
25579            &resident,
25580            n as u32,
25581            &mut cold,
25582            &mut idx,
25583            &mut dw,
25584        ));
25585        assert!(cold.is_empty(), "fully resident bank: {cold:?}");
25586        // With subset+pin together: routed slots carry the FLAT remap value
25587        // (here identity = the expert id), weights the host contract, and
25588        // the pinned shared keeps weight 1 — this exact combination is what
25589        // the pooled arm runs with.
25590        for (r, &e) in ci.iter().enumerate() {
25591            assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
25592            let host = p[e] / wsum;
25593            let d = (dw[r] - host).abs();
25594            assert!(
25595                d <= 1e-6 * host.abs().max(1.0),
25596                "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
25597                dw[r]
25598            );
25599        }
25600        assert_eq!(idx[top_k] as usize, n, "shared pin slot");
25601        assert_eq!(dw[top_k], 1.0, "shared weight");
25602
25603        // Make exactly two of the CHOSEN picks cold.
25604        let mut remap = resident.clone();
25605        remap[ci[0]] = u32::MAX;
25606        remap[ci[2]] = u32::MAX;
25607        let mut cold = Vec::new();
25608        let mut idx2 = Vec::new();
25609        let mut dw2 = Vec::new();
25610        assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25611            &logits,
25612            Some(&bias),
25613            top_k,
25614            2.0,
25615            &remap,
25616            n as u32,
25617            &mut cold,
25618            &mut idx2,
25619            &mut dw2,
25620        ));
25621        // The resident picks of the partial bank still carry exact weights.
25622        // A COLD rank is a placeholder the device writes as slot 0 / weight
25623        // 0.0 — its real (id, weight) pair rides in `cold` — so weight zero
25624        // is what marks it, not the slot value: reading the placeholder as
25625        // a resident expert compared 0.0 against p[0]/wsum and failed with
25626        // Δ=2.552e-1. The test logits keep every sigmoid score strictly
25627        // positive, so a resident pick can never weigh exactly zero.
25628        let mut resident_weight_checked = 0usize;
25629        for (r, &slot) in idx2.iter().enumerate().take(top_k) {
25630            if dw2[r] == 0.0 {
25631                continue; // cold rank: placeholder, not an expert id
25632            }
25633            let e = slot as usize;
25634            assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
25635            let host = p[e] / wsum;
25636            let d = (dw2[r] - host).abs();
25637            assert!(
25638                d <= 1e-6 * host.abs().max(1.0),
25639                "partial-bank resident weight expert {e}: Δ={d:.3e}"
25640            );
25641            resident_weight_checked += 1;
25642        }
25643        assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
25644        println!("partial bank: checked {resident_weight_checked} resident weights");
25645        assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
25646        let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
25647        ids.sort_unstable();
25648        let mut want = vec![ci[0], ci[2]];
25649        want.sort_unstable();
25650        assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
25651        for &(e, w) in &cold {
25652            let host = p[e] / wsum;
25653            let d = (w - host).abs();
25654            println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
25655            assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
25656        }
25657    }
25658
25659    /// The resonance router (top-1) selects by the raw score as the
25660    /// trainer and the graph do — not by softmax probabilities, where two
25661    /// scores closer than 2^-25 collapse to the same `exp(l − max) = 1.0`
25662    /// and the LOWER index wins a token whose score is strictly smaller.
25663    #[test]
25664    fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
25665        let zero = || DenseFfn {
25666            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25667            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25668            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25669            act: Act::Silu,
25670            down_t: None,
25671            segs: Vec::new(),
25672        };
25673        let moe = |resonant: bool, norm_topk: bool| MoeFfn {
25674            router: QTensor::from_f32(vec![0.0; 6], 3, 2),
25675            experts: vec![zero(), zero(), zero()],
25676            top_k: 1,
25677            norm_topk_prob: norm_topk,
25678            router_sigmoid: false,
25679            sigmoid_denom_eps: 1e-6,
25680            expert_bias: None,
25681            routed_scaling: 1.0,
25682            route_tau: None,
25683            shared: None,
25684            stats: std::cell::RefCell::new(Vec::new()),
25685            act_sq: std::cell::RefCell::new(Vec::new()),
25686            act_rows: std::cell::RefCell::new(Vec::new()),
25687            mask: None,
25688            per_expert_scale: None,
25689            router_input_norm: false,
25690            resonance: resonant.then(|| Resonance {
25691                mu: vec![0.0; 6],
25692                u: Vec::new(),
25693                k: 0,
25694                bias: vec![0.0; 3],
25695                shell: Vec::new(),
25696            }),
25697            grown: Vec::new(),
25698        };
25699        // lo = −0.1, hi = the next f32 towards zero: hi − lo = 2^-27 <
25700        // 2^-25, so exp(lo − hi) rounds to exactly 1.0 — a softmax tie.
25701        let lo = -0.1f32;
25702        let hi = f32::from_bits(lo.to_bits() - 1);
25703        assert!(hi > lo && hi - lo < 2f32.powi(-25));
25704        assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
25705        // The gated MoE (softmax) path: the tie hands the token to index 0.
25706        let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
25707        assert_eq!(
25708            idx,
25709            vec![0],
25710            "softmax tie → lower index (the gated contract)"
25711        );
25712        // The resonance path: the strictly larger raw score wins, weight
25713        // exactly 1.0 with and without norm_topk.
25714        for norm in [true, false] {
25715            let m = moe(true, norm);
25716            let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
25717            assert_eq!(idx, vec![1], "norm_topk {norm}");
25718            assert_eq!(p, vec![0.0, 1.0, 0.0]);
25719            assert_eq!(p[1] / wsum, 1.0);
25720            // An exact tie: the first maximum (as `resonance_winner` and
25721            // `embryo_core_route_pick`).
25722            let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
25723            assert_eq!(idx, vec![0]);
25724            // `−∞` never wins; the admitted set is honoured.
25725            let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
25726            assert_eq!(idx, vec![2]);
25727            let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
25728            assert_eq!(idx, vec![0]);
25729            assert_eq!(p[0] / wsum, 1.0);
25730            // Every admitted expert at −∞: the generic path's uniform
25731            // fallback (lowest index, finite weights).
25732            let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
25733            assert_eq!(idx, vec![0]);
25734            assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
25735        }
25736    }
25737}