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    pub vocab_size: usize,
145    pub rms_eps: f64,
146    pub rope_base: f32,
147    pub norm_style: NormStyle,
148    /// RoPE dims actually rotated (≤ head_dim; Qwen3.5 uses head_dim/4).
149    pub rotary_dim: usize,
150    /// Optional Q-head count override for each attention layer (Laguna).
151    pub attention_heads_per_layer: Option<Vec<usize>>,
152    /// Optional KV-head count of each PHYSICAL attention layer (MiMo-V2:
153    /// 4 on full-attention layers, 8 on sliding ones). Set only through
154    /// [`Pipeline::set_attn_geometry`], which also reshapes the layer
155    /// caches. None = every layer has `num_kv_heads`.
156    pub kv_heads_per_layer: Option<Vec<usize>>,
157    /// Width of each V head when it is narrower than `head_dim` (MiMo-V2:
158    /// 128 against 192). V is zero-padded to `head_dim` inside the cache
159    /// and the attention output is compacted back to nh·v_head_dim before
160    /// o_proj (see `QwenAttnCfg::v_head_dim`). None = `head_dim`.
161    pub v_head_dim: Option<usize>,
162    /// `CMF_LAYER_DUMP=<dir>` (read once at construction; tests set it
163    /// directly): the hidden state after every layer, for every position,
164    /// as raw little-endian f32 files `p{pos:06}_l{li:02}.f32` of
165    /// `hidden_size` floats each. Written by the CPU layer walks — the
166    /// batched prefill (`prefill_batch_span`) and the single-token forward
167    /// (`forward_layers_span`) — so both prompt ingest and decode can be
168    /// diffed layer by layer against an external oracle (tools/mimo_ref).
169    /// Layers a device graph runs (wgpu token/batch graph, Metal chunk or
170    /// block graphs) and the own-stack families (DeepSeek-V4/V4.1,
171    /// Qwen3.8-Flash-Next, Gemma-3n) are not dumped: run with CMF_GPU=0
172    /// for a complete set. `li` is the virtual layer index. Final logits:
173    /// `CMF_LOGIT_DUMP=<file>` (hidden + logits of the first decode step).
174    pub layer_dump: Option<std::path::PathBuf>,
175    /// GPU-graph declines already logged for this pipeline, as (graph
176    /// site, reason) — one line each, see `graph_attn_decline_reason`.
177    graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
178    /// MiMo-V2 expert placement (prefix / dynamic bank / hybrid), decided
179    /// on the first forward — see `crate::mimo_moe`.
180    pub(crate) mimo_moe: crate::mimo_moe::Slot,
181    /// Opt-in MiniCPM q4tp expert cache for a graph-produced CPU tail.
182    /// It is deliberately separate from MiMo's placement policy because it
183    /// serves only the stock MiniCPM top-8 geometry and otherwise falls back
184    /// to the ordinary exact host MoE path.
185    pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
186    /// Linear-core geometry (present when the model has linear layers).
187    pub vmf_cfg: Option<VmfPhaseCfg>,
188    /// GatedDeltaNet geometry (faithful vendor operator).
189    pub gdn_cfg: Option<GdnCfg>,
190    /// MiniCPM-class logit scale (tied lm_head → cannot fold into weights).
191    pub logit_multiplier: Option<f32>,
192    /// Cooperative cancel: set from any thread (FFI `cortiq_cancel`,
193    /// a dropped server connection); the generate loop checks it at
194    /// every prefill chunk and decode step and finishes with
195    /// `finish_reason: "cancelled"`. Auto-cleared when honoured.
196    pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
197    /// A GPU graph failure is distinct from a user/request cancellation.
198    /// Graph code sets this before raising the cooperative cancel flag so the
199    /// generation API can return an error instead of reporting a successful
200    /// `finish_reason: cancelled` result.
201    graph_failed: std::sync::atomic::AtomicBool,
202    /// Resident-graph MoE expert banks, built once per layer
203    /// (`GraphMoeBank`). `CMF_GRAPH_MOE_BANK=0` rebuilds them every token.
204    graph_moe_banks: std::sync::Mutex<std::collections::HashMap<GraphMoeKey, GraphMoeBank>>,
205    /// Token ids currently materialized in the KV cache (the forwarded
206    /// prompt + all generated tokens except the last, which is sampled
207    /// but not yet forwarded). Lets the next generate call prefill only
208    /// the suffix when a chat app resends the whole history.
209    pub kv_history: Vec<u32>,
210    /// Owner tag of `kv_history`: the resident device graph held that
211    /// sequence (true) or the host did. Reuse continues only on the owner.
212    pub kv_history_device: bool,
213    /// KDA geometry (Kimi Linear / Kimi-K3) — shared by every Kda layer.
214    pub kda_cfg: Option<crate::linear_core::KdaCfg>,
215    /// Gemma-3n stack (AltUp/LAuReL/PLE/KV-sharing): its own forward —
216    /// weights.layers stays empty, the KV caches are the shared ones.
217    pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
218    /// Gemma-4 E-series stack (dual-geometry gemma4 + PLE + shared KV):
219    /// its own per-token forward, same contract as `g3n`.
220    pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
221    /// DeepSeek-V4 runs its own stack too: its hidden state is `hc_mult`
222    /// copies of a vector, so no loop written for a single residual
223    /// stream can carry it.
224    pub dsv4: Option<
225        Box<(
226            crate::dsv4::Dsv4Globals,
227            Vec<crate::dsv4::Dsv4Layer>,
228            crate::dsv4::Dsv4Cfg,
229            crate::dsv4::Dsv4State,
230        )>,
231    >,
232    /// DeepSeek-V4.1 owns the shared CED/CSA2 attention state, raw Engram
233    /// lookup and four-stream mHC handoff. It cannot use the V4 cache
234    /// layout, so it has a dedicated executor and state tuple.
235    pub dsv41: Option<
236        Box<(
237            crate::dsv41::Dsv41Globals,
238            Vec<crate::dsv41::Dsv41Layer>,
239            crate::dsv41::Dsv41Cfg,
240            crate::dsv41::Dsv41State,
241        )>,
242    >,
243    /// Optional V4.1 vision tower. Text-only files leave this unset.
244    pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
245    /// Prepared image rows consumed by the next V4.1 prefill.
246    dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
247    /// Qwen3.8-Flash-Next owns four residual streams plus QSA/PLE state;
248    /// the generic single-residual layer loop cannot represent it.
249    /// Block-diffusion generation (Clover): preview weights and the
250    /// decoding controls; `None` on every causal model.
251    pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
252    clover_stats: clover::CloverStats,
253    pub qwen4_exp: Option<
254        Box<(
255            crate::qwen4_exp::Globals,
256            Vec<crate::qwen4_exp::Layer>,
257            crate::qwen4_exp::Cfg,
258            crate::qwen4_exp::State,
259        )>,
260    >,
261    /// DeepSeek-V4's own speculation stack: three draft modules, each a full
262    /// layer, plus a confidence head on the last. Empty when the file has
263    /// none, which is the only signal the decode path needs.
264    pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
265    /// The draft's per-sequence state (KV rings, captured trunk hidden).
266    pub dspark: Option<crate::dsv4::DsparkState>,
267    /// Drafts awaiting their verdict: (position, proposals, still matching,
268    /// accepted so far).
269    pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
270    /// Accepted prefix length of every graded draft.
271    pub dspark_hist: Vec<usize>,
272    /// The real tokens the drafts were graded against — a degenerate,
273    /// repeating output would make any acceptance number meaningless, and
274    /// the cheapest guard against believing one is to count them.
275    pub dspark_real: Vec<u32>,
276    /// The trunk's expert picks for the last few tokens, per layer. The
277    /// union over a window of them is what a batched verify would have to
278    /// read, and the ratio to the pick count is all it could save.
279    pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
280    /// (unique, total) expert picks per draft, trunk side and draft side.
281    pub dspark_exp: Vec<(usize, usize, usize, usize)>,
282    /// Wall time spent in the deliberately out-of-core draft. Kept separate
283    /// from trunk decode so block batching can be judged without conflating
284    /// it with GPU chain variance.
285    pub dspark_draft_ns: u128,
286    /// Per-layer winners of the last pooled route — the admission predictor.
287    m3_winners: Vec<Vec<u32>>,
288    /// Route frequency per (layer, expert) — the bank's prewarm profile.
289    m3_counts: Vec<u32>,
290    m3_n_exp: usize,
291    m3_tokens: u64,
292    m3_prewarmed: bool,
293    /// LFM2 short-convolution geometry (present when the model has
294    /// `ShortConv` mixer layers).
295    pub short_conv_cfg: Option<ShortConvCfg>,
296    /// Multi-token-prediction head (None = absent).
297    pub mtp: Option<MtpModule>,
298    /// MiMo-V2's draft stack (three chained MTP layers from the
299    /// `<stem>.mtp.cmf` sidecar); None = absent. Speculative greedy decode
300    /// uses it unless `CMF_MTP=0` / `CMF_MIMO_MTP=0`.
301    pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
302    /// Set while the MiMo speculative verify runs `prefill_batch`: its MoE
303    /// layers take `moe_ffn_rows_exact` (each row bit-identical to decode).
304    verify_exact_moe: bool,
305    /// Speculative decode via MTP (greedy only; `CMF_MTP=0` disables).
306    pub speculative: bool,
307    /// Keep generating past end-of-sequence ids (the llama-bench contract
308    /// for a timed run). A loop flag, deliberately NOT a sampler
309    /// suppression: suppressed ids count as a penalty and switch the
310    /// speculative round and the greedy burst off, so a benchmark that
311    /// suppressed EOS never measured either.
312    pub ignore_eos: bool,
313    /// Draft-head shortlist guard: tokens left during which the draft
314    /// uses the FULL head because a recently committed id lay past the
315    /// `CMF_DRAFT_VOCAB` cut (Cyrillic and CJK ids sit above 131072 in
316    /// Qwen's table, so a prefix shortlist would draft nothing usable
317    /// there — measured on Russian prose: 2.9 → 1.6 accepted a round).
318    pub draft_full_streak: u32,
319    /// Adaptive draft depth for the speculative round (None until the
320    /// first round): grows while nearly every draft is accepted, shrinks
321    /// when fewer than half are. The verify's cost climbs with the rows on
322    /// a discrete card (RTX PRO 4000: 52 ms at 2 rows, 74 at 5, 80 at 6),
323    /// so prose wants k≈3 and code or the repetitive bench k≈5 — measured
324    /// 33.6 vs 27.6 tok/s on an essay at k=3 vs 5, 45.6 vs 38 on code.
325    /// `CMF_GRAPH_SPEC_K` pins it.
326    pub spec_k_adapt: Option<usize>,
327    /// EWMA of the accepted fraction that drives `spec_k_adapt`.
328    pub spec_acc_ewma: f32,
329    rng: SplitMix64,
330    sampler_scratch: SamplerScratch,
331    /// Speculative SAMPLING state (graph_spec_step, temperature > 0): the
332    /// correction token a rejected draft produced — committed by the loop
333    /// top in place of a fresh draw — and the per-round draft
334    /// distributions / target scratch, reused so a round allocates
335    /// nothing at the vocab size.
336    spec_forced: Option<u32>,
337    spec_q: Vec<Vec<f32>>,
338    spec_p: Vec<f32>,
339    spec_res: Vec<f32>,
340    /// The same three for the sparse chain (top-k configs).
341    spec_qs: Vec<sampler::Sparse>,
342    spec_ps: sampler::Sparse,
343    spec_ress: sampler::Sparse,
344    /// Which arm the MTP draft block runs on this generation: Some(true)
345    /// = the whole-token graph (device attention, one submit a step),
346    /// Some(false) = the per-op path; None = not decided yet. Decided
347    /// on the first draft and held, because the two arms keep the MTP
348    /// KV in different places (device mirror vs the CPU cache) and a
349    /// mid-run switch would read the wrong one.
350    mtp_graph_mode: Option<bool>,
351    /// The Metal verify graph of the round in flight, between its sync
352    /// (logits read) and the commit that replays the accepted prefix.
353    #[cfg(target_os = "macos")]
354    metal_verify: Option<MetalVerifyPending>,
355    /// Precomputed RoPE inverse frequencies [head_dim/2]. Arc: the
356    /// forward path clones a handle to escape the &mut self borrow —
357    /// cloning the table itself was a per-forward allocation.
358    pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
359    /// Reusable norm buffers for the decode hot path (roadmap §3 P0:
360    /// steady-state forward should not heap-allocate). Disjoint field
361    /// from `weights`/`kv_cache`, so split borrows keep working.
362    ws: ForwardScratch,
363    /// Persistent worker pool (None = serial; see CMF_THREADS).
364    pool: Option<std::sync::Arc<Pool>>,
365    // ── Dynamic per-token skill routing (spec §9, claim 14/16) ──
366    /// Source model, retained so a skill switch can re-resolve the
367    /// touched layers' FFN tensors (Mapped = mmap pointers, cheap).
368    pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
369    /// Masks present → weights are dequantized f32 (rebuild path).
370    pub(crate) dyn_force_f32: bool,
371    /// Per-skill FFN layers actually replaced (derived from tensors, not
372    /// the meta `layers` field — ru2 replaces down_proj in 0..23 while
373    /// its meta says [20..23]). None = skill touches non-FFN tensors →
374    /// ineligible for cheap dynamic switching (honest refusal).
375    pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
376    /// Currently overlaid skill (index into model.header.skills); None =
377    /// backbone. Set at load time to the statically-overlaid skill so
378    /// `set_active_skill(None)` correctly reverts it (else a static
379    /// skill would silently persist — the union-diff assumes dyn_active
380    /// always mirrors the live overlay). Switched by `set_active_skill`.
381    pub(crate) dyn_active: Option<usize>,
382    /// Pipeline was loaded with a soft blend (materialized working
383    /// tensors, not a single skill index) → dynamic routing refuses:
384    /// there is no single index to revert the blend from.
385    pub(crate) dyn_blend_loaded: bool,
386    /// Layer whose post-residual hidden feeds the router φ (shared by
387    /// swarm skills). None = φ capture off.
388    pub(crate) dyn_phi_layer: Option<usize>,
389    /// EMA of φ at `dyn_phi_layer` over the decode window (on-policy).
390    dyn_phi_ema: Vec<f32>,
391    dyn_phi_seen: usize,
392    /// Hysteresis router driving per-token skill switches during decode
393    /// (None = static/no dynamic routing). Taken out during generation.
394    pub dyn_router: Option<crate::swarm::DynRouter>,
395    /// O(1) Nyström attention setting (CLI/env/header-hint resolved by
396    /// the caller; None = plain cache attention everywhere).
397    o1_cfg: Option<crate::nystrom::O1Cfg>,
398    /// Bumped once per collecting→sealed transition — the GPU state mirror
399    /// re-uploads when it sees a new epoch (each fresh sealed state).
400    o1_epoch: u64,
401    /// Per-layer o1 flags derived from `o1_cfg` (Full layers only).
402    o1_flags: Vec<bool>,
403    /// Emit a structured per-token trace (B4 telemetry channel). Off by
404    /// default — the runtime is silent unless observation is requested.
405    trace: bool,
406    /// Confidence-calibration temperature (B1): reported probability is
407    /// softmax(logits / calib_temp). 1.0 = raw. Set from header.calibration.
408    calib_temp: f32,
409    /// Process-unique id keying this pipeline's device KV mirrors.
410    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
411    graph_kv_id: u64,
412    /// Decode asks the token graph to also run final-norm + lm_head on
413    /// the device (drops the separate per-op lm_head round trip).
414    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
415    graph_want_logits: bool,
416    /// NLL quality gates require the graph's fused head rather than silently
417    /// accepting a CPU head fallback. Generation keeps the historical
418    /// best-effort `graph_want_logits` behavior.
419    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
420    graph_head_required: bool,
421    /// Logits the graph produced for the token just forwarded (taken by
422    /// the decode loop; None = compute on the CPU path).
423    graph_logits: Option<Vec<f32>>,
424    /// Packed Embryo graph model, built lazily on the first eligible
425    /// resident call so other architectures pay no packing cost.
426    embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
427    /// This pipeline's token graph refused for a STRUCTURAL reason (its
428    /// own weights / layer kinds): not retried. Per pipeline, never
429    /// process-wide: a skill lane whose pack does not build must not flip
430    /// the backbone slot mid-sequence onto the host path (and a new lane
431    /// must not clear the backbone's verdict) — R4/NF-2.
432    graph_refused: std::sync::atomic::AtomicBool,
433    /// Token embeddings are multiplied by this at input (Gemma: √hidden).
434    pub embed_multiplier: f32,
435    /// Attention score scale (1/√head_dim unless the arch overrides —
436    /// Gemma's query_pre_attn_scalar).
437    pub attn_scale: f32,
438    /// Sliding-window attention: (window, every-Nth-layer-is-global
439    /// pattern) — Gemma-3.
440    pub swa: Option<(usize, usize)>,
441    /// Sliding-window tail trimming, `(slack, align)` for
442    /// `LayerKvCache::trim_window`: every layer with a window keeps only
443    /// the rows its window can still read (Spark-X2.5: 576..639 rows per
444    /// 512-window layer instead of the whole context — 27 of the 4B's 36
445    /// layers). Set at load for Spark-X2.5 only (`CMF_SWA_TRIM=0` turns
446    /// it off); None keeps every row, as every other model always has.
447    pub swa_trim: Option<(usize, usize)>,
448    /// Prompt end of the current turn: the tail trim keeps every row a
449    /// rollback to it still reads (`LayerKvCache::trim_window_anchored`),
450    /// so the next chat turn can drop this turn's answer.
451    swa_trim_anchor: usize,
452    /// Fewest prompt rows the Metal chunk prefill graph takes; a shorter
453    /// chunk walks the layers on the host. 32 since the graph landed
454    /// (measured on Qwen2.5-0.5B); Spark-X2.5/X2.6 take 8 (`loader`).
455    pub chunk_min_rows: usize,
456    /// Explicit local/global schedule for architectures that cannot be
457    /// represented by Gemma's every-Nth-global convention.
458    pub sliding_layers: Option<Vec<bool>>,
459    /// Per-layer flag: this layer runs WITHOUT RoPE. Recursive Flint applies
460    /// position encoding only on its sliding-window layers; its 14
461    /// full-attention layers have none. The rotary dim of such a layer is 0,
462    /// which is what the attention kernel reads.
463    pub no_rope_layers: Option<Vec<bool>>,
464    /// Natively bounded anchor record (`arch.anchor_core`): the file's
465    /// operator, installed once at load. `Some` = the model is
466    /// bounded-native — `--o1`/`CMF_O1*` are refused, prefix reuse and
467    /// the penalty window are bounded, and no anchor layer stores
468    /// anything per position.
469    pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
470    /// The `[W][rd/2]` relative-rotation table every bounded layer shares
471    /// (built from `inv_freq` once the RoPE setup is final).
472    bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
473    /// Bounded prefix-reuse key (length + rolling hash + tail) of a
474    /// bounded-native model; `kv_history` stays empty there so nothing
475    /// in the pipeline grows with the dialogue.
476    pub kv_prefix: KvPrefix,
477    /// Prompt positions the last `generate*` call actually forwarded
478    /// (prefix reuse subtracts what the cache already held).
479    pub last_prefill_tokens: usize,
480    /// RoPE table of the sliding (local) layers, when they use their
481    /// own base frequency (Gemma-3: 10k local vs 1M global).
482    pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
483    pub rotary_dim_local: Option<usize>,
484    pub rope_scale: f32,
485    pub rope_scale_local: f32,
486    /// Gemma-4: global layers run their own geometry — (head_dim,
487    /// num_kv_heads); sliding layers keep the base fields.
488    pub global_attn: Option<(usize, usize)>,
489    /// Gemma-4: the global layers' proportional RoPE table (len
490    /// global_head_dim/2, zero-padded tail = identity rotation).
491    pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
492    /// Scale-less RMS normalization of V heads before caching (Gemma-4).
493    pub attn_v_norm: bool,
494    /// HunYuan dense: per-head q/k norm runs after RoPE (see the arch flag).
495    pub qk_norm_after_rope: bool,
496    /// The per-head `self_attn.g_proj` output gate uses sigmoid
497    /// (Spark-X2.5) instead of softplus (Laguna).
498    pub proj_gate_sigmoid: bool,
499    /// Final-logit soft-capping C: logits = C·tanh(logits/C) (Gemma-4).
500    pub final_softcap: Option<f32>,
501    /// Cortiq Embryo hierarchical head: cluster matrix [C, hidden]. The
502    /// flat logits h·Eᵀ are turned into the two-level log-probabilities
503    /// log softmax_c(h·Cᵀ)[c(v)] + log softmax_{s∈c(v)}(h·E_c(v)ᵀ)[v].
504    pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
505    /// Gemma-2 attention-logit soft-capping (0.0 = off).
506    pub attn_softcap: f32,
507    /// Compute per-token confidence (a full-vocab softmax each
508    /// token). On by default; `bench --core` turns it off to match
509    /// llama-bench's core timing.
510    confidence_on: bool,
511    /// Test-only one-shot forward failure, scoped to this pipeline so
512    /// parallel scoring tests cannot consume one another's injection.
513    #[cfg(test)]
514    nll_test_fail_at: Option<usize>,
515    /// Test-only route override; avoids mutating the process-wide
516    /// `CMF_PREFILL` environment variable while forcing the serial path.
517    #[cfg(test)]
518    nll_test_force_serial: bool,
519}
520
521#[cfg(target_os = "macos")]
522impl Drop for Pipeline {
523    fn drop(&mut self) {
524        // the async replay writes into `kv_cache` Vecs about to be freed
525        let _ = crate::gpu_metal::wait_replay();
526        crate::gpu::kv_mirror_drop(self.graph_kv_id);
527    }
528}
529
530#[cfg(not(target_os = "macos"))]
531impl Drop for Pipeline {
532    fn drop(&mut self) {
533        // The wgpu resident Embryo graph owns recurrent/KV buffers keyed by
534        // this pipeline's sequence id.  Release that sequence image when a
535        // pooled pipeline is dropped; model weights stay cached for reuse.
536        crate::gpu::graph_kv_reset(self.graph_kv_id);
537    }
538}
539
540/// Model weights. Matrices are `QTensor` (owned f32 for small models
541/// and tests — bit-identical to the historical paths — or quantized
542/// bytes zero-copy from the CMF mmap for big models). 1-D norms are
543/// always small and stay f32.
544pub struct PipelineWeights {
545    /// Embedding table: [vocab_size, hidden_size]
546    pub embed_tokens: QTensor,
547    /// Per-layer weights
548    pub layers: Vec<LayerWeights>,
549    /// LM head: [vocab_size, hidden_size]
550    pub lm_head: QTensor,
551    /// Final norm: [hidden_size]
552    pub final_norm: Vec<f32>,
553}
554
555/// One transformer layer: shared norms + MLP, attention by kind.
556pub struct LayerWeights {
557    pub input_norm: Vec<f32>,
558    /// The pre-FFN norm (`post_attention_layernorm` classically;
559    /// `pre_feedforward_layernorm` on Gemma-2/3 sandwich layers).
560    pub post_norm: Vec<f32>,
561    /// Gemma-2/3 sandwich: norm applied to the ATTENTION OUTPUT before
562    /// its residual add (`post_attention_layernorm` there).
563    pub attn_out_norm: Option<Vec<f32>>,
564    /// Gemma-4: the whole layer output is multiplied by this scalar.
565    pub layer_scale: Option<f32>,
566    /// Gemma-2/3 sandwich: norm applied to the FFN OUTPUT before its
567    /// residual add (`post_feedforward_layernorm`).
568    pub ffn_out_norm: Option<Vec<f32>>,
569    pub ffn: FfnKind,
570    pub attn: AttnKind,
571}
572
573/// FFN gate activation: SiLU (SwiGLU family), tanh-GELU (Gemma's GeGLU),
574/// or a source-specific gated contract. A property of the model, carried on
575/// every FFN triple.
576#[derive(Clone, Copy, PartialEq, Debug, Default)]
577pub enum Act {
578    #[default]
579    Silu,
580    GeluTanh,
581    /// Exact erf GELU (HF `hidden_act = "gelu"`: Spark-X2.5).
582    Gelu,
583    /// Kimi-K3 SituAndMul: BOTH halves transform —
584    /// a = β·tanh(g/β)·σ(g), up' = linβ·tanh(u/linβ) (linβ>0), out = a·up'.
585    Situ {
586        beta: f32,
587        linear_beta: f32,
588    },
589    /// MiniMax M3's SwiGLU-OAI: upper-clamped gate, symmetrically clamped
590    /// up branch, `sigmoid(alpha * gate)`, and a `+1` up-branch offset.
591    /// It deliberately has no generic GPU graph arm until a matching kernel
592    /// exists; CPU uses the exact scalar reference in `inference`.
593    SwiGluOai {
594        alpha: f32,
595        limit: f32,
596    },
597}
598
599impl Act {
600    pub fn from_arch(name: &str) -> Self {
601        if name == "gelu_tanh" {
602            Self::GeluTanh
603        } else if name == "gelu" {
604            Self::Gelu
605        } else {
606            Self::Silu
607        }
608    }
609
610    /// Arch-driven constructor (activation name + situ betas).
611    pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
612        match arch.hidden_act.as_str() {
613            "situ" => Self::Situ {
614                beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
615                linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
616            },
617            // `swigluoai` alone is deliberately not a generic activation
618            // alias.  Its alpha/limit are part of MiniMax M3's checked
619            // source contract, and accepting that string on an unrelated
620            // architecture would silently impose M3 math on its FFN.
621            "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
622                alpha: 1.702,
623                limit: 7.0,
624            },
625            other => Self::from_arch(other),
626        }
627    }
628
629    /// Construct the MiniMax M3 SwiGLU-OAI activation from a checked model
630    /// contract. This is intentionally explicit rather than recognizing an
631    /// arbitrary `hidden_act` string: callers must validate `alpha` and
632    /// `limit` against their source architecture before enabling it.
633    #[inline]
634    pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
635        Self::SwiGluOai { alpha, limit }
636    }
637
638    #[inline]
639    pub fn apply(self, x: f32) -> f32 {
640        match self {
641            Self::Silu => inference::silu(x),
642            Self::GeluTanh => inference::gelu_tanh(x),
643            Self::Gelu => inference::gelu_erf(x),
644            Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
645            Self::SwiGluOai { alpha, limit } => {
646                let gate = x.min(limit);
647                gate * (1.0 / (1.0 + (-alpha * gate).exp()))
648            }
649        }
650    }
651
652    /// Gated combine — the FFN contract. Situ transforms the UP half
653    /// too, so callers must use this instead of apply(g)·u.
654    #[inline]
655    pub fn combine(self, g: f32, u: f32) -> f32 {
656        match self {
657            Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
658                self.apply(g) * (linear_beta * (u / linear_beta).tanh())
659            }
660            Self::SwiGluOai { alpha, limit } => {
661                inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
662            }
663            _ => self.apply(g) * u,
664        }
665    }
666
667    /// The wgpu graphs' arm for this activation; None = no device kernel
668    /// computes it, and the graph builder must refuse the layer (the dense
669    /// graph FFN once computed SiLU for whatever the model asked for).
670    pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
671        match self {
672            Self::Silu => Some(crate::gpu::GraphAct::Silu),
673            Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
674            Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
675            Self::Situ { .. } | Self::SwiGluOai { .. } => None,
676        }
677    }
678
679    /// The WGPU graph arm.  Keep this deliberately separate from
680    /// `graph_act`: OAI needs its dedicated code rather than a generic SiLU
681    /// fallback.  Only the frozen MiniMax-M3 source constants get a device
682    /// code; a future OAI-like architecture with different constants must add
683    /// and test its own kernel contract.
684    pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
685        self.moe_job_graph_contract().map(|(act, _)| act)
686    }
687
688    /// The exact device contract for a gate/up/down MoE job.  OAI is not a
689    /// generic "clamped SiLU": code 3 commits a backend to MiniMax M3's
690    /// alpha, asymmetric up transform, and frozen clamp.  Keep the clamp
691    /// alongside the marker so a backend cannot accidentally run code 3 with
692    /// a nearby model's limit.
693    pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
694        match self {
695            Self::SwiGluOai { alpha, limit }
696                if alpha.to_bits() == 1.702f32.to_bits()
697                    && limit.to_bits() == 7.0f32.to_bits() =>
698            {
699                Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
700            }
701            _ => self.graph_act().map(|act| (act, 0.0)),
702        }
703    }
704}
705
706/// Dense gated triple — the FFN of a dense layer or of one expert.
707pub struct DenseFfn {
708    pub gate_proj: QTensor,
709    pub up_proj: QTensor,
710    pub down_proj: QTensor,
711    /// Gate activation (SiLU default; Gemma: tanh-GELU).
712    pub act: Act,
713    /// `down_proj` stored transposed (`[inter, hidden]`), when the file
714    /// carries it. Only the per-token sparse path reads it: a neuron's
715    /// down weights are a contiguous ROW there, so the token's chosen
716    /// neurons are the only bytes touched. `None` = the ordinary layout,
717    /// and the sparse path stays off.
718    pub down_t: Option<QTensor>,
719    /// Task tubes (spec: defragged task-conditional width). The three
720    /// matrices above are the CORE — the neurons every task computes;
721    /// each tube is an independently quantized slice of the SAME layer
722    /// holding the neurons only some tasks need. A tube is a normal
723    /// tensor triple, so every kernel runs it unchanged, and the bytes
724    /// of an inactive tube are never read. Empty = ordinary dense FFN.
725    pub segs: Vec<FfnSeg>,
726}
727
728/// One task tube: a contiguous slice of a layer's FFN neurons, stored
729/// as its own `[w, hidden]` / `[hidden, w]` triple. `start` is the
730/// neuron's index in the layer's FULL space (core first, then tubes in
731/// order) — the bit a task mask sets to switch this tube on.
732pub struct FfnSeg {
733    pub gate: QTensor,
734    pub up: QTensor,
735    pub down: QTensor,
736    pub start: usize,
737    pub width: usize,
738}
739
740/// FFN operator of a layer, decided by tensor presence at load time
741/// (router `mlp.gate.weight` in the directory = MoE layer).
742pub enum FfnKind {
743    Dense(DenseFfn),
744    /// Mixture-of-Experts (Qwen2-MoE / Qwen3-MoE): softmax over ALL
745    /// expert logits → top-k, optional renorm; experts stay quantized
746    /// in mmap — only the selected ones are touched per token.
747    Moe(MoeFfn),
748    /// Gemma-4 MoE: a dense MLP branch AND a routed-expert branch in
749    /// the SAME layer, each with its own norm sandwich. The dense
750    /// branch reads the pre-FFN-normed input; the expert branch (and
751    /// the router) read the RAW residual through `pre_norm_2`:
752    ///   d = post_norm_1(dense(x̂));  m = post_norm_2(Σwₑ·FFNₑ(pre_norm_2(h)))
753    ///   ffn_out = d + m   (the caller's ffn_out_norm + residual follow)
754    DenseMoe(Box<DenseMoeFfn>),
755}
756
757/// Gemma-4 dual-branch FFN (see `FfnKind::DenseMoe`).
758pub struct DenseMoeFfn {
759    pub dense: DenseFfn,
760    pub moe: MoeFfn,
761    /// post_feedforward_layernorm_1 — dense-branch output norm.
762    pub post_norm_1: Vec<f32>,
763    /// pre_feedforward_layernorm_2 — expert-branch input norm (applied
764    /// to the RAW residual, not the pre-FFN-normed activation).
765    pub pre_norm_2: Vec<f32>,
766    /// post_feedforward_layernorm_2 — expert-branch output norm.
767    pub post_norm_2: Vec<f32>,
768}
769
770pub struct MoeFfn {
771    /// Router `mlp.gate.weight` [num_experts, hidden].
772    pub router: QTensor,
773    pub experts: Vec<DenseFfn>,
774    pub top_k: usize,
775    pub norm_topk_prob: bool,
776    /// Router scores per-expert with a sigmoid (LFM2-MoE / DeepSeek-V3
777    /// `noaux_tc`) instead of a softmax over all experts (Qwen).
778    pub router_sigmoid: bool,
779    /// Addend in the selected-score denominator of a sigmoid router.
780    /// Most supported checkpoints use the historical `1e-6` floor; Flint's
781    /// reference routing uses `1e-20`; MiniMax M3.1 preserves the same FP32
782    /// `sum + 1e-20` law. This is per-MoE rather than a global family switch
783    /// because the same sigmoid routing law has several contracts.
784    pub sigmoid_denom_eps: f32,
785    /// Per-expert selection bias `mlp.expert_bias` [num_experts]
786    /// (LFM2-MoE): added to the sigmoid scores for the top-k CHOICE only;
787    /// the gathered weights use the unbiased scores. None = no bias.
788    pub expert_bias: Option<Vec<f32>>,
789    /// Top-k weights are multiplied by this after the optional renorm
790    /// (LFM2-MoE `routed_scaling_factor`; 1.0 = off).
791    pub routed_scaling: f32,
792    /// Adaptive routing (CMF_MOE_TAU, opt-in): keep the smallest
793    /// prefix of the top-k whose renormalized mass reaches τ —
794    /// confident tokens touch 1–2 experts, flat ones keep all k.
795    /// MoE decode is memory-bound, so skipped experts are skipped
796    /// weight traffic. None = classic fixed top-k (bit-identical).
797    pub route_tau: Option<f32>,
798    /// Always-on shared expert. Qwen2-MoE carries an additional sigmoid
799    /// gate; Laguna adds the shared expert unconditionally (`None`).
800    pub shared: Option<(DenseFfn, Option<QTensor>)>,
801    /// Expert-selection counters (truncated Fisher B-field of claim 12:
802    /// routing frequency during calibration). Filled by every forward,
803    /// read by the CLI via CMF_MOE_STATS. RefCell: decode is single-threaded.
804    pub stats: std::cell::RefCell<Vec<u64>>,
805    /// Per-CHANNEL sum of squares of this FFN's input, accumulated over a
806    /// calibration run (`CMF_RMS_TRACE`). These are the RMS activation
807    /// traces AWNP needs: raw weight magnitude says every channel matters
808    /// equally, and the question AWNP asks is whether the ACTIVATIONS
809    /// disagree. Off unless the env var is set — an f64 add per channel
810    /// per token is cheap, but not free.
811    pub act_sq: std::cell::RefCell<Vec<f64>>,
812    /// Raw FFN-input rows captured for the layers named by `CMF_ACT_DUMP`
813    /// (`"9,19"`). AWNP is nullspace PROJECTION: after dropping channels the
814    /// survivors are refitted to absorb what was removed, and how much they
815    /// can absorb depends on the activation COVARIANCE, not on per-channel
816    /// RMS. Per-channel numbers can only bound the cost from above.
817    pub act_rows: std::cell::RefCell<Vec<f32>>,
818    /// Task mask over routed experts (DTG-MA over MoE, claim-12 B-field
819    /// applied): `false` experts are excluded from selection, the
820    /// softmax renormalizes over the allowed set. Built by the loader
821    /// from CMF_MOE_MASK=<stats.json> + CMF_MOE_MASK_COVER. None = all.
822    pub mask: Option<Vec<bool>>,
823    /// Gemma-4: per-expert weight scale applied AFTER the top-k renorm
824    /// (`router.per_expert_scale`). None = 1.0 everywhere.
825    pub per_expert_scale: Option<Vec<f32>>,
826    /// Gemma-4: the router reads a SCALE-LESS rms-norm of its input
827    /// (the constant gain router.scale·√hidden is folded into the
828    /// router weights at convert time).
829    pub router_input_norm: bool,
830    /// Cortiq Embryo: resonance routing (P1) — the "logits" are
831    /// bias_e − ‖(x−μ_e) − U_eᵀU_e(x−μ_e)‖², argmax = the expert whose
832    /// descriptor reconstructs the input best. `router` is a placeholder.
833    pub resonance: Option<Resonance>,
834    /// Growth records (`kind = "expert_append"`, spec §9.5.1) mounted
835    /// behind the trunk experts, one entry per grown expert IN THE ORDER
836    /// they sit in `experts` (the tail `experts[experts.len() - grown.len()..]`).
837    /// The trunk keeps `experts.len() - grown.len()` experts. Empty on a
838    /// gated MoE, on a file without records and under `CMF_GROWTH=off`.
839    pub grown: Vec<GrownExpert>,
840}
841
842/// One grown expert of an `expert_append` record as the loader mounted it.
843#[derive(Debug, Clone, PartialEq, Eq)]
844pub struct GrownExpert {
845    /// The record's skill id.
846    pub record: String,
847    /// Position of the record in `header.skills`.
848    pub record_index: usize,
849    pub layer: usize,
850    /// The index the tensor name declares (`experts.{e}` — the chain rule
851    /// of the format); the executed position may be smaller when an
852    /// earlier record of the layer is not mounted.
853    pub expert: usize,
854}
855
856/// Per-expert resonance descriptors of one MoE layer (`mlp.desc.*`).
857pub struct Resonance {
858    /// [E, hidden]
859    pub mu: Vec<f32>,
860    /// [E, k, hidden] orthonormal directions (k may be 0)
861    pub u: Vec<f32>,
862    pub k: usize,
863    /// [E] selection bias (loss-free balancing, trained online)
864    pub bias: Vec<f32>,
865    /// [E] reconstruction-error shell: an expert whose error
866    /// `‖(x−μ)⊥U‖² = d² − proj` exceeds its shell scores `−∞` (it never
867    /// wins). `+inf` = no shell — every trunk expert; a grown expert
868    /// (`expert_append`) carries the finite `desc.shell` its record stores.
869    /// Empty = no shell anywhere (legacy constructors).
870    pub shell: Vec<f32>,
871}
872
873/// `CMF_GROWTH_SHELL` state: 0 = not yet read from the environment, 1 = on,
874/// 2 = off. Process-wide, like the environment it mirrors.
875static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
876
877/// Is the growth shell applied (`Resonance::scores` −∞ rule, the resident
878/// graph's packed shell)? `CMF_GROWTH_SHELL=off` disables it for
879/// measurement; [`set_growth_shell`] overrides the environment in-process
880/// (`growth-eval --shell`). Default: on.
881pub fn growth_shell_enabled() -> bool {
882    use std::sync::atomic::Ordering;
883    match GROWTH_SHELL.load(Ordering::Relaxed) {
884        1 => true,
885        2 => false,
886        _ => {
887            let off = std::env::var("CMF_GROWTH_SHELL")
888                .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
889                .unwrap_or(false);
890            GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
891            !off
892        }
893    }
894}
895
896/// Switch the growth shell on/off for this process (`None` = re-read
897/// `CMF_GROWTH_SHELL` on the next query). A pipeline packed into the
898/// resident graph BEFORE the switch keeps the shell it was packed with —
899/// build a new pipeline after switching.
900pub fn set_growth_shell(on: Option<bool>) {
901    GROWTH_SHELL.store(
902        match on {
903            Some(true) => 1,
904            Some(false) => 2,
905            None => 0,
906        },
907        std::sync::atomic::Ordering::Relaxed,
908    );
909}
910
911impl Resonance {
912    /// Does any expert carry a finite shell (a mounted growth record)?
913    pub fn has_shell(&self) -> bool {
914        self.shell.iter().any(|s| s.is_finite())
915    }
916
917    /// The shell the runtime applies right now: the stored one, or all
918    /// `+inf` when the shell is switched off (`CMF_GROWTH_SHELL=off`).
919    pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
920        let mut out = vec![f32::INFINITY; ne];
921        if growth_shell_enabled() {
922            for (o, s) in out.iter_mut().zip(&self.shell) {
923                *o = *s;
924            }
925        }
926        out
927    }
928
929    /// Routing scores for one input row (higher = better). A grown
930    /// expert whose reconstruction error lies outside its shell gets
931    /// `−∞` (unless the shell is switched off); trunk rows are the exact
932    /// bit pattern they were before growth.
933    pub fn scores(&self, x: &[f32], out: &mut [f32]) {
934        let h = x.len();
935        let ne = out.len();
936        let shell_on = growth_shell_enabled() && !self.shell.is_empty();
937        for e in 0..ne {
938            let mu = &self.mu[e * h..(e + 1) * h];
939            let mut d2 = 0.0f32;
940            for j in 0..h {
941                let d = x[j] - mu[j];
942                d2 += d * d;
943            }
944            let mut proj = 0.0f32;
945            for i in 0..self.k {
946                let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
947                let mut p = 0.0f32;
948                for j in 0..h {
949                    p += (x[j] - mu[j]) * u[j];
950                }
951                proj += p * p;
952            }
953            let err = d2 - proj;
954            out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
955            if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
956                out[e] = f32::NEG_INFINITY;
957            }
958        }
959    }
960}
961
962/// Attention operator of a layer. Extension point: new operators are
963/// new variants here + a forward in their own module.
964pub enum AttnKind {
965    /// GQA softmax attention (+ optional Qwen3.5 qk-norm / output gate).
966    Full {
967        wq: QTensor,
968        wk: QTensor,
969        wv: QTensor,
970        wo: QTensor,
971        q_norm: Option<Vec<f32>>,
972        k_norm: Option<Vec<f32>>,
973        output_gate: bool,
974        /// Laguna: a separate softplus projection applied to the attention
975        /// output before O. The bool means one scalar per head (broadcast
976        /// across head_dim); false means one scalar per element.
977        softplus_gate: Option<(QTensor, bool)>,
978        /// Qwen2-family projection biases (q, k, v).
979        bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
980    },
981    /// Canonical linear core (VMF phase attention).
982    Linear(VmfPhaseWeights),
983    /// Faithful vendor linear operator (Qwen3.5 GatedDeltaNet).
984    LinearGdn(GdnWeights),
985    /// LFM2 gated short-convolution mixer (no KV cache; conv ring state
986    /// lives in the layer's `linear_state`).
987    ShortConv(ShortConvWeights),
988    /// DeepSeek-V2 Multi-head Latent Attention. v1 executes it as
989    /// expand-to-MHA: the latent is projected per token, K/V expand to
990    /// every head and live in the ordinary cache (K head layout
991    /// [rope | nope] so the standard partial rotary covers the shared
992    /// rope key; V rows are zero-padded to the K head_dim and the pad
993    /// is sliced off before O). Latent-resident cache is a later
994    /// optimization, not a semantic change.
995    Mla(Box<MlaWeights>),
996    /// Kimi Delta Attention (Kimi Linear / Kimi-K3): per-channel decayed
997    /// delta rule, separate q/k/v short convs, sigmoid-gated output norm.
998    /// State lives in the layer's `linear_state` (no KV cache).
999    Kda(Box<crate::linear_core::KdaWeights>),
1000    /// Natively bounded softmax anchor `swa_sink_v1` (Embryo-O1): ring of
1001    /// the last W raw keys with relative RoPE + trained NoPE sinks, one
1002    /// softmax. State is the fixed-size ring in `LayerKvCache::bounded`;
1003    /// nothing is stored per position (see `crate::bounded`).
1004    Bounded(Box<crate::bounded::BoundedWeights>),
1005}
1006
1007/// DeepSeek-V2 MLA projections (see `AttnKind::Mla`).
1008pub struct MlaWeights {
1009    /// `[nh·(rope+nope), hidden]` (or `[…, q_lora]` when compressed) —
1010    /// the converter permutes each head rope-first so rotary_dim =
1011    /// qk_rope works unchanged.
1012    pub q_proj: QTensor,
1013    /// Compressed q (K3/V3 class): x → q_a `[q_lora, hidden]` →
1014    /// rms(q_a_norm) → q_proj (= q_b). None = direct q (V2-Lite).
1015    pub q_a: Option<QTensor>,
1016    pub q_a_norm: Option<Vec<f32>>,
1017    /// `kv_a_proj_with_mqa` `[lora + rope, hidden]` (latent first).
1018    pub kv_a: QTensor,
1019    /// RMS-norm weights over the latent (`kv_a_layernorm`, [lora]).
1020    pub kv_a_norm: Vec<f32>,
1021    /// `[nh·(nope+v), lora]` — per head [k_nope | v].
1022    pub kv_b: QTensor,
1023    /// `[hidden, nh·v]`.
1024    pub o_proj: QTensor,
1025    pub nh: usize,
1026    pub qk_rope: usize,
1027    pub qk_nope: usize,
1028    pub v_dim: usize,
1029    pub lora: usize,
1030    /// Softmax scale (1/√(rope+nope), YaRN-mscale-corrected at load).
1031    pub scale: f32,
1032    /// Kimi Linear NoPE: skip the rotary entirely (layout unchanged).
1033    pub nope: bool,
1034}
1035
1036/// Multi-token-prediction head (DeepSeek/Qwen style, spec §2.1):
1037/// `x = eh_proj·[enorm(embed(next)); hnorm(hidden)]` → one transformer
1038/// block over its own KV → shared lm_head. Drafts the token after next;
1039/// the main model verifies, so output is exact — MTP only buys speed.
1040pub struct MtpModule {
1041    pub enorm: Vec<f32>,
1042    pub hnorm: Vec<f32>,
1043    /// [hidden, 2·hidden]
1044    pub eh_proj: QTensor,
1045    pub layer: LayerWeights,
1046    pub final_norm: Vec<f32>,
1047    pub kv: crate::kv_cache::LayerKvCache,
1048}
1049
1050/// Teacher-forced activation statistics for one MTP draft FFN.
1051///
1052/// `mass[i]` is `Σ_rows |activation_i|`; `squared_mass[i]` is
1053/// `Σ_rows activation_i²`, where `activation = act(gate) · up` immediately
1054/// before the draft layer's down projection. `observed_positions` is the
1055/// number of teacher-forced rows that contributed to both arrays.
1056///
1057/// Rows of FFN input the canonical draft probe measures in one batch.
1058const MTP_PROBE_FFN_ROWS: usize = 128;
1059
1060/// [`Pipeline::probe_mtp_ffn`] returns one of these for each actual draft
1061/// layer.  It intentionally has a separate type/API from
1062/// [`Pipeline::probe_ffn_mass`] so a compactor cannot accidentally substitute
1063/// trunk statistics for an MTP sidecar whose FFN has a different width.
1064#[derive(Clone, Debug, PartialEq)]
1065pub struct MtpFfnProbe {
1066    pub mass: Vec<f64>,
1067    pub squared_mass: Vec<f64>,
1068    pub observed_positions: usize,
1069}
1070
1071/// A Metal verify graph after its sync: what the commit needs — the
1072/// graph (per-layer replay scratch), the GDN layers in encode order (their
1073/// CPU states receive the replay), and the attention layers with the CPU
1074/// row count they were encoded against (the accepted rows are pulled from
1075/// the mirror from there).
1076/// One item of the Metal rows-graph plan.
1077#[cfg(target_os = "macos")]
1078enum MetalRowsItem<'a> {
1079    Gdn {
1080        run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1081        first: usize,
1082    },
1083    Attn {
1084        l: crate::gpu_metal::AttnGpuLayer<'a>,
1085        li: usize,
1086        q_norm: Option<&'a [f32]>,
1087        k_norm: Option<&'a [f32]>,
1088        output_gate: bool,
1089    },
1090}
1091
1092#[cfg(target_os = "macos")]
1093struct MetalVerifyPending {
1094    graph: crate::gpu_metal::VerifyGraph,
1095    gdn_layers: Vec<usize>,
1096    attn_layers: Vec<(usize, usize)>,
1097}
1098
1099/// A round's batched MTP warm-up, submitted but not yet waited
1100/// (`mtp_warm_batch_submit` → `mtp_warm_batch_finish`): the trunk commit's
1101/// GDN replay is queued between the two.
1102#[cfg(target_os = "macos")]
1103struct MetalWarmPending {
1104    graph: crate::gpu_metal::VerifyGraph,
1105    cpu_stored: usize,
1106    b: usize,
1107}
1108
1109#[cfg(target_os = "macos")]
1110enum MetalRowsRun {
1111    /// Capability/preflight refusal before a command buffer was committed.
1112    Declined,
1113    /// A graph was admitted and then failed; callers must clear the sequence
1114    /// rather than replaying it through CPU/serial state.
1115    Failed,
1116    Completed(MetalVerifyPending),
1117}
1118
1119#[cfg(target_os = "macos")]
1120enum MetalPrefillOutcome {
1121    Declined,
1122    Failed,
1123    Completed(Vec<f32>),
1124}
1125
1126#[cfg(target_os = "macos")]
1127enum MetalBatchNllOutcome {
1128    Declined,
1129    Failed(String),
1130    Completed(f64, usize),
1131}
1132
1133/// The speculation trial's phases (see the decode loop): up to five timed
1134/// speculative rounds, eight timed plain tokens, then the faster arm until a
1135/// re-check. Two consecutive rounds with no accepted draft skip directly to
1136/// the plain measurement: another speculative round cannot pay when it emits
1137/// exactly the one token the plain loop would have produced.
1138#[derive(Clone, Copy)]
1139enum SpecTrial {
1140    Spec {
1141        t0: std::time::Instant,
1142        gen0: usize,
1143        rounds: usize,
1144    },
1145    Plain {
1146        t0: std::time::Instant,
1147        gen0: usize,
1148    },
1149    Decided {
1150        spec: bool,
1151        recheck_at: usize,
1152    },
1153}
1154
1155/// `CMF_GRAPH_SPEC_TIME`: 0 = off, 1 = one line per speculative round
1156/// plus the host stamps of any OUTLIER round (wall > 1.4× the running
1157/// median), 2 = the host stamps of every round.
1158pub(crate) fn spec_time_level() -> u8 {
1159    static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1160    *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1161        Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1162        Err(_) => 0,
1163    })
1164}
1165
1166/// `GraphMoeBank` reuse across tokens (`CMF_GRAPH_MOE_BANK=0` rebuilds
1167/// every layer's bank on every token, the route before the cache).
1168fn graph_moe_bank_on() -> bool {
1169    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1170    *ON.get_or_init(|| std::env::var("CMF_GRAPH_MOE_BANK").as_deref() != Ok("0"))
1171}
1172
1173/// Decide whether the batched speculative verifier is admitted by the
1174/// user/backend policy, before capability checks.  Native Metal's row-batch
1175/// verifier is deliberately fail-closed by default: it has produced a
1176/// different *emitted* greedy token than the serial path on a real model.
1177/// It remains available only through the documented explicit experiment
1178/// spelling, `CMF_GRAPH_SPEC=1`; other backends retain their historical
1179/// nonzero override behaviour.
1180fn graph_spec_wanted_by_policy(
1181    spec_env: Option<&str>,
1182    spec_default_ok: bool,
1183    penalized: bool,
1184    metal_wgpu: bool,
1185    native_metal: bool,
1186) -> bool {
1187    match spec_env {
1188        Some("0") => false,
1189        // Keep the native path genuinely explicit.  In particular, a
1190        // typo/nonzero value must not silently re-enable an unqualified
1191        // verifier just because historical non-Metal parsing treated it as
1192        // force-on.
1193        Some("1") => true,
1194        Some(_) => !native_metal,
1195        // The discrete/wgpu default stays as it was; only native Metal has
1196        // a quality gate here.
1197        None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1198    }
1199}
1200
1201/// The round's host stamps: `spec_stamp(name)` records the time since
1202/// the previous stamp (the section that just ended) — from anywhere on
1203/// the round's call chain (the Metal verify, the draft step, the commit),
1204/// no plumbing. Off (a single atomic load) unless `CMF_GRAPH_SPEC_TIME`
1205/// is set; one decode thread at a time is assumed (diagnostics).
1206struct SpecStampLog {
1207    t_last: std::time::Instant,
1208    items: Vec<(&'static str, f32)>,
1209}
1210
1211static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1212
1213pub(crate) fn spec_stamp(name: &'static str) {
1214    if spec_time_level() == 0 {
1215        return;
1216    }
1217    if let Ok(mut g) = SPEC_STAMPS.lock() {
1218        if let Some(log) = g.as_mut() {
1219            let now = std::time::Instant::now();
1220            log.items
1221                .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1222            log.t_last = now;
1223        }
1224    }
1225}
1226
1227fn spec_stamps_begin() {
1228    if spec_time_level() == 0 {
1229        return;
1230    }
1231    if let Ok(mut g) = SPEC_STAMPS.lock() {
1232        *g = Some(SpecStampLog {
1233            t_last: std::time::Instant::now(),
1234            items: Vec::with_capacity(64),
1235        });
1236    }
1237}
1238
1239fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1240    SPEC_STAMPS
1241        .lock()
1242        .ok()
1243        .and_then(|mut g| g.take())
1244        .map(|l| l.items)
1245        .unwrap_or_default()
1246}
1247
1248/// One line: every stamp name in first-seen order with its total over the
1249/// round and, when it fired more than once (the draft steps), the count.
1250fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1251    let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1252    for &(n, ms) in items {
1253        match agg.iter_mut().find(|e| e.0 == n) {
1254            Some(e) => {
1255                e.1 += ms;
1256                e.2 += 1;
1257            }
1258            None => agg.push((n, ms, 1)),
1259        }
1260    }
1261    let mut s = String::with_capacity(agg.len() * 16);
1262    for (n, ms, k) in agg {
1263        if k > 1 {
1264            s.push_str(&format!("{n} {ms:.1}/{k} "));
1265        } else {
1266            s.push_str(&format!("{n} {ms:.1} "));
1267        }
1268    }
1269    s
1270}
1271
1272/// The speculation monitor: exponential averages of a round's wall time
1273/// and of the tokens it produced, and the plain token's wall time — the
1274/// three numbers the keep/stop rule needs. A round pays when
1275/// `tokens_per_round · plain_ms > round_ms · 1.03`. The one-shot trial
1276/// (four rounds against eight tokens) mis-called prose: the first rounds
1277/// after a prompt are formulaic and accept well, the body does not (an
1278/// essay measured 39 against a plain 44.8 with the trial saying
1279/// "speculate"), so the rule now runs on EVERY round and stops after four
1280/// consecutive losing rounds; a stopped speculation is retried 128 tokens
1281/// later.
1282///
1283/// Native Metal (`metal: true`) does not pay the eight plain tokens up
1284/// front: on the 27B a plain token is ~150 ms, so the trial alone cost
1285/// ~1.2 s of every answer. There the plain phase is (a) skipped while the
1286/// rounds land at least `SPEC_PROXY_TOKENS` tokens each — a k=7 round on
1287/// Metal costs ~1.9 plain tokens (286 against 148 ms measured on the M4),
1288/// so 3.5 tokens/round cannot lose on any Metal round/plain ratio seen —
1289/// and (b) otherwise bounded to the fewest tokens that time it: two, or
1290/// as many as fit in `SPEC_PLAIN_MIN_MS` (a 150-ms token measures itself;
1291/// a 10-ms one needs the eight). The keep/stop rule itself is unchanged:
1292/// the moment a plain rate exists, it decides.
1293#[derive(Default, Clone, Copy)]
1294struct SpecMon {
1295    round_ms: f64,
1296    tokens: f64,
1297    plain_ms: f64,
1298    n: u32,
1299    fails: u32,
1300    /// Consecutive rounds that produced only their verifier-tip token (zero
1301    /// accepted MTP drafts). This is deliberately counted before the warm-up
1302    /// round is excluded from timing: two real zero-acceptance rounds are
1303    /// enough to stop charging an obviously losing speculative arm.
1304    zero_accept_rounds: u8,
1305    metal: bool,
1306}
1307
1308/// Tokens per round at or above which a Metal round pays without a plain
1309/// measurement (see `SpecMon`).
1310const SPEC_PROXY_TOKENS: f64 = 3.5;
1311/// The Metal plain phase: at least two tokens, and more until this much
1312/// wall time has been timed (up to the eight the other backends time).
1313const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1314
1315impl SpecMon {
1316    fn round(&mut self, dt_ms: f64, produced: usize) {
1317        if produced <= 1 {
1318            self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1319        } else {
1320            self.zero_accept_rounds = 0;
1321        }
1322        self.n += 1;
1323        if self.n == 1 {
1324            return; // round 1 pays the batch scratch and the draft mirror
1325        }
1326        let a = if self.n == 2 { 1.0 } else { 0.3 };
1327        self.round_ms += a * (dt_ms - self.round_ms);
1328        self.tokens += a * (produced as f64 - self.tokens);
1329    }
1330    fn pays(&self) -> bool {
1331        if self.plain_ms > 0.0 {
1332            self.tokens * self.plain_ms > self.round_ms * 1.03
1333        } else {
1334            self.metal && self.tokens >= SPEC_PROXY_TOKENS
1335        }
1336    }
1337    fn repeated_zero_acceptance(&self) -> bool {
1338        self.zero_accept_rounds >= 2
1339    }
1340    /// Has the plain phase timed enough tokens to decide?
1341    fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1342        let n = generated.saturating_sub(gen0);
1343        if n >= 8 {
1344            return true;
1345        }
1346        self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1347    }
1348}
1349
1350/// Ids of the consumed prefix the bounded reuse key remembers literally
1351/// (the rest is covered by the rolling hash).
1352pub const KV_PREFIX_TAIL: usize = 128;
1353
1354/// Bounded prefix-reuse key: how many ids the cache holds, a rolling
1355/// hash of ALL of them and the last [`KV_PREFIX_TAIL`] ids literally.
1356/// Answers "does this prompt strictly extend what is cached" exactly
1357/// (hash over the whole consumed prefix + literal tail) without keeping
1358/// the dialogue — the record is a fixed size whatever the session length.
1359#[derive(Debug, Clone, Default)]
1360pub struct KvPrefix {
1361    len: usize,
1362    hash: u64,
1363    tail: Vec<u32>,
1364    /// Owner of the state this key describes: the resident device graph
1365    /// (true) or the host. A turn continues the prefix only on its owner
1366    /// (R4: a host continuation of a device sequence reads an empty host
1367    /// state; a device continuation of a host sequence has no image).
1368    device: bool,
1369}
1370
1371impl KvPrefix {
1372    #[inline]
1373    fn fold(mut h: u64, ids: &[u32]) -> u64 {
1374        for &id in ids {
1375            h ^= id as u64;
1376            h = h.wrapping_mul(0x100000001b3);
1377            h ^= h >> 29;
1378        }
1379        h
1380    }
1381
1382    pub fn clear(&mut self) {
1383        self.len = 0;
1384        self.hash = 0xcbf29ce484222325;
1385        self.tail.clear();
1386        self.device = false;
1387    }
1388
1389    /// Was the prefix built on the resident device graph?
1390    pub fn on_device(&self) -> bool {
1391        self.device
1392    }
1393
1394    /// Tag the owner of the recorded prefix.
1395    pub fn set_on_device(&mut self, device: bool) {
1396        self.device = device;
1397    }
1398
1399    /// Ids the cache holds (the forwarded prefix).
1400    pub fn len(&self) -> usize {
1401        self.len
1402    }
1403
1404    pub fn is_empty(&self) -> bool {
1405        self.len == 0
1406    }
1407
1408    /// Literal tail currently kept (≤ `KV_PREFIX_TAIL`).
1409    pub fn tail_len(&self) -> usize {
1410        self.tail.len()
1411    }
1412
1413    /// Replace the key with `ids` (a fresh sequence).
1414    pub fn set(&mut self, ids: &[u32]) {
1415        self.clear();
1416        self.extend(ids);
1417    }
1418
1419    /// Append `more` to the consumed prefix (an extension-only turn).
1420    pub fn extend(&mut self, more: &[u32]) {
1421        if self.len == 0 && self.hash == 0 {
1422            self.hash = 0xcbf29ce484222325;
1423        }
1424        self.hash = Self::fold(self.hash, more);
1425        self.len += more.len();
1426        if more.len() >= KV_PREFIX_TAIL {
1427            self.tail.clear();
1428            self.tail
1429                .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1430        } else {
1431            let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1432            self.tail.drain(..drop);
1433            self.tail.extend_from_slice(more);
1434        }
1435    }
1436
1437    /// Cached positions when `ids` strictly extends the consumed prefix,
1438    /// 0 otherwise. The tail is compared literally first (cheap), then
1439    /// the hash over the whole prefix must agree.
1440    pub fn extension(&self, ids: &[u32]) -> usize {
1441        if self.len == 0 || ids.len() <= self.len {
1442            return 0;
1443        }
1444        let t = self.tail.len();
1445        if ids[self.len - t..self.len] != self.tail[..] {
1446            return 0;
1447        }
1448        if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1449            return 0;
1450        }
1451        self.len
1452    }
1453}
1454
1455/// Result of a generation call.
1456pub struct GenerateResult {
1457    pub text: String,
1458    pub token_ids: Vec<u32>,
1459    pub prompt_tokens: usize,
1460    pub tokens_generated: usize,
1461    pub finish_reason: String,
1462    /// Speculative-decode stats (0/0 when MTP is absent or inactive).
1463    pub mtp_drafted: usize,
1464    pub mtp_accepted: usize,
1465    /// Per-generated-token confidence = softmax probability of the token
1466    /// that was actually emitted (softmax probability on the chosen state). High =
1467    /// the model was sure; low = it was guessing. Same length as the
1468    /// generated slice of `token_ids`.
1469    pub token_confidence: Vec<f32>,
1470    /// Structured per-token telemetry (B4 channel). Empty unless
1471    /// `set_trace(true)`; otherwise same length as the generated slice.
1472    pub traces: Vec<TokenTrace>,
1473}
1474
1475/// One row of the structured telemetry trace (B4): the model's internal
1476/// routing state at the moment a token was emitted. Every field is a
1477/// quantity the runtime already computes — nothing is inferred or
1478/// estimated (anti-principle: only measured bytes).
1479#[derive(Clone, Debug)]
1480pub struct TokenTrace {
1481    /// 0-based index within the generated slice.
1482    pub t: usize,
1483    /// The emitted token id.
1484    pub token_id: u32,
1485    /// Softmax probability on the emitted token — how sure the model was.
1486    pub confidence: f32,
1487    /// Skill in force while this token was generated (None = backbone).
1488    pub active_skill: Option<String>,
1489    /// Recon error E = ‖r−BBᵀr‖²/‖φ‖² at the last routing eval — coherence
1490    /// with the active skill's subspace (low = coherent). None = no router
1491    /// or not yet evaluated.
1492    pub recon: Option<f32>,
1493    /// The router changed the active skill right after this token (a
1494    /// domain boundary crossed under the hysteresis barrier).
1495    pub switched: bool,
1496}
1497
1498/// Calibrated softmax probability of `id` under `logits` (the confidence on
1499/// the emitted token) — the confidence signal, cheap from logits already
1500/// computed for sampling. `temp` is the calibration temperature (B1):
1501/// softmax(logits / temp); 1.0 = raw.
1502#[cfg_attr(not(test), allow(dead_code))]
1503fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1504    let t = if temp > 1e-3 { temp } else { 1.0 };
1505    let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1506    let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1507    if sum > 0.0 {
1508        (((logits[id as usize] - max) / t).exp()) / sum
1509    } else {
1510        0.0
1511    }
1512}
1513
1514/// prefill-GEMM enabled? (CMF_PREFILL=seq — emergency fallback to the
1515/// sequential path.)
1516fn prefill_batched() -> bool {
1517    std::env::var("CMF_PREFILL")
1518        .map(|v| v != "seq")
1519        .unwrap_or(true)
1520}
1521
1522/// Decide the graph NLL route without conflating graph quality with the
1523/// optional native-Metal fused head. A hidden-state graph remains a valid
1524/// quality route on Vulkan/Wgpu; only native Metal requires graph logits.
1525#[inline]
1526/// ‖a − b‖ / ‖b‖ (infinite when the lengths differ).
1527fn rel_l2(a: &[f32], b: &[f32]) -> f64 {
1528    if a.len() != b.len() {
1529        return f64::INFINITY;
1530    }
1531    let (mut d, mut r) = (0f64, 0f64);
1532    for (&x, &y) in a.iter().zip(b) {
1533        let e = (x - y) as f64;
1534        d += e * e;
1535        r += y as f64 * y as f64;
1536    }
1537    (d / r.max(1e-30)).sqrt()
1538}
1539
1540fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1541    let graph_quality = unmasked && prefer_graph;
1542    let fused_head_quality = graph_quality && native_metal;
1543    (graph_quality, fused_head_quality)
1544}
1545
1546/// Input to the layer-major batched span walk: token ids (embeds itself,
1547/// full-stack and coordinator prefill) or ready boundary hiddens (the
1548/// network worker's side of a split).
1549#[derive(Clone, Copy)]
1550enum PrefillIn<'a> {
1551    Ids(&'a [u32]),
1552    Hidden(&'a [f32]),
1553}
1554
1555/// The batched prefill walks `weights.layers`. Architectures that load
1556/// their own stack (gemma-3n's AltUp replicas, DeepSeek-V4's hyper-
1557/// connections) leave that empty and must go position by position — asking
1558/// otherwise indexes an empty vector, which is a panic rather than a
1559/// fallback. Every call site goes through here so the next such
1560/// architecture is one line, not four.
1561impl Pipeline {
1562    pub(crate) fn can_prefill_batched(&self) -> bool {
1563        #[cfg(test)]
1564        let force_serial = self.nll_test_force_serial;
1565        #[cfg(not(test))]
1566        let force_serial = false;
1567        prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1568    }
1569
1570    /// The backend's automatic capacity split for a mapped transformer.
1571    /// Kept as a method so prefill and decode use the exact same boundary.
1572    fn automatic_gpu_prefix(&self) -> Option<usize> {
1573        let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1574        crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1575    }
1576
1577    /// Positions per batched pass of the layer-stack prefill for THIS
1578    /// model on THIS backend (see [`prefill_chunk_rule`]). Pub: the network
1579    /// split must chunk exactly like the local path to reproduce it.
1580    pub fn prefill_chunk(&self) -> usize {
1581        let env = env_prefill_chunk();
1582        if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1583            return prefill_chunk_rule(env, ChunkHost::here(), false);
1584        }
1585        prefill_chunk_rule(
1586            None,
1587            ChunkHost::Other,
1588            self.chunk_stack_facts().dense_on_discrete(),
1589        )
1590    }
1591
1592    /// Architectures that run a dedicated per-token stack (gemma-3n's
1593    /// AltUp flow, the gemma-4 E-series PLE/shared-KV body): batched
1594    /// prefill and the graphs stay off for them.
1595    fn per_token_stack(&self) -> bool {
1596        self.g3n.is_some() || self.g4e.is_some()
1597    }
1598
1599    fn chunk_stack_facts(&self) -> ChunkStackFacts {
1600        let plain_dense = !self.weights.layers.is_empty()
1601            && !self.per_token_stack()
1602            && self.dsv4.is_none()
1603            && self.dsv41.is_none()
1604            && self.qwen4_exp.is_none()
1605            && self.weights.layers.iter().all(|lw| {
1606                // LFM2/LFM3's short-conv mixer is two projection GEMMs and
1607                // an elementwise conv: the host-driven chunk walk starves
1608                // the card the same way (LFM3-1.7B q8_2f on an A40, 2048
1609                // positions: 137 tok/s at 48, 460-640 at 512).
1610                matches!(lw.attn, AttnKind::Full { .. } | AttnKind::ShortConv(_))
1611                    && matches!(lw.ffn, FfnKind::Dense(_))
1612            });
1613        let gpu_on = crate::gpu::enabled();
1614        ChunkStackFacts {
1615            plain_dense,
1616            discrete: gpu_on && crate::gpu::discrete(),
1617            gpu_on,
1618            // Only asked when the rest already qualifies: it opens the
1619            // backend's capacity plan.
1620            capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1621                || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1622            multi_gpu: self.gpu_plan.is_some(),
1623            o1: self.o1_active(),
1624        }
1625    }
1626}
1627
1628/// Prefill chunk (positions per batched pass), model-agnostic form. On
1629/// macOS the AMX GEMM path wants tall panels — M=48 starves the matrix
1630/// units (ggml uses ubatch 512); elsewhere the historical 48 stays.
1631/// CMF_PREFILL_CHUNK overrides. The architectures with their own stacks
1632/// (DeepSeek-V4/V4.1) chunk with this; the layer-stack prefill asks
1633/// [`Pipeline::prefill_chunk`], which also knows the model and the card.
1634/// A different chunk is a different (equally valid) generation: panel
1635/// width reorders float accumulation.
1636pub fn prefill_chunk() -> usize {
1637    prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1638}
1639
1640fn env_prefill_chunk() -> Option<usize> {
1641    std::env::var("CMF_PREFILL_CHUNK")
1642        .ok()
1643        .and_then(|v| v.parse::<usize>().ok())
1644}
1645
1646/// The host classes the chunk width distinguishes.
1647#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1648enum ChunkHost {
1649    Macos,
1650    /// Linux/Android aarch64 (phones, SBCs).
1651    Aarch64,
1652    /// Everything else: x86-64 Linux/Windows, CPU or Vulkan/DX12.
1653    Other,
1654}
1655
1656impl ChunkHost {
1657    fn here() -> Self {
1658        if cfg!(target_os = "macos") {
1659            ChunkHost::Macos
1660        } else if cfg!(target_arch = "aarch64") {
1661            ChunkHost::Aarch64
1662        } else {
1663            ChunkHost::Other
1664        }
1665    }
1666}
1667
1668/// Chunk for a plain dense stack whose every layer lives on a discrete
1669/// card. On x86 the layer-stack prefill is host-driven: each GEMM and the
1670/// chunk attention (which re-uploads the whole KV prefix per layer) is a
1671/// separate submit + readback, so 48 positions a pass left the card idle
1672/// between them. Measured in-process on an RTX 3090 (Vulkan), 2048-token
1673/// prompt — see CHANGELOG 0.7.6 for the table.
1674const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1675
1676/// The chunk-width rule. `dense_on_discrete` is true only for a plain
1677/// dense transformer (full attention, dense FFN, no special stack) that
1678/// is entirely resident on one discrete card — the one case measured
1679/// here. GDN hybrids, MoE, DeepSeek stacks, capacity-split and CPU-only
1680/// runs keep the width they were tuned with.
1681fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1682    if let Some(n) = env {
1683        return n.max(1);
1684    }
1685    match host {
1686        ChunkHost::Macos => 512,
1687        // Mobile: big enough to feed the batched attend (gate b ≥ 32)
1688        // and the blocked SDOT GEMM without the memory of 512.
1689        ChunkHost::Aarch64 => 256,
1690        ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1691        ChunkHost::Other => 48,
1692    }
1693}
1694
1695/// What the chunk rule needs to know about a loaded stack.
1696#[derive(Clone, Copy, Debug, Default)]
1697struct ChunkStackFacts {
1698    /// Every layer is `AttnKind::Full` or `AttnKind::ShortConv` +
1699    /// `FfnKind::Dense`, and no architecture-owned stack (g3n,
1700    /// DeepSeek-V4/V4.1, qwen4-exp) is set.
1701    plain_dense: bool,
1702    /// The active GPU backend is a discrete card.
1703    discrete: bool,
1704    /// The backend is up and not paused.
1705    gpu_on: bool,
1706    /// A capacity-derived device prefix: some layers run on the host.
1707    capacity_split: bool,
1708    /// An in-process multi-GPU plan is set.
1709    multi_gpu: bool,
1710    /// O(1) layers (their Q trace is recorded by the prefill).
1711    o1: bool,
1712}
1713
1714impl ChunkStackFacts {
1715    fn dense_on_discrete(self) -> bool {
1716        self.plain_dense
1717            && self.discrete
1718            && self.gpu_on
1719            && !self.capacity_split
1720            && !self.multi_gpu
1721            && !self.o1
1722    }
1723}
1724
1725/// Number of prompt rows that have a real teacher-forced next-token pair in a
1726/// prefill span.  The final prompt row has no successor token, so it must not
1727/// be handed to the MTP warm-up.  Keeping this arithmetic in one helper makes
1728/// the full-chunk and tail-chunk boundaries explicit for both the graph and
1729/// CPU implementations.
1730#[inline]
1731fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1732    if end <= start || start >= input_len {
1733        return 0;
1734    }
1735    let rows = (end.min(input_len) - start).min(input_len - start);
1736    if end < input_len {
1737        rows
1738    } else {
1739        rows.saturating_sub(1)
1740    }
1741}
1742
1743/// Callback for streaming tokens. Return `false` to cancel.
1744pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1745
1746/// One layer's cache ownership at a cross-turn KV reuse boundary.
1747#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1748pub(crate) struct ReuseLayer {
1749    /// Exact-attention layer (rows in `LayerKvCache`); otherwise a
1750    /// recurrent / latent mixer whose state cannot be rewound.
1751    pub full: bool,
1752    /// Positions the host owner cache reaches (`pos_len`: a trimmed
1753    /// sliding tail stores only the newest of them).
1754    pub host_rows: usize,
1755    /// Rows the wgpu token graph's device mirror holds (None: no mirror).
1756    pub device_rows: Option<usize>,
1757    /// A recurrent state lives on the device (advanced past the host copy).
1758    pub device_state: bool,
1759}
1760
1761/// What a reused turn must do before its tail prefill runs on the HOST.
1762#[derive(Debug, Clone, PartialEq, Eq)]
1763pub(crate) enum ReusePlan {
1764    /// Host caches already hold exactly the reused prefix.
1765    Ready,
1766    /// Copy device mirror rows `[from..to)` into the host cache of each
1767    /// listed layer (the rows decode wrote on the device only).
1768    Pull(Vec<(usize, usize, usize)>),
1769    /// The prefix cannot be continued on the host exactly: start fresh.
1770    Fresh,
1771}
1772
1773/// The wgpu whole-token graph decodes into a DEVICE K/V mirror and never
1774/// writes those rows back to the host cache, while the chunked prefill of a
1775/// pure-attention model reads (and appends to) the host cache. A reused turn
1776/// therefore found its host cache ending at the previous PROMPT, not at the
1777/// previous answer: the tail prefill attended without the model's own
1778/// answer and appended its rows at the wrong index (MiniCPM5 on Vulkan
1779/// repeated its tool call instead of reading the tool result). Every layer
1780/// must hold exactly `reuse_from` host rows before the host continues; rows
1781/// that exist only on the device are pulled back, anything else is fresh.
1782pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1783    let mut pulls = Vec::new();
1784    for (li, l) in layers.iter().enumerate() {
1785        if !l.full {
1786            if l.device_state {
1787                return ReusePlan::Fresh;
1788            }
1789            continue;
1790        }
1791        if l.host_rows == reuse_from {
1792            continue;
1793        }
1794        if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1795            pulls.push((li, l.host_rows, reuse_from));
1796            continue;
1797        }
1798        return ReusePlan::Fresh;
1799    }
1800    if pulls.is_empty() {
1801        ReusePlan::Ready
1802    } else {
1803        ReusePlan::Pull(pulls)
1804    }
1805}
1806
1807impl Pipeline {
1808    /// Reset state whose meaning is local to a single MTP draft sequence.
1809    /// Unlike ordinary KV reuse, MTP is always rebuilt from the prompt.
1810    fn reset_fresh_mtp_draft_state(&mut self) {
1811        self.draft_full_streak = 0;
1812    }
1813
1814    /// Clear all per-sequence state, including backend device mirrors.
1815    ///
1816    /// The host KV/history buffers are only half of the request lifecycle on
1817    /// wgpu: GDN/O(1) state and cached graph bind groups are keyed by the
1818    /// pipeline id and otherwise survive a pooled request.  Keep every fresh
1819    /// sequence entry point on this one reset path so a new request cannot
1820    /// inherit the prior request's device state.
1821    fn clear_sequence_state(&mut self) {
1822        // a replay still writing the GDN owners must land before they are
1823        // cleared or reallocated (the device holds raw pointers to them)
1824        #[cfg(target_os = "macos")]
1825        let _ = crate::gpu_metal::wait_replay();
1826        self.kv_cache.clear();
1827        // Both reuse keys (the legacy `kv_history` and the bounded
1828        // `kv_prefix`) describe the state being dropped here.
1829        self.clear_history();
1830        self.graph_logits = None;
1831        if let Some(b) = &mut self.dsv41 {
1832            b.3.clear();
1833        }
1834        crate::gpu::graph_kv_reset(self.graph_kv_id);
1835        // MTP is detached from `self` for the duration of generation, so its
1836        // device mirror is not covered by the trunk reset above.  Reset the
1837        // derived id as well: a failed/aborted warm-up must never leave a
1838        // mirror that a later request can mistake for a current MTP cache.
1839        crate::gpu::graph_kv_reset(self.mtp_kv_id());
1840    }
1841
1842    /// Make the host caches own exactly the reused prefix `[0..reuse_from)`
1843    /// before a reused turn's tail prefill runs on the host (see
1844    /// [`kv_reuse_plan`]). Returns false when the prefix cannot be continued
1845    /// exactly — the caller then starts a fresh sequence. A model whose
1846    /// prefill runs through the token graph keeps its device state as the
1847    /// authority and is left untouched.
1848    fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1849        if self.graph_prefill_preferred() {
1850            return true;
1851        }
1852        let kv_id = self.graph_kv_id;
1853        let layers: Vec<ReuseLayer> = (0..self.num_layers)
1854            .map(|li| {
1855                let full = matches!(
1856                    self.weights.layers[self.phys_layer(li)].attn,
1857                    AttnKind::Full { .. }
1858                );
1859                ReuseLayer {
1860                    full,
1861                    // Absolute depth: a trimmed sliding tail stores fewer
1862                    // rows than the positions it has seen.
1863                    host_rows: self.kv_cache.layers[li].pos_len(),
1864                    device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1865                    device_state: crate::gpu::graph_state_resident(kv_id, li),
1866                }
1867            })
1868            .collect();
1869        // No wgpu device state at all (CPU, Metal — whose graph appends every
1870        // decoded row to the owner cache itself): the host is the owner and
1871        // the extension check already proved the prefix.
1872        if layers
1873            .iter()
1874            .all(|l| l.device_rows.is_none() && !l.device_state)
1875        {
1876            return true;
1877        }
1878        let plan = kv_reuse_plan(reuse_from, &layers);
1879        let (what, rows, n) = match &plan {
1880            ReusePlan::Ready => ("host ready", 0, 0),
1881            ReusePlan::Fresh => ("fresh", 0, 0),
1882            ReusePlan::Pull(p) => (
1883                "pull",
1884                p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1885                p.len(),
1886            ),
1887        };
1888        let t0 = std::time::Instant::now();
1889        let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1890        if std::env::var("CMF_PREFILL_PROF").is_ok() {
1891            eprintln!(
1892                "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1893                if ok { "" } else { " (failed → fresh)" },
1894                t0.elapsed().as_secs_f64() * 1e3
1895            );
1896        }
1897        ok
1898    }
1899
1900    fn apply_kv_reuse_plan(
1901        &mut self,
1902        reuse_from: usize,
1903        plan: ReusePlan,
1904        layers: &[ReuseLayer],
1905    ) -> bool {
1906        let kv_id = self.graph_kv_id;
1907        match plan {
1908            ReusePlan::Fresh => return false,
1909            ReusePlan::Ready => {}
1910            ReusePlan::Pull(pulls) => {
1911                // Mirrors of one uniform geometry: one batched read serves
1912                // every layer. Per-layer geometry (MiMo-V2: 4/8 KV heads,
1913                // narrow V, sliding rings) is read layer by layer in the
1914                // host layout instead.
1915                let (nkv, hd) = {
1916                    let c = &self.kv_cache.layers[pulls[0].0];
1917                    (c.num_kv_heads, c.head_dim)
1918                };
1919                let uniform = pulls.iter().all(|&(li, _, _)| {
1920                    let c = &self.kv_cache.layers[li];
1921                    (c.num_kv_heads, c.head_dim) == (nkv, hd)
1922                });
1923                let batched = if uniform {
1924                    crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1925                } else {
1926                    None
1927                };
1928                let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1929                    Some(rows) => rows,
1930                    None => {
1931                        let mut rows = Vec::with_capacity(pulls.len());
1932                        for &(li, from, to) in &pulls {
1933                            let (lnkv, lhd) = {
1934                                let c = &self.kv_cache.layers[li];
1935                                (c.num_kv_heads, c.head_dim)
1936                            };
1937                            let Some((k, v, first_valid)) =
1938                                crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1939                            else {
1940                                return false;
1941                            };
1942                            // The host continues at `to`: a sliding layer
1943                            // reads back only its last window, a full one
1944                            // every row it lacks.
1945                            let need_from = match self.layer_window(li) {
1946                                Some(w) => from.max((to + 1).saturating_sub(w)),
1947                                None => from,
1948                            };
1949                            if first_valid > need_from {
1950                                return false;
1951                            }
1952                            rows.push((k, v));
1953                        }
1954                        rows
1955                    }
1956                };
1957                for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1958                    let cache = &mut self.kv_cache.layers[li];
1959                    let row = cache.num_kv_heads * cache.head_dim;
1960                    for p in 0..to - from {
1961                        cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1962                    }
1963                    if cache.pos_len() != to {
1964                        return false;
1965                    }
1966                }
1967            }
1968        }
1969        // A mirror past the prefix (a greedy burst that ran beyond the stop)
1970        // holds rows of the OLD continuation: rewind it so the next graph
1971        // token re-syncs those positions from the host.
1972        for (li, l) in layers.iter().enumerate() {
1973            if l.full
1974                && l.device_rows.is_some_and(|d| d > reuse_from)
1975                && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1976            {
1977                return false;
1978            }
1979        }
1980        true
1981    }
1982
1983    /// Finish a generation lifecycle after the MTP/router owners were
1984    /// detached.  Every terminal path must put those owners back before the
1985    /// pooled pipeline can serve another request.  Graph side channels and
1986    /// device mirrors are cleared on errors and cancellations; a successful
1987    /// generation keeps its decode-ready host cache for KV reuse.
1988    fn finish_generation(
1989        &mut self,
1990        mtp: &mut Option<MtpModule>,
1991        router: &mut Option<crate::swarm::DynRouter>,
1992        clear_sequence: bool,
1993    ) {
1994        // A dynamic route may have switched the overlay before the terminal
1995        // path. Restore the backbone while the detached router is still
1996        // available, because set_active_skill also owns the overlay reset.
1997        if router.is_some() {
1998            let _ = self.set_active_skill(None);
1999        }
2000        // The last speculative round's replay may still be in flight on
2001        // the second queue: whoever reads the host cache after generate()
2002        // returns (session export, the network split's KV wire, a KV
2003        // reuse) must see the final states.
2004        // A replay that failed leaves the GDN owners half-written: fail
2005        // closed and drop the sequence instead of handing the cache on.
2006        #[cfg(target_os = "macos")]
2007        let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
2008        if clear_sequence {
2009            self.clear_sequence_state();
2010            if let Some(m) = mtp.as_mut() {
2011                // The MTP owner is detached while generation runs, so the
2012                // trunk reset above cannot clear its host cache.  Drop its
2013                // partial rows before reattaching it to the pooled pipeline;
2014                // the next request must start from the same empty anchor on
2015                // CPU and on the device mirror.
2016                m.kv.clear();
2017            }
2018            if let Some(m) = self.mtp.as_mut() {
2019                // A non-speculative request leaves the configured MTP owner
2020                // attached.  Clear that dormant cache too when a shared
2021                // generation failure/cancellation resets the sequence.
2022                m.kv.clear();
2023            }
2024        }
2025        self.graph_want_logits = false;
2026        self.graph_head_required = false;
2027        self.graph_logits = None;
2028        self.graph_failed
2029            .store(false, std::sync::atomic::Ordering::Relaxed);
2030        self.cancel
2031            .store(false, std::sync::atomic::Ordering::Relaxed);
2032        self.dyn_router = router.take().or(self.dyn_router.take());
2033        self.mtp = mtp.take().or(self.mtp.take());
2034        self.mtp_graph_mode = None;
2035        self.spec_forced = None;
2036    }
2037
2038    /// Consume a graph failure reported by a forward that returns only a
2039    /// hidden vector.  `forward_ids` is a public Result API, so it must not
2040    /// turn the graph's zero hidden sentinel into a valid lm_head result.
2041    fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2042        if self
2043            .graph_failed
2044            .swap(false, std::sync::atomic::Ordering::Relaxed)
2045        {
2046            self.cancel
2047                .store(false, std::sync::atomic::Ordering::Relaxed);
2048            self.clear_sequence_state();
2049            self.graph_logits = None;
2050            self.graph_want_logits = false;
2051            self.graph_head_required = false;
2052            return Err(format!("GPU graph failed during {phase} at position {pos}"));
2053        }
2054        Ok(())
2055    }
2056
2057    #[cfg(target_os = "macos")]
2058    fn fail_metal_graph(&mut self, reason: &str) {
2059        crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2060        self.clear_sequence_state();
2061        self.graph_logits = None;
2062        self.graph_failed
2063            .store(true, std::sync::atomic::Ordering::Relaxed);
2064        self.cancel
2065            .store(true, std::sync::atomic::Ordering::Relaxed);
2066        tracing::error!("native Metal TokenGraph failed closed: {reason}");
2067    }
2068
2069    /// Start an NLL/PPL request with all graph side channels in a known
2070    /// state.  A graph failure also raises the cooperative cancel bit; it is
2071    /// consumed here and that graph-induced bit is cleared so an independent
2072    /// request can be reused.  A caller-owned cancellation remains intact.
2073    fn nll_begin(&mut self) -> Result<(), String> {
2074        if self
2075            .graph_failed
2076            .swap(false, std::sync::atomic::Ordering::Relaxed)
2077        {
2078            self.cancel
2079                .store(false, std::sync::atomic::Ordering::Relaxed);
2080            self.clear_sequence_state();
2081            self.graph_logits = None;
2082            self.graph_want_logits = false;
2083            self.graph_head_required = false;
2084            return Err("GPU graph failed before NLL scoring".to_string());
2085        }
2086        self.clear_sequence_state();
2087        self.graph_logits = None;
2088        self.graph_want_logits = false;
2089        self.graph_head_required = false;
2090        Ok(())
2091    }
2092
2093    /// End an NLL/PPL request, including the side channels that are not part
2094    /// of the host KV cache.  This is intentionally explicit instead of
2095    /// relying on a tuple/sentinel return: callers must see every failure.
2096    fn nll_end(&mut self) {
2097        self.clear_sequence_state();
2098        self.graph_logits = None;
2099        self.graph_want_logits = false;
2100        self.graph_head_required = false;
2101        self.graph_failed
2102            .store(false, std::sync::atomic::Ordering::Relaxed);
2103    }
2104
2105    /// Check the graph failure channel at a scoring boundary and leave the
2106    /// pipeline reusable when the device path failed.
2107    fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2108        #[cfg(test)]
2109        if self.nll_test_fail_at == Some(pos) {
2110            self.nll_test_fail_at = None;
2111            self.graph_failed
2112                .store(true, std::sync::atomic::Ordering::Relaxed);
2113            self.cancel
2114                .store(true, std::sync::atomic::Ordering::Relaxed);
2115        }
2116        if self
2117            .graph_failed
2118            .swap(false, std::sync::atomic::Ordering::Relaxed)
2119        {
2120            self.cancel
2121                .store(false, std::sync::atomic::Ordering::Relaxed);
2122            self.clear_sequence_state();
2123            self.graph_logits = None;
2124            self.graph_want_logits = false;
2125            return Err(format!(
2126                "GPU graph failed during NLL {phase} at position {pos}"
2127            ));
2128        }
2129        Ok(())
2130    }
2131
2132    /// Map a virtual layer index to its physical weight index.
2133    /// Looped Transformer (Nanbeige 4.2): 22 physical layers × 2 loops = 44 virtual;
2134    /// virtual layer 23 maps back to physical layer 1 (23 % 22 = 1).
2135    #[inline]
2136    pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2137        virtual_idx % self.physical_layers
2138    }
2139
2140    /// True when `virtual_idx` is the last layer of a loop iteration
2141    /// (used for loop_final_norm insertion).
2142    #[inline]
2143    pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2144        self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2145    }
2146
2147    /// Build a pipeline from parts (used by the loader and tests).
2148    #[allow(clippy::too_many_arguments)]
2149
2150    /// Whole-block q1 token graph on the GPU (macOS/Metal): the run of
2151    /// consecutive q1 layers — GDN *and* full attention — starting at
2152    /// `start` executes as few command buffers as the CPU truly needs.
2153    /// Hidden stays device-resident across every layer; the only syncs
2154    /// are before each CPU attend (it needs q/k/v and owns the KV
2155    /// cache) and the final hidden readback. Recurrent states
2156    /// round-trip through shared memory (the CPU stays their owner, so
2157    /// every other path remains coherent). Returns the first layer
2158    /// index NOT covered (== `start` → refused, caller falls through
2159    /// to the per-layer CPU path).
2160    /// Should prefill run position-by-position through the GPU token
2161    /// graph instead of the batched CPU chunk-GEMM? True for q1 GDN
2162    /// hybrids on native Metal: their chunk prefill is walled by the
2163    /// sequential scalar recurrence, so the graph's decode rate wins.
2164    /// NOT for Looped Transformers, despite the per-chunk loop_final_norm
2165    /// sync: the chunk-GEMM amortizes each weight over the whole chunk,
2166    /// which the per-position graph cannot (Nanbeige 4.2 on M4, 512-token
2167    /// prompt: 85 tok/s chunked vs 14 through the graph).
2168    #[cfg(target_os = "macos")]
2169    fn graph_prefill_preferred(&self) -> bool {
2170        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2171        if !crate::gpu::enabled_here()
2172            || !graph_force
2173            || std::env::var("CMF_GPU_BLOCK")
2174                .map(|v| v == "0")
2175                .unwrap_or(false)
2176            // CMF_PREFILL_GRAPH=0: the chunked prefill (GEMM projections,
2177            // CPU recurrence) instead of the per-position token graph.
2178            || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2179        {
2180            return false;
2181        }
2182        self.weights
2183            .layers
2184            .iter()
2185            .any(|lw| {
2186                matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2187            })
2188    }
2189
2190    /// Prompt ingest through the batched wgpu graph in device-prefix mode:
2191    /// a MoE stack that does not fit the card runs each chunk's leading
2192    /// layers on the device (experts resident) and the rest on the host's
2193    /// batched walk. On by default for models with per-layer attention
2194    /// geometry (MiMo-V2 — its measured default); `CMF_BATCH_PREFIX=1`
2195    /// opts any other MoE model in, `=0` keeps the chunked host prefill.
2196    #[cfg(not(target_os = "macos"))]
2197    fn batch_prefix_prefill(&self) -> bool {
2198        let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2199            Ok("0") => return false,
2200            Ok("1") => true,
2201            _ => false,
2202        };
2203        crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2204            && crate::gpu::enabled_here()
2205            && !self.graph_refused()
2206            && (forced || self.graph_attn_decline_reason().is_some())
2207            && self.wgpu_graph_attn_decline().is_none()
2208            && self.attn_softcap == 0.0
2209            && self
2210                .weights
2211                .layers
2212                .iter()
2213                .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2214            && self.automatic_gpu_prefix().is_some()
2215    }
2216
2217    /// Gemma-4's prompt through the batched wgpu graph (`gemma_batch`): a
2218    /// dense stack wholly on a discrete card, whose token graph already
2219    /// decodes it. Not `graph_prefill_preferred` — the host stays the KV
2220    /// owner for reuse and scoring; only generation's ingest changes.
2221    /// `CMF_G4_BATCH=0` keeps the chunked host prefill.
2222    fn g4_batch_prefill(&self) -> bool {
2223        #[cfg(target_os = "macos")]
2224        {
2225            false
2226        }
2227        #[cfg(not(target_os = "macos"))]
2228        {
2229            // An EMPTY layer stack (`all()` is vacuously true) plus a
2230            // gemma4 geometry flag is exactly the gemma-4 E-series
2231            // per-token stack — it has no batch graph rows to ingest.
2232            !self.weights.layers.is_empty()
2233                && !self.per_token_stack()
2234                && (self.global_attn.is_some() || self.attn_v_norm)
2235                && crate::gpu::g4_batch_on()
2236                && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2237                && crate::gpu::enabled_here()
2238                && crate::gpu::discrete()
2239                && !self.graph_refused()
2240                && !self.o1_active()
2241                && self.attn_softcap == 0.0
2242                && self.wgpu_graph_attn_decline().is_none()
2243                && self.weights.layers.iter().all(|lw| {
2244                    matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2245                })
2246                && self.automatic_gpu_prefix().is_none()
2247        }
2248    }
2249
2250    /// Gemma-4's batched prefill of `ids` at positions `pos0..`
2251    /// (`g4_batch_prefill`): chunks of `generation_batch_k` rows through the
2252    /// batch graph. Ok((n, rows)): `n` positions ingested; `rows` = the last
2253    /// one's hidden, or every row's (`keep_all`). A refusal stops early
2254    /// (n < ids.len()) after pulling the device rows the host walk needs.
2255    /// Err: a failure after admission — the device state is undefined.
2256    #[cfg(not(target_os = "macos"))]
2257    fn g4_batch_ingest(
2258        &mut self,
2259        ids: &[u32],
2260        pos0: usize,
2261        keep_all: bool,
2262    ) -> Result<(usize, Vec<f32>), String> {
2263        let hs = self.hidden_size;
2264        let chunk = self.generation_batch_k().max(1);
2265        let end_all = pos0 + ids.len();
2266        let mut pos = pos0;
2267        let mut out = Vec::new();
2268        while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2269            let end = (pos + chunk).min(end_all);
2270            let bk = end - pos;
2271            let mut hiddens = vec![0f32; bk * hs];
2272            for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2273                hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2274            }
2275            let positions: Vec<usize> = (pos..end).collect();
2276            let t_chunk = std::time::Instant::now();
2277            let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2278            if std::env::var("CMF_GRAPH_PROF").is_ok() {
2279                let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2280                eprintln!(
2281                    "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2282                    bk as f64 / (ms / 1000.0)
2283                );
2284            }
2285            match outcome {
2286                crate::gpu::BatchGraphOutcome::Completed => {
2287                    if keep_all {
2288                        out.extend_from_slice(&hiddens);
2289                    } else {
2290                        out = hiddens[(bk - 1) * hs..].to_vec();
2291                    }
2292                    pos = end;
2293                }
2294                crate::gpu::BatchGraphOutcome::Failed => {
2295                    return Err("Gemma-4 batched prefill failed after admission".into());
2296                }
2297                crate::gpu::BatchGraphOutcome::Declined => {
2298                    #[cfg(feature = "gpu")]
2299                    if pos > pos0 {
2300                        self.pull_lagging_host_kv(0, self.num_layers, pos);
2301                    }
2302                    break;
2303                }
2304            }
2305        }
2306        Ok((pos - pos0, out))
2307    }
2308
2309    /// Hidden rows of every position of `ids` through Gemma-4's batched
2310    /// prefill from a fresh sequence (the prefill's teacher-forced logits
2311    /// for parity gates: `logits_from_hidden` per row). None when the model
2312    /// or the card does not take that route, or it refused part way.
2313    pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2314        #[cfg(not(target_os = "macos"))]
2315        {
2316            if ids.is_empty() || !self.g4_batch_prefill() {
2317                return None;
2318            }
2319            self.clear_sequence_state();
2320            match self.g4_batch_ingest(ids, 0, true) {
2321                Ok((n, rows)) if n == ids.len() => Some(rows),
2322                _ => {
2323                    self.clear_sequence_state();
2324                    None
2325                }
2326            }
2327        }
2328        #[cfg(target_os = "macos")]
2329        {
2330            let _ = ids;
2331            None
2332        }
2333    }
2334
2335    #[cfg(not(target_os = "macos"))]
2336    fn graph_prefill_preferred(&self) -> bool {
2337        // Discrete-GPU wgpu whole-token graph: GDN layers carry recurrent state
2338        // (conv ring + delta-rule S) resident on the GPU. A batched CPU prefill
2339        // builds that state on the CPU only, leaving the GPU buffers zeroed at
2340        // decode → garbage. Route GDN-hybrid prefill through the graph one
2341        // position at a time so the resident state is seeded exactly as decode
2342        // will read it. Pure-attention models keep the batched CPU prefill (its
2343        // KV mirror re-syncs from the CPU cache, so no seeding gap).
2344        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2345        if !graph_on || !crate::gpu::enabled_here() {
2346            return false;
2347        }
2348        // Embryo's phase state is device-owned by the resident graph during
2349        // prefill; the batched CPU path would leave decode seeing a zeroed
2350        // device recurrence.  Route the prompt position-by-position too.
2351        if self.embryo_resident_eligible() {
2352            // The resident graph owns the phase state and anchor KV on the
2353            // device.  A prefill-only graph would leave decode on the host
2354            // with no way to import that state, so keep the whole sequence
2355            // on one owner (or use the ordinary CPU prefill/decode pair).
2356            return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2357        }
2358        // The descriptor-aware Prism graph now carries both the FWHT/affine
2359        // transforms and resident GDN state, so it is also the exact prefill
2360        // path for this model.  Keeping it here (rather than falling through
2361        // to the CPU chunk walk) is required for a long prompt to seed the
2362        // same device state that decode consumes.
2363        // O(1) needs the CPU prefill: the q-trace that seals the Nyström
2364        // skeleton is recorded there and nowhere else. The GDN half of
2365        // the hybrid loses nothing — the graph's first decode creates
2366        // its (ring, S) entries seeded from `cpu_state`, the same
2367        // handoff every graph run relies on when the entry is fresh.
2368        // Without this line the two designs collide on hybrids and o1
2369        // never becomes graph-portable: prefill through the graph
2370        // records no trace, so views stay None forever.
2371        if self.o1_active() {
2372            return false;
2373        }
2374        // A model the wgpu graphs decline outright would walk its prompt
2375        // one position at a time through a graph that never runs: the
2376        // batched CPU chunk prefill is the right ingest for it.
2377        if self.wgpu_graph_attn_decline().is_some() {
2378            return false;
2379        }
2380        if self
2381            .weights
2382            .layers
2383            .iter()
2384            .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2385        {
2386            return true;
2387        }
2388        // MoE models too: the chunked CPU prefill runs every expert on the
2389        // host (Hy-MT2-30B-A3B on a Xeon: 8 tok/s of ingest against 53 of
2390        // graph decode), while the token graph — and the batched graph under
2391        // CMF_BATCH_K — keep the experts resident. Full attention in the
2392        // graph writes the KV mirror that decode reads, exactly as it does
2393        // for the hybrids' attention layers. Only when the whole stack is
2394        // resident: with a device prefix the per-position walk finishes
2395        // every token on the host, and the chunked prefill (GEMMs on the
2396        // card, the expert loop batched on the host) is the faster ingest
2397        // (the 8 GB ladder point: 7 tok/s chunked against ~1 walked).
2398        self.weights
2399            .layers
2400            .iter()
2401            .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2402            && self.automatic_gpu_prefix().is_none()
2403    }
2404
2405    #[cfg(target_os = "macos")]
2406    fn q1_graph_gpu(
2407        &mut self,
2408        start: usize,
2409        upto: Option<usize>,
2410        position: usize,
2411        h: &mut [f32],
2412    ) -> usize {
2413        let _mt0 = std::time::Instant::now(); // CMF_METAL_HOSTPROF
2414        use crate::gpu::{
2415            AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, SconvGpuLayer, TokenGraph,
2416        };
2417        const MOE_WORKING_SET_REASON: &str =
2418            "model weights exceed Metal working set for native MoE graph";
2419        // A prior admission check rejected this pipeline. Avoid rebuilding the
2420        // entire 40-layer graph plan at every individual host layer after the
2421        // fallback begins; the per-operation path below stays eligible.
2422        if self
2423            .graph_declines
2424            .borrow()
2425            .iter()
2426            .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2427        {
2428            return start;
2429        }
2430        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2431        if self.attn_softcap > 0.0 // capped scores: no graph kernel — CPU path
2432            || !self.metal_rope_scale_ok()
2433            || !crate::gpu::enabled_here()
2434            || !graph_force
2435            || std::env::var("CMF_GPU_BLOCK")
2436                .map(|v| v == "0")
2437                .unwrap_or(false)
2438        {
2439            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2440                eprintln!(
2441                    "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2442                    self.attn_softcap > 0.0,
2443                    crate::gpu::enabled_here(),
2444                    graph_force,
2445                );
2446            }
2447            if self.graph_head_required {
2448                self.fail_metal_graph("native graph front gate refused");
2449            }
2450            return start;
2451        }
2452        // The graph encodes SiLU or exact-GELU FFNs and attention with an
2453        // explicit model scale. Sliding-window layers with their own RoPE
2454        // table / rotary width ride it too (`metal_graph_swa`): both the
2455        // device attend and the sandwich's CPU attend read each layer's
2456        // window and table. Sandwich norms and other activations fall back
2457        // to the CPU path.
2458        // Gemma-4 (`metal_gemma4`): its sliding layers ride the same door,
2459        // its global layers, V norm, sandwich norms, layer scalar and
2460        // tanh-GELU the `gpu_metal::gemma4` kernels.
2461        let gemma4 = self.metal_gemma4();
2462        let swa_graph = self.metal_graph_swa() || gemma4;
2463        // The normal native GQA kernel carries a learned, value-less
2464        // softmax column.  It also takes the exact per-layer sliding
2465        // window, but its split-K twin has no sink column.  Thus a
2466        // sink-bearing model may use this door only when the ordinary
2467        // sliding geometry has passed `metal_graph_swa` (or when it has no
2468        // sliding layers at all).
2469        let graph_attn_reason = self.graph_attn_decline_reason();
2470        let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2471            && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2472        // Plain-weight (x̂·w) sandwich norms (LFM3) take the generic norm
2473        // kernels, as on the wgpu graph; Gemma-2/3's (1+w) norms and the
2474        // layer scalar stay Gemma-4's.
2475        let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
2476        if (self.swa.is_some() && !swa_graph)
2477            || (self.global_attn.is_some() && !gemma4)
2478            || self.attention_heads_per_layer.is_some()
2479            || (self.attn_v_norm && !gemma4)
2480            // Per-layer KV heads / narrow V stay outside this graph.  A
2481            // learned sink is admitted only through native_sink_attend.
2482            || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2483            || self.weights.layers.iter().any(|lw| {
2484                ((lw.layer_scale.is_some()
2485                    || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some())))
2486                    && !gemma4)
2487                    || matches!(&lw.ffn, FfnKind::Dense(d)
2488                        if !(matches!(d.act, Act::Silu | Act::Gelu)
2489                            || (gemma4 && d.act == Act::GeluTanh)))
2490            })
2491        {
2492            // The Metal graphs' device attend has no per-layer attention
2493            // geometry (the wgpu graphs do): say so once, by name.
2494            if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2495                self.note_graph_decline("metal block graph", reason);
2496            }
2497            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2498                eprintln!(
2499                    "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2500                    self.swa.is_some(),
2501                    self.global_attn.is_some(),
2502                    self.attention_heads_per_layer.is_some(),
2503                    self.attn_v_norm,
2504                    (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2505                );
2506            }
2507            if self.graph_head_required {
2508                self.fail_metal_graph("native graph architecture gate refused");
2509            }
2510            return start;
2511        }
2512        // Looped Transformer: the graph covers ALL loop iterations;
2513        // encode_loop_norm is inserted on-device at each boundary.
2514        let limit = upto
2515            .map(|u| u + 1)
2516            .unwrap_or(self.num_layers)
2517            .min(self.num_layers);
2518
2519        enum Item<'a> {
2520            Gdn {
2521                run: Vec<GdnGpuLayer<'a>>,
2522                first: usize,
2523            },
2524            /// A run of LFM2/LFM3 short-conv layers (conv ring on device).
2525            Sconv {
2526                run: Vec<SconvGpuLayer<'a>>,
2527                first: usize,
2528            },
2529            Attn {
2530                l: AttnGpuLayer<'a>,
2531                li: usize,
2532                q_norm: Option<&'a [f32]>,
2533                k_norm: Option<&'a [f32]>,
2534                output_gate: bool,
2535                /// `self_attn.g_proj` output gate (per-head flag): the
2536                /// sandwich projects the device-normed input on the host
2537                /// and gates the attend's output before O.
2538                proj_gate: Option<(&'a QTensor, bool)>,
2539                /// The same gate's f32 rows `[nh × hidden]` when the device
2540                /// attend can apply it (per-head sigmoid, f32 in RAM).
2541                head_gate_w: Option<&'a [f32]>,
2542                bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2543                /// Attend on the device too (no sync): F32 KV, no
2544                /// o1/bias, dims inside the kernels' contract.
2545                full_gpu: bool,
2546            },
2547        }
2548
2549        // Device-attend KERNEL contract, shared by every Full layer. The
2550        // hd>128 default-off POLICY is applied after the scan: it was
2551        // measured on dense models, and a MoE plan inverts it — with the
2552        // experts on device each CPU-attend sandwich costs a
2553        // commit+wait, ~30 submits/token (W2 on M4: 14.7 tok/s
2554        // sandwiched vs 27.1 device-attend vs 18.8 pure CPU).
2555        let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2556        let attend_contract = attend_mode != "0"
2557            && attend_mode != "off"
2558            && self.head_dim % 4 == 0
2559            && self.head_dim <= 256
2560            // Flint has explicit no-RoPE layers.  The device RoPE kernel
2561            // accepts rd=0 (zero rotation lanes) and binds a sentinel table;
2562            // retain the historical lane-pair requirement for nonzero rd.
2563            && (self.rotary_dim == 0
2564                || (self.rotary_dim >= 2
2565                    && self.rotary_dim <= self.head_dim
2566                    && (self.rotary_dim / 2) % 32 == 0))
2567            && self.num_kv_heads > 0
2568            && self.num_heads % self.num_kv_heads == 0;
2569
2570        let mut plan: Vec<Item> = Vec::new();
2571        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2572        // Break-reason diagnostics ride the same env as the plan summary.
2573        let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2574        let mut scan = start;
2575        while scan < limit {
2576            let lw = &self.weights.layers[self.phys_layer(scan)];
2577            let ffn = match &lw.ffn {
2578                FfnKind::Dense(d) if d.segs.is_empty() => {
2579                    let (Some(g), Some(u), Some(dn)) = (
2580                        d.gate_proj.metal_graph_parts(),
2581                        d.up_proj.metal_graph_parts(),
2582                        d.down_proj.metal_graph_parts(),
2583                    ) else {
2584                        if block_diag {
2585                            eprintln!(
2586                                "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2587                            );
2588                        }
2589                        break;
2590                    };
2591                    MetalFfn::Dense {
2592                        gate: g,
2593                        up: u,
2594                        down: dn,
2595                        // The front gate admitted SiLU, exact GELU and
2596                        // (Gemma-4) tanh-GELU only.
2597                        act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2598                    }
2599                }
2600                FfnKind::Moe(m) => {
2601                    // Flint's detached Q4 shared expert is intentionally
2602                    // tried before the generic uniform-bank graph builder.
2603                    // The latter correctly refuses its ragged Q2/Q4 widths;
2604                    // this narrow descriptor keeps the router and selected
2605                    // routed experts device-resident inside a bounded page.
2606                    let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2607                    {
2608                        MetalFfn::FlintMoe(flint)
2609                    } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2610                        MetalFfn::Moe(moe)
2611                    } else {
2612                        if block_diag {
2613                            eprintln!(
2614                                "block-graph: L{scan} MoE outside the graph contract — run ends"
2615                            );
2616                        }
2617                        break;
2618                    };
2619                    if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2620                        model_ref.get_or_insert_with(|| model.clone());
2621                    }
2622                    ffn
2623                }
2624                _ => {
2625                    if block_diag {
2626                        eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2627                    }
2628                    break;
2629                }
2630            };
2631            match &lw.attn {
2632                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2633                    let parts = (
2634                        w.in_proj_qkv.metal_graph_parts(),
2635                        w.in_proj_z.metal_graph_parts(),
2636                        w.in_proj_a.f32_parts(),
2637                        w.in_proj_b.f32_parts(),
2638                        w.out_proj.metal_graph_parts(),
2639                    );
2640                    let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2641                        if block_diag {
2642                            eprintln!(
2643                                "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2644                                w.in_proj_qkv.metal_graph_parts().is_some(),
2645                                w.in_proj_z.metal_graph_parts().is_some(),
2646                                w.in_proj_a.f32_parts().is_some(),
2647                                w.in_proj_b.f32_parts().is_some(),
2648                                w.out_proj.metal_graph_parts().is_some(),
2649                            );
2650                        }
2651                        break;
2652                    };
2653                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2654                        model_ref.get_or_insert_with(|| model.clone());
2655                    }
2656                    let gl = GdnGpuLayer {
2657                        attn_norm: &lw.input_norm,
2658                        post_norm: &lw.post_norm,
2659                        qkv,
2660                        z,
2661                        a,
2662                        b,
2663                        out,
2664                        ffn,
2665                        conv1d: &w.conv1d,
2666                        a_log: &w.a_log,
2667                        dt_bias: &w.dt_bias,
2668                        gnorm: &w.norm,
2669                    };
2670                    match plan.last_mut() {
2671                        Some(Item::Gdn { run, .. }) => run.push(gl),
2672                        _ => plan.push(Item::Gdn {
2673                            run: vec![gl],
2674                            first: scan,
2675                        }),
2676                    }
2677                }
2678                AttnKind::ShortConv(w) if self.short_conv_cfg.is_some() => {
2679                    let (Some(inp), Some(out)) =
2680                        (w.in_proj.metal_graph_parts(), w.out_proj.metal_graph_parts())
2681                    else {
2682                        if block_diag {
2683                            eprintln!("block-graph: L{scan} short-conv parts refused");
2684                        }
2685                        break;
2686                    };
2687                    if let QTensor::Mapped { model, .. } = &w.in_proj {
2688                        model_ref.get_or_insert_with(|| model.clone());
2689                    }
2690                    let sl = SconvGpuLayer {
2691                        attn_norm: &lw.input_norm,
2692                        post_norm: &lw.post_norm,
2693                        in_proj: inp,
2694                        taps: &w.conv,
2695                        out,
2696                        attn_out_norm: lw.attn_out_norm.as_deref(),
2697                        ffn_out_norm: lw.ffn_out_norm.as_deref(),
2698                        ffn,
2699                    };
2700                    match plan.last_mut() {
2701                        Some(Item::Sconv { run, .. }) => run.push(sl),
2702                        _ => plan.push(Item::Sconv {
2703                            run: vec![sl],
2704                            first: scan,
2705                        }),
2706                    }
2707                }
2708                AttnKind::Full {
2709                    wq,
2710                    wk,
2711                    wv,
2712                    wo,
2713                    q_norm,
2714                    k_norm,
2715                    output_gate,
2716                    softplus_gate,
2717                    bias,
2718                } if (!self.kv_cache.layers[scan].o1_sealed()
2719                    // Sealed o1 stays plannable when the Metal o1 port
2720                    // is on: full_gpu attends through the device state,
2721                    // and any refusal falls to the sandwich, whose CPU
2722                    // core routes sealed layers through the nystrom step.
2723                    || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2724                    // A projected output gate: Spark-X2.5's per-head
2725                    // sigmoid form only (Laguna's softplus gate is
2726                    // unmeasured on these paths and keeps the CPU walk).
2727                    && softplus_gate
2728                        .as_ref()
2729                        .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2730                {
2731                    let parts = (
2732                        wq.metal_graph_parts(),
2733                        wk.metal_graph_parts(),
2734                        wv.metal_graph_parts(),
2735                        wo.metal_graph_parts(),
2736                    );
2737                    let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2738                        break;
2739                    };
2740                    if let QTensor::Mapped { model, .. } = wq {
2741                        model_ref.get_or_insert_with(|| model.clone());
2742                    }
2743                    let cache = &self.kv_cache.layers[scan];
2744                    // O(1) layer on Metal: the device attends through the
2745                    // sealed Nystrom state (opt-in while the port proves
2746                    // itself). Unsealed -> sandwich path = the CPU o1 step.
2747                    let o1_metal = cache.o1.is_some()
2748                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2749                        && cache.o1_views().is_some();
2750                    // The device attend takes the layer's window and RoPE
2751                    // table, and the per-head gate from f32 rows; a gate
2752                    // held otherwise sandwiches.
2753                    let head_gate_w = softplus_gate
2754                        .as_ref()
2755                        .and_then(|(g, _)| g.f32_parts())
2756                        .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2757                        .map(|(d, _, _)| d);
2758                    // Gemma-4's global layers are 512 wide over one KV head
2759                    // (the `gemma4` 16-slot kernels, `CMF_METAL_GEMMA4_W16`).
2760                    let (nkv_g, hd_g) = if gemma4 {
2761                        let (a, b, _) = self.layer_geom(scan);
2762                        (a, b)
2763                    } else {
2764                        (self.num_kv_heads, self.head_dim)
2765                    };
2766                    let full_gpu = attend_contract
2767                        && softplus_gate.is_none() == head_gate_w.is_none()
2768                        && cache.mode == crate::kv_cache::KvMode::F32
2769                        && (cache.o1.is_none() || o1_metal)
2770                        && bias.is_none()
2771                        && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2772                        && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2773                        && pk.1 == nkv_g * hd_g
2774                        && pv.1 == nkv_g * hd_g
2775                        && po.2 == self.num_heads * hd_g;
2776                    plan.push(Item::Attn {
2777                        l: AttnGpuLayer {
2778                            attn_norm: &lw.input_norm,
2779                            post_norm: &lw.post_norm,
2780                            wq: pq,
2781                            wk: pk,
2782                            wv: pv,
2783                            wo: po,
2784                            ffn,
2785                            attn_out_norm: lw.attn_out_norm.as_deref(),
2786                            ffn_out_norm: lw.ffn_out_norm.as_deref(),
2787                            layer_scale: lw.layer_scale,
2788                        },
2789                        li: scan,
2790                        q_norm: q_norm.as_deref(),
2791                        k_norm: k_norm.as_deref(),
2792                        output_gate: *output_gate,
2793                        proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2794                        head_gate_w,
2795                        bias: bias
2796                            .as_ref()
2797                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2798                        full_gpu,
2799                    });
2800                }
2801                _ => break,
2802            }
2803            scan += 1;
2804        }
2805        let Some(model) = model_ref else {
2806            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2807                eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2808            }
2809            if self.graph_head_required {
2810                self.fail_metal_graph("native graph has no mapped model reference");
2811            }
2812            return start;
2813        };
2814        if plan.is_empty() {
2815            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2816                eprintln!("q1-graph: empty plan at layer {start}");
2817            }
2818            if self.graph_head_required {
2819                self.fail_metal_graph("native graph plan is empty");
2820            }
2821            return start;
2822        }
2823        let has_moe = plan.iter().any(|it| match it {
2824            Item::Gdn { run, .. } => run
2825                .iter()
2826                .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2827            Item::Sconv { run, .. } => run
2828                .iter()
2829                .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2830            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2831        });
2832        let has_flint_moe = plan.iter().any(|it| match it {
2833            Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2834            Item::Sconv { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2835            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2836        });
2837        let metal_working_set = crate::gpu_metal::working_set_bytes();
2838        let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2839        let model_bytes = model.primary_bytes().len() as u64;
2840        // Flint q2 can sit below recommendedMaxWorkingSetSize on a 24 GB
2841        // M4 yet still exceed maxBufferLength. In that case a normal graph
2842        // would manufacture overlapping whole-file WeightArena windows;
2843        // treat it as the same bounded-page requirement rather than letting
2844        // the generic graph reintroduce broad residency.
2845        let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2846            || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2847        // An oversized MoE mmap normally has to decline: the jobs kernels
2848        // bind all WeightArena windows.  The opt-in pager takes a different,
2849        // bounded route: a *complete sequence of logical layers* is put in
2850        // one transient no-copy page, graph-finished, dropped, then the outer
2851        // layer loop calls us again for the next page.  That preserves exact
2852        // hidden/GDN/KV handoff while replacing ~40 host-routed expert waits
2853        // with a few page-boundary syncs.  Never use it for a fused-head NLL
2854        // request: that contract explicitly requires a single whole graph.
2855        let pager_cap = if !has_moe {
2856            None
2857        } else if has_flint_moe {
2858            // Do not let generic `CMF_METAL_MOE_PAGER` turn the experimental
2859            // full-bank Flint graph on. Its direct selected-view pager stays
2860            // independently automatic; this path needs its own opt-in.
2861            crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2862        } else {
2863            crate::gpu_metal::moe_pager_cap_bytes()
2864        };
2865        let pager_eligible =
2866            pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2867        let mut pager_span: Option<(usize, usize)> = None;
2868        if pager_eligible {
2869            let cap = pager_cap.expect("checked above");
2870            // When this page reaches the true model end, include the fused
2871            // lm_head too.  Otherwise `lm_head_ok` would correctly refuse a
2872            // binding outside the page after the layers had already run.
2873            let tail_span = (self.graph_want_logits
2874                && upto.is_none()
2875                && scan == self.num_layers
2876                && std::env::var("CMF_GPU_LMHEAD")
2877                    .map(|v| v != "0")
2878                    .unwrap_or(true))
2879            .then(|| self.weights.lm_head.metal_graph_parts())
2880            .flatten()
2881            .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2882            let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2883                let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2884                Some(match span {
2885                    Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2886                    None => (lo, hi),
2887                })
2888            };
2889            let page_span = |span: (usize, usize), include_tail: bool| {
2890                let (mut lo, mut hi) = span;
2891                if include_tail {
2892                    if let Some((tail_lo, tail_hi)) = tail_span {
2893                        lo = lo.min(tail_lo);
2894                        hi = hi.max(tail_hi);
2895                    }
2896                }
2897                crate::gpu_metal::page_round_span(lo, hi)
2898            };
2899            let mut selected = 0usize;
2900            let mut selected_span: Option<(usize, usize)> = None;
2901            // `Item::Gdn` is built as a maximal run so the normal resident
2902            // graph can batch its recurrent state work.  A physical CMF can
2903            // nevertheless place that whole run across a page cap while each
2904            // logical layer fits.  Split *only the final admitted run* at a
2905            // layer boundary: the next outer-loop call rebuilds the remaining
2906            // suffix, preserving hidden/state handoff and keeping the normal
2907            // long-run batching within every page.
2908            let mut gdn_prefix: Option<(usize, usize)> = None;
2909            'items: for (i, item) in plan.iter().enumerate() {
2910                match item {
2911                    // Short-conv runs are not paged (no oversized
2912                    // short-conv MoE has been measured): the page ends.
2913                    Item::Sconv { .. } => break,
2914                    Item::Attn { li, .. } => {
2915                        let Some(span) = add_layer_span(selected_span, *li) else {
2916                            break;
2917                        };
2918                        let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2919                            break;
2920                        };
2921                        if page_hi - page_lo > cap {
2922                            break;
2923                        }
2924                        selected = i + 1;
2925                        selected_span = Some((page_lo, page_hi));
2926                    }
2927                    Item::Gdn { first, run } => {
2928                        // Preserve a complete recurrent run whenever it can
2929                        // occupy a page on its own.  If the preceding items
2930                        // merely used the remaining cap, close that page and
2931                        // let the next q1 invocation retain the full run.
2932                        // Prefix-splitting is only for a run that genuinely
2933                        // cannot fit an empty page.
2934                        let mut whole = None;
2935                        for take in 1..=run.len() {
2936                            whole = add_layer_span(whole, *first + take - 1);
2937                            if whole.is_none() {
2938                                break;
2939                            }
2940                        }
2941                        let whole_fits_empty_page = whole
2942                            .and_then(|span| page_span(span, false))
2943                            .is_some_and(|(lo, hi)| hi - lo <= cap);
2944                        if whole_fits_empty_page {
2945                            let mut span = selected_span;
2946                            for take in 1..=run.len() {
2947                                let Some(next) = add_layer_span(span, *first + take - 1) else {
2948                                    break 'items;
2949                                };
2950                                span = Some(next);
2951                            }
2952                            let Some(span) = span else {
2953                                break;
2954                            };
2955                            let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2956                            else {
2957                                break;
2958                            };
2959                            if page_hi - page_lo > cap {
2960                                break;
2961                            }
2962                            selected = i + 1;
2963                            selected_span = Some((page_lo, page_hi));
2964                            continue;
2965                        }
2966                        if selected != 0 {
2967                            break;
2968                        }
2969                        let mut span = None;
2970                        let mut best: Option<(usize, (usize, usize))> = None;
2971                        for take in 1..=run.len() {
2972                            let Some(next) = add_layer_span(span, *first + take - 1) else {
2973                                break;
2974                            };
2975                            // A partial final run cannot encode the lm_head;
2976                            // only its complete form above may include it.
2977                            let Some((page_lo, page_hi)) = page_span(next, false) else {
2978                                break;
2979                            };
2980                            if page_hi - page_lo > cap {
2981                                break;
2982                            }
2983                            span = Some((page_lo, page_hi));
2984                            best = Some((take, (page_lo, page_hi)));
2985                        }
2986                        let Some((take, page)) = best else {
2987                            break;
2988                        };
2989                        selected = i + 1;
2990                        selected_span = Some(page);
2991                        if take != run.len() {
2992                            gdn_prefix = Some((i, take));
2993                            break 'items;
2994                        }
2995                    }
2996                }
2997            }
2998            if selected != 0 {
2999                if let Some((item, take)) = gdn_prefix {
3000                    match &mut plan[item] {
3001                        Item::Gdn { run, .. } => run.truncate(take),
3002                        Item::Sconv { .. } | Item::Attn { .. } => {
3003                            unreachable!("only a GDN run is splittable")
3004                        }
3005                    }
3006                }
3007                let (page_lo, page_hi) = selected_span.expect("selected page span");
3008                tracing::info!(
3009                    page_start = page_lo,
3010                    page_end = page_hi,
3011                    page_mb = (page_hi - page_lo) / (1024 * 1024),
3012                    cap_mb = cap / (1024 * 1024),
3013                    graph_items = selected,
3014                    "native Metal MoE graph uses transient bounded weight page"
3015                );
3016                plan.truncate(selected);
3017                pager_span = Some((page_lo, page_hi));
3018            } else {
3019                tracing::warn!(
3020                    cap_mb = cap / (1024 * 1024),
3021                    "native Metal MoE pager declined: one graph item does not fit its safe page cap"
3022                );
3023            }
3024        }
3025        // A native MoE graph binds every WeightArena window to its jobs
3026        // kernels. On UMA, an mmap that already exceeds the driver's
3027        // recommended working set will therefore fail after admission, when
3028        // some recurrent/KV scratch has already been touched. Decline before
3029        // allocating/encoding the graph instead; the ordinary per-operation
3030        // route remains available and only materializes the needed window.
3031        // Do not apply this to dense graphs: they bind one projection at a
3032        // time and do not have the MoE jobs' all-window lifetime.
3033        if has_moe && needs_bounded_pager && pager_span.is_none() {
3034            tracing::info!(
3035                model_bytes,
3036                metal_working_set,
3037                "native Metal MoE graph declined before allocation"
3038            );
3039            self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
3040            if self.graph_head_required {
3041                self.fail_metal_graph(MOE_WORKING_SET_REASON);
3042            }
3043            return start;
3044        }
3045        let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
3046        let dev_attend = attend_contract
3047            && (self.head_dim <= 128
3048                || has_moe
3049                // A GDN hybrid attends on a quarter of its layers: the
3050                // hd>128 caution was measured on pure-dense models where
3051                // gqa_attend dominates, and on Qwen3.8-27B (hd 256, 48
3052                // GDN + 16 attn) the sandwich costs 2x the whole decode
3053                // (1.2 vs 2.21 tok/s measured before the arena fix).
3054                || (self.head_dim <= 256 && has_gdn)
3055                // Sliding-window layers bound most attends by the window:
3056                // Spark-X2.5-1.7B q4tp on the M4 decodes 67 tok/s
3057                // device-attended against 23 sandwiched (28 syncs/token).
3058                || (self.head_dim <= 256 && swa_graph)
3059                || attend_mode == "force"
3060                || attend_mode == "256");
3061        if !dev_attend {
3062            for it in &mut plan {
3063                if let Item::Attn { li, full_gpu, .. } = it {
3064                    // The hd>128 policy is about gqa_attend; an o1 layer
3065                    // attends through its own kernel set.
3066                    let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
3067                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
3068                    if !keep_o1 {
3069                        *full_gpu = false;
3070                    }
3071                }
3072            }
3073        }
3074        if std::env::var("CMF_GRAPH_DBG").is_ok() {
3075            use std::sync::atomic::{AtomicBool, Ordering};
3076            static SAID: AtomicBool = AtomicBool::new(false);
3077            if !SAID.swap(true, Ordering::Relaxed) {
3078                let fg = plan
3079                    .iter()
3080                    .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
3081                    .count();
3082                let att = plan
3083                    .iter()
3084                    .filter(|it| matches!(it, Item::Attn { .. }))
3085                    .count();
3086                eprintln!(
3087                    "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
3088                    plan.len(),
3089                    self.head_dim,
3090                    self.rotary_dim,
3091                    self.num_kv_heads,
3092                    self.num_heads,
3093                );
3094            }
3095        }
3096        let dims = GraphDims {
3097            hidden: self.hidden_size,
3098            eps: self.rms_eps as f32,
3099            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3100        };
3101        // The outer `forward_layers_span` scope is one logical token.  This
3102        // guard starts at q1_graph_gpu entry and ends after all page readback,
3103        // so a multi-page token cannot be reported as several whole tokens.
3104        let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
3105            crate::gpu_metal::MoePagerPageScope::new(
3106                _mt0,
3107                position,
3108                start,
3109                page_lo,
3110                page_hi,
3111                // `pager_span` is selected only after a cap was admitted;
3112                // keep the profiler observational if that invariant is ever
3113                // loosened rather than introducing a new inference panic.
3114                pager_cap.unwrap_or(0),
3115            )
3116        });
3117        let graph_new = match pager_span {
3118            Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3119            None => TokenGraph::new(&model, dims, h),
3120        };
3121        let Some(mut graph) = graph_new else {
3122            if self.graph_head_required {
3123                self.fail_metal_graph("native TokenGraph allocation refused");
3124            }
3125            return start;
3126        };
3127        if swa_graph && self.head_dim > 128 {
3128            // Spark-X2.5 (hd 256, 4 Q heads per KV head): the GQA-shared
3129            // split-K attend reads each K/V row once for the group instead
3130            // of once per head plus the importance re-read — at depth 1000
3131            // on the M4, 59.5 tok/s against 50.0 with the per-head kernel
3132            // up to the default 512 (every sliding layer sits at <= 512).
3133            // From the second position on and in 32-position blocks (more
3134            // threadgroups in flight for a latency-bound walk): Spark-X2.6
3135            // q4tp decode 0.86x the GPU time short and 0.88x at depth 1000,
3136            // q8_2f 0.91x / 0.92x (in-process A/B, M4).
3137            graph.set_attend_blk(1, 32);
3138        }
3139        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3140            nv: cfg.num_v_heads,
3141            nk: cfg.num_k_heads,
3142            dk: cfg.key_head_dim,
3143            dv: cfg.value_head_dim,
3144            kk: cfg.conv_kernel,
3145            hidden: self.hidden_size,
3146            inter: self.intermediate_size,
3147            c_dim: cfg.conv_dim(),
3148            eps: cfg.rms_eps as f32,
3149            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3150        });
3151        // Validate the whole plan BEFORE encoding anything: after the
3152        // first sync a refused layer would leave the token
3153        // half-executed, so truncate to the provably encodable prefix.
3154        let mut valid = 0usize;
3155        let mut end = start;
3156        crate::gpu::stageprof(1, _mt0.elapsed()); // конец планирования
3157        if std::env::var("CMF_PLAN_DUMP").is_ok() {
3158            static ONCE: std::sync::Once = std::sync::Once::new();
3159            ONCE.call_once(|| {
3160                for it in &plan {
3161                    match it {
3162                        Item::Gdn { first, run } => {
3163                            eprintln!("plan: Gdn first={first} len={}", run.len())
3164                        }
3165                        Item::Sconv { first, run } => {
3166                            eprintln!("plan: Sconv first={first} len={}", run.len())
3167                        }
3168                        Item::Attn { li, full_gpu, .. } => {
3169                            eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3170                        }
3171                    }
3172                }
3173            });
3174        }
3175        for item in &plan {
3176            let ok = match item {
3177                Item::Gdn { run, .. } => gcfg
3178                    .as_ref()
3179                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3180                    .unwrap_or(false),
3181                Item::Sconv { run, .. } => self
3182                    .short_conv_cfg
3183                    .is_some_and(|sc| run.iter().all(|l| graph.sconv_ok(l, sc.kernel))),
3184                Item::Attn { l, .. } => graph.attn_ok(l),
3185            };
3186            if !ok {
3187                if block_diag {
3188                    eprintln!(
3189                        "block-graph: plan item {} ({}) failed graph preflight",
3190                        valid,
3191                        match item {
3192                            Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3193                            Item::Sconv { run, first } => {
3194                                format!("short-conv run L{first}+{}", run.len())
3195                            }
3196                            Item::Attn { li, .. } => format!("Attn L{li}"),
3197                        }
3198                    );
3199                }
3200                break;
3201            }
3202            valid += 1;
3203            end += match item {
3204                Item::Gdn { run, .. } => run.len(),
3205                Item::Sconv { run, .. } => run.len(),
3206                Item::Attn { .. } => 1,
3207            };
3208        }
3209        plan.truncate(valid);
3210        if plan.is_empty() {
3211            if self.graph_head_required {
3212                self.fail_metal_graph("native graph preflight produced no valid items");
3213            }
3214            return start;
3215        }
3216
3217        if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3218            self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3219            return start;
3220        }
3221
3222        // Plain dense decode (every item a device-attended full-attention
3223        // layer with a dense FFN, no O(1) state): the only plan shape the
3224        // masked-nibble q4tp matvec and the concurrent layer encoder were
3225        // measured on (MiniCPM5-2B, Qwen3-0.6B on the M4). Hybrids, MoE and
3226        // o1 layers keep the historical serial path bit for bit.
3227        // Every projection must be ONE dispatch (q1t adds an overlay pass,
3228        // Prism q2tp a transform pass — dependent pairs a concurrent
3229        // encoder would race).
3230        let one_pass = |t: (usize, usize, usize)| {
3231            use cortiq_core::TensorDtype as D;
3232            matches!(
3233                model.tensors[t.0].dtype,
3234                D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3235            )
3236        };
3237        let dense_fast = plan.iter().all(|it| match it {
3238            Item::Attn {
3239                l, li, full_gpu, ..
3240            } => {
3241                *full_gpu
3242                    && self.kv_cache.layers[*li].o1.is_none()
3243                    && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3244                    && match l.ffn {
3245                        MetalFfn::Dense { gate, up, down, .. } => {
3246                            one_pass(gate) && one_pass(up) && one_pass(down)
3247                        }
3248                        _ => false,
3249                    }
3250            }
3251            // A short-conv run encodes serially in its own encoder: its
3252            // one-pass projections take the same matvec levers.
3253            Item::Sconv { run, .. } => run.iter().all(|l| {
3254                one_pass(l.in_proj)
3255                    && one_pass(l.out)
3256                    && match l.ffn {
3257                        MetalFfn::Dense { gate, up, down, .. } => {
3258                            one_pass(gate) && one_pass(up) && one_pass(down)
3259                        }
3260                        _ => false,
3261                    }
3262            }),
3263            Item::Gdn { .. } => false,
3264        });
3265        // MoE decode with every layer device-attended (Mellum2.1): the
3266        // routed experts' jobs ladder and the q8_2f attention projections
3267        // take their measured levers (`MOE_*`, `DENSE_Q8R4`).
3268        let moe_plan = !dense_fast
3269            && plan.iter().all(|it| match it {
3270                Item::Attn {
3271                    l, li, full_gpu, ..
3272                } => {
3273                    *full_gpu
3274                        && self.kv_cache.layers[*li].o1.is_none()
3275                        && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3276                }
3277                Item::Gdn { .. } | Item::Sconv { .. } => false,
3278            });
3279        let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3280        if block_diag {
3281            use std::sync::atomic::{AtomicBool, Ordering};
3282            static SAID_AB: AtomicBool = AtomicBool::new(false);
3283            if !SAID_AB.swap(true, Ordering::Relaxed) {
3284                eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3285            }
3286        }
3287        let _mv_fast = match ab {
3288            Some((bits, _)) => {
3289                graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3290                crate::gpu_metal::MvFastGuard::set_raw(bits)
3291            }
3292            None if moe_plan => {
3293                graph.set_dense_concurrent(
3294                    crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3295                );
3296                crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3297            }
3298            None => {
3299                graph.set_dense_concurrent(dense_fast);
3300                // Spark-X2.5 q8_2f: the four-row q8_2f matvec decodes the
3301                // 1.7B at 40.5 tok/s on the M4 against 27.7 one-row.
3302                let q8r4 = if swa_graph {
3303                    crate::gpu_metal::DENSE_Q8R4
3304                } else {
3305                    0
3306                };
3307                crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3308                    crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3309                } else {
3310                    0
3311                })
3312            }
3313        };
3314
3315        // RoPE table and rotary width are per layer (`layer_inv_freq`,
3316        // `layer_geom`) inside the loop below.
3317        let pool = self.pool.clone();
3318        let (nh, nkv, hd, hs, eps) = (
3319            self.num_heads,
3320            self.num_kv_heads,
3321            self.head_dim,
3322            self.hidden_size,
3323            self.rms_eps,
3324        );
3325        let norm_style = self.norm_style;
3326        let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3327        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3328        let kv_id = self.graph_kv_id;
3329        // GDN runs whose states await readback after the next sync
3330        // (device-attended layers add no sync, so several may stack).
3331        let mut pending: Vec<(usize, usize)> = Vec::new();
3332        // Device-attended layers: their K/V/imp are pulled from the
3333        // mirror after the final sync.
3334        let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3335        for item in &plan {
3336            let _xt0 = std::time::Instant::now();
3337            let _xkind: u32 = match item {
3338                Item::Gdn { .. } | Item::Sconv { .. } => 2,
3339                Item::Attn { .. } => 3,
3340            };
3341            // Looped Transformer: insert on-device norm at loop boundaries.
3342            if self.loop_final_norm {
3343                let item_start = match item {
3344                    Item::Gdn { first, .. } | Item::Sconv { first, .. } => *first,
3345                    Item::Attn { li, .. } => *li,
3346                };
3347                if item_start > start && self.is_loop_end(item_start - 1) {
3348                    graph.encode_loop_norm(&self.weights.final_norm);
3349                }
3350            }
3351            match item {
3352                Item::Gdn { run, first } => {
3353                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3354                        if l.linear_state.len() != want {
3355                            l.linear_state = vec![0f32; want];
3356                        }
3357                    }
3358                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3359                        .iter()
3360                        .map(|l| l.linear_state.as_slice())
3361                        .collect();
3362                    let _ig = std::time::Instant::now();
3363                    if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3364                        // Unreachable: the plan was validated above.
3365                        tracing::error!("q1 graph: GDN run refused after validation");
3366                        return start;
3367                    }
3368                    // Early commit: the GPU starts the run while the
3369                    // CPU encodes the next layer (nothing to wait on).
3370                    graph.commit_kind = 2;
3371                    graph.commit();
3372                    crate::gpu::stageprof(0, _ig.elapsed());
3373                    pending.push((*first, run.len()));
3374                }
3375                Item::Sconv { run, first } => {
3376                    let sc = self.short_conv_cfg.expect("validated short-conv run");
3377                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3378                        if l.linear_state.len() != sc.state_len() {
3379                            l.linear_state = vec![0f32; sc.state_len()];
3380                        }
3381                    }
3382                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3383                        .iter()
3384                        .map(|l| l.linear_state.as_slice())
3385                        .collect();
3386                    if !graph.encode_sconv_run(run, &ro, sc.kernel) {
3387                        tracing::error!("q1 graph: short-conv run refused after validation");
3388                        return start;
3389                    }
3390                    graph.commit_kind = 2;
3391                    graph.commit();
3392                    pending.push((*first, run.len()));
3393                }
3394                Item::Attn {
3395                    l,
3396                    li,
3397                    q_norm,
3398                    k_norm,
3399                    output_gate,
3400                    proj_gate,
3401                    head_gate_w,
3402                    bias,
3403                    full_gpu,
3404                } => {
3405                    let _ia = std::time::Instant::now();
3406                    // This layer's attention geometry: its window, RoPE
3407                    // table and rotary width (Spark-X2.5 interleaves
3408                    // 512-window layers rotating all dims at θ 1e4 with
3409                    // full layers rotating a quarter at θ 5e6). Every model
3410                    // without sliding layers reads the global ones here.
3411                    let inv_freq_l = self.layer_inv_freq(*li);
3412                    let rd_l = self.layer_geom(*li).2;
3413                    let window_l = self.layer_window(*li);
3414                    let head_gate_w = *head_gate_w;
3415                    // This layer's KV heads and head width: Gemma-4's global
3416                    // layers (one 512-wide KV head) differ from the base.
3417                    let (nkv, hd) = if gemma4 {
3418                        let (a, b, _) = self.layer_geom(*li);
3419                        (a, b)
3420                    } else {
3421                        (nkv, hd)
3422                    };
3423                    // ── Fully device-resident attention: no sync at all.
3424                    if *full_gpu {
3425                        let cache = &self.kv_cache.layers[*li];
3426                        let o1p = if cache.o1.is_some() {
3427                            match cache.o1_views() {
3428                                Some(views) => Some(crate::gpu::O1AttnParams {
3429                                    views,
3430                                    epoch: self.o1_epoch,
3431                                }),
3432                                // Sealed state gone mid-run: sandwich.
3433                                None => None,
3434                            }
3435                        } else {
3436                            None
3437                        };
3438                        let o1_layer = cache.o1.is_some();
3439                        if o1_layer && o1p.is_none() {
3440                            // fall to the sandwich (CPU o1 step)
3441                        }
3442                        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3443                        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3444                        let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3445                        // A trimmed tail always holds the window the
3446                        // device attend reads (`first = rows + 1 − w`).
3447                        debug_assert!(
3448                            cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3449                        );
3450                        let p = crate::gpu::AttnDeviceParams {
3451                            kv_id,
3452                            layer: *li,
3453                            nh,
3454                            nkv,
3455                            hd,
3456                            rd: rd_l,
3457                            position,
3458                            scale: self.attn_scale,
3459                            eps: eps as f32,
3460                            gemma,
3461                            late_qk_norm: self.qk_norm_after_rope,
3462                            output_gate: *output_gate,
3463                            q_norm: *q_norm,
3464                            k_norm: *k_norm,
3465                            inv_freq: &inv_freq_l,
3466                            rope_scale: self.layer_rope_scale(*li),
3467                            cpu_k,
3468                            cpu_v,
3469                            cpu_stored,
3470                            cpu_gen: cache.generation(),
3471                            o1: o1p,
3472                            window: window_l,
3473                            softmax_sink: cache.sinks.as_deref(),
3474                            head_gate: head_gate_w,
3475                            v_norm: self.attn_v_norm,
3476                        };
3477                        let o1_bad = o1_layer && p.o1.is_none();
3478                        if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3479                        {
3480                            // o1 layers leave no mirror row to pull.
3481                            if p.o1.is_none() {
3482                                dev_attn.push((*li, nkv, hd));
3483                            }
3484                            graph.commit_kind = 3;
3485                            graph.commit();
3486                            // The footer below is skipped by `continue`:
3487                            // account the device-attn item here or its
3488                            // cost hides from the stage profile entirely.
3489                            crate::gpu::stageprof(_xkind, _xt0.elapsed());
3490                            continue;
3491                        }
3492                        // Mirror refused (nothing encoded) → sandwich.
3493                    }
3494                    graph.encode_attn_prefix(l);
3495                    if let Err(err) = graph.sync_checked() {
3496                        self.fail_metal_graph(&err);
3497                        return start;
3498                    }
3499                    if !pending.is_empty() {
3500                        let idxs: Vec<usize> =
3501                            pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3502                        let mut outs: Vec<&mut [f32]> = self
3503                            .kv_cache
3504                            .layers
3505                            .iter_mut()
3506                            .enumerate()
3507                            .filter(|(i, _)| idxs.binary_search(i).is_ok())
3508                            .map(|(_, s)| s.linear_state.as_mut_slice())
3509                            .collect();
3510                        graph.read_states(&mut outs);
3511                    }
3512                    let mut q_raw = attention::take_buf(l.wq.1);
3513                    let mut k = attention::take_buf(l.wk.1);
3514                    let mut v = attention::take_buf(l.wv.1);
3515                    graph.read_qkv(&mut q_raw, &mut k, &mut v);
3516                    // Projected output gate: g = G·norm(h) on the host, off
3517                    // the normed input the prefix left on the device.
3518                    let mut gate_raw = proj_gate.map(|(gp, _)| {
3519                        let mut normed = attention::take_buf(hs);
3520                        graph.read_normed(&mut normed);
3521                        let mut raw = attention::take_buf(gp.rows());
3522                        gp.matvec(&normed, &mut raw, pool.as_deref());
3523                        attention::recycle_buf(&mut normed);
3524                        raw
3525                    });
3526                    let cfg = QwenAttnCfg {
3527                        num_heads: nh,
3528                        num_kv_heads: nkv,
3529                        head_dim: hd,
3530                        hidden_size: hs,
3531                        position,
3532                        inv_freq: &inv_freq_l,
3533                        rotary_dim: rd_l,
3534                        scale: self.attn_scale,
3535                        softcap: self.attn_softcap,
3536                        window: window_l,
3537                        // Gemma-4 only (the front gate refuses V norm
3538                        // otherwise).
3539                        v_norm: self.attn_v_norm,
3540                        qk_norm_after_rope: self.qk_norm_after_rope,
3541                        gate_sigmoid: self.proj_gate_sigmoid,
3542                        q_norm: *q_norm,
3543                        k_norm: *k_norm,
3544                        output_gate: *output_gate,
3545                        softplus_gate: None,
3546                        rope_scale: self.layer_rope_scale(*li),
3547                        bias: *bias,
3548                        rms_eps: eps,
3549                        norm_style,
3550                        pool: pool.as_deref(),
3551                        v_head_dim: hd,
3552                    };
3553                    // CMF_ATTN_ORACLE=1: diff the device attend against
3554                    // this CPU attend on identical inputs (bring-up).
3555                    let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3556                        || std::env::var("CMF_ATTN_DUMP").is_ok();
3557                    let _ = full_gpu;
3558                    let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3559                    let mut ao = attention::qwen_attention_core(
3560                        q_raw,
3561                        k,
3562                        v,
3563                        &mut self.kv_cache.layers[*li],
3564                        &cfg,
3565                    );
3566                    // CMF_ATTN_DUMP=<dir>: this token's rope'd Q and the layer's whole
3567                    // K/V cache as raw f32 (offline attention-statistics probes:
3568                    // block bounds, mass concentration). Needs CMF_GPU_ATTEND=0.
3569                    if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3570                        if let Some((qr0, k0, v0)) = oracle_in.clone() {
3571                            let (cq, _cg, _ck, _cv) =
3572                                attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3573                            let cache = &self.kv_cache.layers[*li];
3574                            let n = cache.head_keys(0).len() / hd;
3575                            let mut bytes: Vec<u8> = Vec::new();
3576                            for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3577                                bytes.extend_from_slice(&v.to_le_bytes());
3578                            }
3579                            for v in &cq {
3580                                bytes.extend_from_slice(&v.to_le_bytes());
3581                            }
3582                            for g in 0..nkv {
3583                                for v in cache.head_keys(g) {
3584                                    bytes.extend_from_slice(&v.to_le_bytes());
3585                                }
3586                            }
3587                            for g in 0..nkv {
3588                                for v in cache.head_values(g) {
3589                                    bytes.extend_from_slice(&v.to_le_bytes());
3590                                }
3591                            }
3592                            let _ =
3593                                std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3594                        }
3595                    }
3596                    if let Some((qr0, k0, v0)) =
3597                        oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3598                    {
3599                        let (cq, _cg, ck, cv) =
3600                            attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3601                        let mut h_now = vec![0f32; hs];
3602                        graph.read_h(&mut h_now);
3603                        let cache = &self.kv_cache.layers[*li];
3604                        let n_after = cache.head_keys(0).len() / hd;
3605                        // A sealed O(1) cache may have no dense current-row
3606                        // entry. The oracle is a debug probe, so let it see
3607                        // zero stored exact rows instead of underflowing.
3608                        let stored = n_after.saturating_sub(1);
3609                        let cpu_k: Vec<&[f32]> = (0..nkv)
3610                            .map(|g| &cache.head_keys(g)[..stored * hd])
3611                            .collect();
3612                        let cpu_v: Vec<&[f32]> = (0..nkv)
3613                            .map(|g| &cache.head_values(g)[..stored * hd])
3614                            .collect();
3615                        let p = crate::gpu::AttnDeviceParams {
3616                            kv_id,
3617                            layer: *li,
3618                            nh,
3619                            nkv,
3620                            hd,
3621                            // The layer's own geometry, as the CPU attend
3622                            // above used it (the projected gate is applied
3623                            // after this probe on both sides).
3624                            rd: rd_l,
3625                            position,
3626                            scale: self.attn_scale,
3627                            eps: eps as f32,
3628                            gemma,
3629                            late_qk_norm: self.qk_norm_after_rope,
3630                            output_gate: *output_gate,
3631                            q_norm: *q_norm,
3632                            k_norm: *k_norm,
3633                            inv_freq: &inv_freq_l,
3634                            rope_scale: self.layer_rope_scale(*li),
3635                            cpu_k,
3636                            cpu_v,
3637                            cpu_stored: stored,
3638                            cpu_gen: cache.generation(),
3639                            o1: None,
3640                            window: window_l,
3641                            softmax_sink: cache.sinks.as_deref(),
3642                            head_gate: None,
3643                            v_norm: self.attn_v_norm,
3644                        };
3645                        if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3646                            let md = |a: &[f32], b: &[f32]| {
3647                                a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3648                            };
3649                            let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3650                            eprintln!(
3651                                "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}",
3652                                nn(&cq),
3653                                md(&cq, &dq),
3654                                nn(&ck),
3655                                md(&ck, &dk),
3656                                nn(&cv),
3657                                md(&cv, &dv),
3658                                nn(&ao),
3659                                md(&ao, &dao)
3660                            );
3661                        } else {
3662                            eprintln!("attn-oracle L{li}: device probe declined");
3663                        }
3664                    }
3665                    if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3666                        // V is as wide as the head (the front gate), so ao
3667                        // is nh·hd here, as in `qwen_attention`.
3668                        attention::apply_projected_gate(
3669                            &mut ao,
3670                            raw,
3671                            per_head,
3672                            hd,
3673                            self.proj_gate_sigmoid,
3674                        );
3675                    }
3676                    if let Some(mut raw) = gate_raw.take() {
3677                        attention::recycle_buf(&mut raw);
3678                    }
3679                    graph.encode_attn_suffix(l, &ao);
3680                    // Early commit: the GPU starts O+FFN while the CPU
3681                    // encodes the following GDN run / attention prefix.
3682                    graph.commit();
3683                    attention::recycle_buf(&mut ao);
3684                }
3685            }
3686
3687            crate::gpu::stageprof(_xkind, _xt0.elapsed());
3688        }
3689        // Ride the final norm + lm_head in the same command buffer when
3690        // this run reaches the model's end and the caller wants logits:
3691        // the separate per-op lm_head submit (a full round trip) folds
3692        // into the sync that already happens here.
3693        let mut lm_rows = None;
3694        if self.graph_want_logits
3695            && upto.is_none()
3696            && end == self.num_layers
3697            && std::env::var("CMF_GPU_LMHEAD")
3698                .map(|v| v != "0")
3699                .unwrap_or(true)
3700        {
3701            if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3702                if graph.lm_head_ok(lm) {
3703                    graph.encode_lm_head(&self.weights.final_norm, lm);
3704                    lm_rows = Some(lm.1);
3705                }
3706            }
3707        }
3708        if self.graph_head_required && lm_rows.is_none() {
3709            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3710            self.fail_metal_graph("fused graph head was requested but not encodable");
3711            return start;
3712        }
3713        let _sy0 = std::time::Instant::now();
3714        if let Err(err) = graph.sync_checked() {
3715            if let Some(profile) = pager_profile_page.as_mut() {
3716                profile.finish(start, graph.pager_page_stats(), "sync-error");
3717            }
3718            self.fail_metal_graph(&err);
3719            return start;
3720        }
3721        let _rs0 = std::time::Instant::now();
3722        if !pending.is_empty() {
3723            let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3724            let mut outs: Vec<&mut [f32]> = self
3725                .kv_cache
3726                .layers
3727                .iter_mut()
3728                .enumerate()
3729                .filter(|(i, _)| idxs.binary_search(i).is_ok())
3730                .map(|(_, s)| s.linear_state.as_mut_slice())
3731                .collect();
3732            graph.read_states(&mut outs);
3733        }
3734        if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3735            use std::sync::atomic::{AtomicU64, Ordering};
3736            static SY: AtomicU64 = AtomicU64::new(0);
3737            static RS: AtomicU64 = AtomicU64::new(0);
3738            static N: AtomicU64 = AtomicU64::new(0);
3739            SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3740            RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3741            let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3742            if n % 100 == 0 {
3743                eprintln!(
3744                    "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3745                    SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3746                    RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3747                );
3748            }
3749        }
3750        if let Some(rows) = lm_rows {
3751            crate::gpu::hostprof_encode_done(_mt0);
3752            let mut lg = attention::take_buf(rows.min(self.vocab_size));
3753            graph.read_logits(&mut lg);
3754            crate::gpu::hostprof_total(_mt0);
3755            lg.resize(self.vocab_size, 0.0);
3756            if let Some(c) = self.final_softcap {
3757                softcap_in_place(&mut lg, c, self.pool.as_deref());
3758            }
3759            self.graph_logits = Some(lg);
3760        }
3761        graph.read_h(h);
3762        if self.graph_head_required && self.graph_logits.is_none() {
3763            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3764            self.fail_metal_graph("fused graph head completed without logits readback");
3765            return start;
3766        }
3767        METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3768        METAL_GRAPH_LAYERS.fetch_add(
3769            end.saturating_sub(start) as u64,
3770            std::sync::atomic::Ordering::Relaxed,
3771        );
3772        if self.graph_head_required {
3773            METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3774        }
3775        // Device-attended layers: replay the CPU bookkeeping — append
3776        // the mirror's new K/V row (rope'd on the GPU) into the owner
3777        // cache, then bank this token's attention-importance mass.
3778        for (li, nkv, hd) in dev_attn {
3779            let mut krow = attention::take_buf(nkv * hd);
3780            let mut vrow = attention::take_buf(nkv * hd);
3781            if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3782                let cache = &mut self.kv_cache.layers[li];
3783                cache.append(&krow, &vrow, &[]);
3784                let n = cache.seq_len;
3785                let mut imp = attention::take_buf(n);
3786                crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3787                cache.accumulate_imp(&imp);
3788                attention::recycle_buf(&mut imp);
3789            }
3790            attention::recycle_buf(&mut krow);
3791            attention::recycle_buf(&mut vrow);
3792        }
3793        if let Some(profile) = pager_profile_page.as_mut() {
3794            profile.finish(end, graph.pager_page_stats(), "page-complete");
3795        }
3796        if let Some((_, arm)) = ab {
3797            crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3798        }
3799        end
3800    }
3801
3802    pub fn new(
3803        tokenizer: Tokenizer,
3804        weights: PipelineWeights,
3805        hidden_size: usize,
3806        intermediate_size: usize,
3807        num_heads: usize,
3808        num_kv_heads: usize,
3809        head_dim: usize,
3810        num_layers: usize,
3811        physical_layers: usize,
3812        loop_final_norm: bool,
3813        vocab_size: usize,
3814        rms_eps: f64,
3815        rope_base: f32,
3816        norm_style: NormStyle,
3817        max_seq_len: usize,
3818        sampler_config: SamplerConfig,
3819    ) -> Self {
3820        let rng = match sampler_config.seed {
3821            Some(s) => SplitMix64::new(s),
3822            None => SplitMix64::from_entropy(),
3823        };
3824        let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3825        let pool = Pool::from_env();
3826        if let Some(p) = &pool {
3827            tracing::info!("worker pool: {} threads", p.n_workers());
3828            // Keep the workers on the socket that holds the weights.
3829            if let Some(model) = weights
3830                .lm_head
3831                .model_arc()
3832                .or_else(|| weights.embed_tokens.model_arc())
3833            {
3834                let regions: Vec<&[u8]> =
3835                    model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3836                p.bind_numa(&regions);
3837            }
3838        }
3839        Self {
3840            gpu_plan: None,
3841            tokenizer: std::sync::Arc::new(tokenizer),
3842            kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3843            sampler_config,
3844            weights,
3845            hidden_size,
3846            intermediate_size,
3847            num_heads,
3848            num_kv_heads,
3849            head_dim,
3850            num_layers,
3851            physical_layers,
3852            loop_final_norm,
3853            vocab_size,
3854            rms_eps,
3855            rope_base,
3856            norm_style,
3857            rotary_dim: head_dim,
3858            attention_heads_per_layer: None,
3859            kv_heads_per_layer: None,
3860            v_head_dim: None,
3861            layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3862                .filter(|v| !v.is_empty())
3863                .map(std::path::PathBuf::from),
3864            graph_declines: std::cell::RefCell::new(Vec::new()),
3865            mimo_moe: Default::default(),
3866            minicpm_tail: Default::default(),
3867            vmf_cfg: None,
3868            gdn_cfg: None,
3869            kda_cfg: None,
3870            g3n: None,
3871            g4e: None,
3872            dsv4: None,
3873            dsv41: None,
3874            dsv41_vision: None,
3875            dsv41_prefill: None,
3876            qwen4_exp: None,
3877            clover: None,
3878            clover_stats: clover::CloverStats::default(),
3879            dsv4_mtp: Vec::new(),
3880            dspark: None,
3881            dspark_pending: Vec::new(),
3882            dspark_hist: Vec::new(),
3883            dspark_real: Vec::new(),
3884            dspark_trunk_picks: Vec::new(),
3885            dspark_exp: Vec::new(),
3886            dspark_draft_ns: 0,
3887            m3_winners: Vec::new(),
3888            m3_counts: Vec::new(),
3889            m3_n_exp: 0,
3890            m3_tokens: 0,
3891            m3_prewarmed: false,
3892            logit_multiplier: None,
3893            cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3894            graph_failed: std::sync::atomic::AtomicBool::new(false),
3895            graph_moe_banks: std::sync::Mutex::new(std::collections::HashMap::new()),
3896            kv_history: Vec::new(),
3897            kv_history_device: false,
3898            short_conv_cfg: None,
3899            mtp: None,
3900            mimo_mtp: None,
3901            verify_exact_moe: false,
3902            speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3903            ignore_eos: false,
3904            draft_full_streak: 0,
3905            spec_k_adapt: None,
3906            spec_acc_ewma: 0.7,
3907            rng,
3908            sampler_scratch: SamplerScratch::default(),
3909            spec_forced: None,
3910            spec_q: Vec::new(),
3911            spec_p: Vec::new(),
3912            spec_res: Vec::new(),
3913            spec_qs: Vec::new(),
3914            spec_ps: Vec::new(),
3915            spec_ress: Vec::new(),
3916            mtp_graph_mode: None,
3917            #[cfg(target_os = "macos")]
3918            metal_verify: None,
3919            inv_freq,
3920            ws: ForwardScratch::new(hidden_size),
3921            pool,
3922            model: None,
3923            dyn_force_f32: false,
3924            dyn_skill_layers: Vec::new(),
3925            dyn_active: None,
3926            dyn_blend_loaded: false,
3927            dyn_phi_layer: None,
3928            dyn_phi_ema: Vec::new(),
3929            dyn_phi_seen: 0,
3930            dyn_router: None,
3931            o1_cfg: None,
3932            o1_epoch: 0,
3933            o1_flags: Vec::new(),
3934            trace: false,
3935            calib_temp: 1.0,
3936            confidence_on: true,
3937            embed_multiplier: 1.0,
3938            attn_scale: 1.0 / (head_dim as f32).sqrt(),
3939            swa: None,
3940            swa_trim: None,
3941            swa_trim_anchor: usize::MAX,
3942            chunk_min_rows: 32,
3943            sliding_layers: None,
3944            no_rope_layers: None,
3945            anchor_core: None,
3946            bounded_rope: None,
3947            kv_prefix: KvPrefix::default(),
3948            last_prefill_tokens: 0,
3949            inv_freq_local: None,
3950            rotary_dim_local: None,
3951            rope_scale: 1.0,
3952            rope_scale_local: 1.0,
3953            global_attn: None,
3954            inv_freq_global: None,
3955            attn_v_norm: false,
3956            qk_norm_after_rope: false,
3957            proj_gate_sigmoid: false,
3958            final_softcap: None,
3959            head_clusters: None,
3960            attn_softcap: 0.0,
3961            graph_want_logits: false,
3962            graph_head_required: false,
3963            graph_logits: None,
3964            embryo_graph: None,
3965            graph_refused: std::sync::atomic::AtomicBool::new(false),
3966            graph_kv_id: {
3967                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3968                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3969            },
3970            #[cfg(test)]
3971            nll_test_fail_at: None,
3972            #[cfg(test)]
3973            nll_test_force_serial: false,
3974        }
3975    }
3976
3977    /// Enable/disable per-layer O(1) Nyström attention. Only Full
3978    /// layers are eligible (a linear layer keeps its own operator).
3979    /// Applies to generation (`generate*`/`forward_ids`): the prompt
3980    /// pass stays exact, then the state seals after prefill or at the
3981    /// deferred skeleton-safe boundary for short prompts; decode runs on
3982    /// the O(1) state. Teacher-forced scoring (`ppl_ids`) intentionally
3983    /// stays exact.
3984    pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3985        if let Err(e) = self.try_set_o1(cfg) {
3986            tracing::error!("{e}");
3987        }
3988    }
3989
3990    /// True when the file carries a native bounded anchor
3991    /// (`arch.anchor_core`): its state is a fixed record the header
3992    /// fixes, and the post-hoc O(1) override is meaningless on it.
3993    pub fn bounded_native(&self) -> bool {
3994        self.anchor_core.is_some()
3995    }
3996
3997    /// Bytes the resident device graph holds for this pipeline's sequence:
3998    /// `(recurrent state, anchor KV/ring)`; None on the host path.
3999    pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
4000        crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
4001    }
4002
4003    /// Why an O(1) override is refused on this pipeline, if it is.
4004    pub fn o1_refusal(&self) -> Option<String> {
4005        self.anchor_core.as_ref().map(|ac| {
4006            format!(
4007                "--o1 / CMF_O1 refused: the anchor is native bounded \
4008                 (anchor_core kind={} window={} sink={}); the file's operator \
4009                 is executed as-is and no post-hoc Nyström overlay applies",
4010                ac.kind, ac.window, ac.sink
4011            )
4012        })
4013    }
4014
4015    /// `set_o1` that reports the refusal instead of logging it.
4016    pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
4017        if let Some(c) = &cfg {
4018            if let Some(why) = self.o1_refusal() {
4019                self.o1_flags = Vec::new();
4020                self.o1_cfg = None;
4021                return Err(why);
4022            }
4023            if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
4024                self.o1_flags.clear();
4025                self.o1_cfg = None;
4026                return Err(format!(
4027                    "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
4028                    c.w, c.sink
4029                ));
4030            }
4031        }
4032        self.o1_flags = match &cfg {
4033            Some(c) => {
4034                let mut flags = c.layer_flags(self.num_layers);
4035                for (li, f) in flags.iter_mut().enumerate() {
4036                    // The Nyström state replaces a full-context plain
4037                    // softmax: a sliding window or a learned sink is not
4038                    // something it can represent, and a V narrower than
4039                    // the head is not what its streaming state stores.
4040                    // Those layers keep exact cache attention.
4041                    if *f
4042                        && (!matches!(
4043                            self.weights.layers[self.phys_layer(li)].attn,
4044                            AttnKind::Full { .. }
4045                        ) || self.layer_window(li).is_some()
4046                            || self.kv_cache.layers[li].sinks.is_some()
4047                            || self.layer_v_dim(li) != self.layer_geom(li).1)
4048                    {
4049                        *f = false;
4050                    }
4051                }
4052                flags
4053            }
4054            None => Vec::new(),
4055        };
4056        if let Some(c) = &cfg {
4057            let n = self.o1_flags.iter().filter(|&&f| f).count();
4058            tracing::info!(
4059                "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
4060                self.num_layers,
4061                c.m,
4062                c.w,
4063                c.sink,
4064                c.rect
4065            );
4066        }
4067        self.o1_cfg = cfg;
4068        Ok(())
4069    }
4070
4071    /// Install the file's bounded anchor: one fixed-size ring per
4072    /// `AttnKind::Bounded` layer (from the header, not per prompt) and
4073    /// the shared relative-rotation table. Must run after the RoPE setup
4074    /// (`set_rotary`, YaRN) so the table is built from the final
4075    /// `inv_freq`.
4076    pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
4077        if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
4078            return Err(format!(
4079                "anchor_core kind '{}' is not executable by this runtime",
4080                cfg.kind
4081            ));
4082        }
4083        if cfg.window == 0 {
4084            return Err("anchor_core.window must be >= 1".into());
4085        }
4086        let mut n = 0usize;
4087        for li in 0..self.num_layers {
4088            let pl = self.phys_layer(li);
4089            if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
4090                if w.window != cfg.window || w.sink != cfg.sink {
4091                    return Err(format!(
4092                        "layer {li}: bounded weights (window {} sink {}) disagree with \
4093                         anchor_core (window {} sink {})",
4094                        w.window, w.sink, cfg.window, cfg.sink
4095                    ));
4096                }
4097                self.kv_cache.layers[li].install_bounded(cfg.window);
4098                n += 1;
4099            }
4100        }
4101        if n == 0 {
4102            return Err("anchor_core is present but no layer executes it".into());
4103        }
4104        let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
4105        self.bounded_rope = Some(std::sync::Arc::new(rope));
4106        self.anchor_core = Some(cfg.clone());
4107        self.embryo_graph = None;
4108        tracing::info!(
4109            "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
4110            cfg.kind,
4111            cfg.window,
4112            cfg.sink,
4113            self.kv_cache
4114                .layers
4115                .iter()
4116                .map(|l| l.bounded_state_bytes())
4117                .max()
4118                .unwrap_or(0)
4119        );
4120        Ok(())
4121    }
4122
4123    /// Tag every layer's cache with its wire record kind and the model's
4124    /// operator identity (hash64 of `linear_core_identity` JSON) so the
4125    /// versioned state wire refuses a peer holding another operator.
4126    pub fn install_wire_identity(&mut self, identity: u64) {
4127        for li in 0..self.kv_cache.layers.len() {
4128            let pl = self.phys_layer(li);
4129            let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
4130                Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
4131                Some(AttnKind::Linear(_))
4132                | Some(AttnKind::LinearGdn(_))
4133                | Some(AttnKind::ShortConv(_))
4134                | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
4135                _ => crate::kv_cache::WireKind::Full,
4136            };
4137            let l = &mut self.kv_cache.layers[li];
4138            l.wire_kind = kind;
4139            l.wire_identity = identity;
4140            // A per-layer geometry (`set_attn_geometry`, Gemma's global
4141            // heads) rebuilds a layer cache with index 0: re-tag it.
4142            l.wire_layer = li as u32;
4143        }
4144    }
4145
4146    /// Forget the reuse keys (legacy `kv_history` and the bounded prefix).
4147    pub fn clear_history(&mut self) {
4148        self.kv_history.clear();
4149        self.kv_history_device = false;
4150        self.kv_prefix.clear();
4151    }
4152
4153    /// Did THIS pipeline's token graph refuse for a structural reason?
4154    /// (Per pipeline: another lane's refusal, or a new pipeline of the
4155    /// same model, never changes it.)
4156    pub fn graph_refused(&self) -> bool {
4157        self.graph_refused
4158            .load(std::sync::atomic::Ordering::Relaxed)
4159    }
4160
4161    /// Remember a structural refusal of this pipeline's token graph.
4162    pub fn mark_graph_refused(&self) {
4163        if !self
4164            .graph_refused
4165            .swap(true, std::sync::atomic::Ordering::Relaxed)
4166        {
4167            tracing::info!(
4168                "token graph: unsupported for this pipeline (seq {}) — not retrying",
4169                self.graph_kv_id
4170            );
4171        }
4172    }
4173
4174    /// Position the resident Embryo graph holds for this pipeline's
4175    /// sequence (`Some(next position)`), `None` when the device holds no
4176    /// image of it (host-owned sequence, or none started).
4177    pub fn device_sequence_position(&self) -> Option<usize> {
4178        crate::gpu::embryo_device_next_position(self.graph_kv_id)
4179    }
4180
4181    /// Is the sequence this pipeline's reuse key describes owned by the
4182    /// device path it would take now? A prefix recorded on the resident
4183    /// graph continues only there (at exactly `n`), a host prefix only on
4184    /// the host; any mismatch means the next turn re-prefills from zero.
4185    fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4186        let dev = self.device_sequence_position();
4187        if recorded_on_device {
4188            dev == Some(n) && self.embryo_resident_wanted()
4189        } else {
4190            dev.is_none()
4191        }
4192    }
4193
4194    /// The weights under the sequence changed (a real skill switch): every
4195    /// cached state was computed by other weights. Clear the host KV /
4196    /// ring / recurrent state AND the reuse keys — a surviving `kv_prefix`
4197    /// would let the next turn "extend" a prefix the new weights never
4198    /// saw — drop the packed resident graph (it holds the old FFN
4199    /// tensors; the next build packs the live ones under a fresh id) and
4200    /// reset its device sequence.
4201    pub(crate) fn invalidate_for_weight_change(&mut self) {
4202        self.clear_sequence_state();
4203        self.embryo_graph = None;
4204    }
4205
4206    /// Prompt positions the cache already holds when `input_ids`
4207    /// strictly EXTENDS the consumed prefix (0 otherwise). Bounded-native
4208    /// models answer from the fixed-size `kv_prefix` record; everything
4209    /// else from the legacy `kv_history` vector (which the network split
4210    /// also reads and writes).
4211    fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4212        let (n, on_device) = if self.bounded_native() {
4213            (
4214                self.kv_prefix.extension(input_ids),
4215                self.kv_prefix.on_device(),
4216            )
4217        } else {
4218            let h = &self.kv_history;
4219            if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4220                (h.len(), self.kv_history_device)
4221            } else if !h.is_empty() && !self.kv_history_device {
4222                (self.rollback_prefix_len(input_ids), false)
4223            } else {
4224                (0, false)
4225            }
4226        };
4227        // The owner tag: the state the key describes must live where this
4228        // turn will continue it (R4) — else a fresh sequence.
4229        if n > 0 && !self.prefix_owner_matches(n, on_device) {
4230            tracing::warn!(
4231                "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4232                 the device now holds {:?} — re-prefilling from zero",
4233                if on_device { "resident device" } else { "host" },
4234                self.device_sequence_position()
4235            );
4236            return 0;
4237        }
4238        n
4239    }
4240
4241    /// Positions a turn whose ids DIVERGE from the consumed prefix keeps:
4242    /// their longest common prefix, when every layer can drop its rows past
4243    /// it exactly — full attention whose f32 rows the host owns (no wgpu
4244    /// mirror ahead of the host, no recurrent, O(1) or bounded state) and
4245    /// every trimmed sliding tail still holding the window the query at the
4246    /// cut reads. A chat app re-renders the previous answer through the
4247    /// template (a thinking model's history drops its `<think>` block), so a
4248    /// conversation almost never extends token for token: extension-only
4249    /// reuse re-prefilled the whole history every turn. One id at least is
4250    /// left to prefill. `CMF_KV_ROLLBACK=0` keeps extension-only reuse.
4251    fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
4252        if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
4253            || self.device_sequence_position().is_some()
4254        {
4255            return 0;
4256        }
4257        let h = &self.kv_history;
4258        let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
4259        let n = lcp.min(input_ids.len().saturating_sub(1));
4260        if n == 0 || self.kv_cache.layers.len() < self.num_layers {
4261            return 0;
4262        }
4263        let kv_id = self.graph_kv_id;
4264        for li in 0..self.num_layers {
4265            let l = &self.kv_cache.layers[li];
4266            let tail_ok = l.base() == 0
4267                || self
4268                    .layer_window(li)
4269                    .is_some_and(|w| l.base() + w <= n + 1);
4270            if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
4271                || l.o1.is_some()
4272                || l.bounded.is_some()
4273                || !l.linear_state.is_empty()
4274                || l.mode != crate::kv_cache::KvMode::F32
4275                || l.pos_len() != h.len()
4276                || !tail_ok
4277                || crate::gpu::graph_kv_stored(kv_id, li).is_some()
4278                || crate::gpu::graph_state_resident(kv_id, li)
4279            {
4280                return 0;
4281            }
4282        }
4283        n
4284    }
4285
4286    /// Public view of the prefix-reuse decision for `input_ids` (positions
4287    /// the next `generate*` would take from the cache; 0 = fresh sequence).
4288    pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4289        self.cached_prefix_len(input_ids)
4290    }
4291
4292    /// Record the forwarded prefix as the next turn's reuse key. A
4293    /// bounded-native model extends the rolling record (`reused` = the
4294    /// positions this turn found cached); others keep the literal vector.
4295    /// Either way the key carries its OWNER: the resident device graph
4296    /// (it holds an image of this sequence) or the host.
4297    fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4298        let on_device = self.device_sequence_position().is_some();
4299        if self.bounded_native() {
4300            let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4301            let prev_device = self.kv_prefix.on_device();
4302            self.kv_history.clear();
4303            self.kv_history_device = false;
4304            if keep && prev_device == on_device {
4305                self.kv_prefix.extend(&consumed[reused..]);
4306            } else {
4307                self.kv_prefix.set(consumed);
4308            }
4309            self.kv_prefix.set_on_device(on_device);
4310        } else {
4311            self.kv_history = consumed.to_vec();
4312            self.kv_history_device = on_device;
4313        }
4314    }
4315
4316    /// True when at least one layer runs the O(1) kernel.
4317    pub fn o1_active(&self) -> bool {
4318        self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4319    }
4320
4321    /// Whether generation's prompt ingest is routed through the whole-token
4322    /// graph.  The bench uses this to label the measured generation prefill
4323    /// honestly; keep the predicate in Pipeline so CLI labels cannot drift
4324    /// from the production route.
4325    /// Positions per batched-graph submit for the prompt: `CMF_BATCH_K`
4326    /// when set (0 = one position at a time through the token graph),
4327    /// otherwise 128 (32 for a MoE stack) on a discrete card whose prompt
4328    /// takes the graph route.
4329    /// The batched graph read a 2048-token prompt at 53 tok/s against 28.5
4330    /// one position at a time on an RTX PRO 4000 (Qwen3.8-27B q4tp: TTFT
4331    /// 39 s against 72), and its states are the speculative verify's,
4332    /// measured identical to the plain path. macOS keeps its own arm.
4333    /// 128, not 32: the q4tp prefill GEMM tiles 64 batch rows, so a
4334    /// 32-row chunk left half of every tile idle. Qwen3.8-27B q4tp on an
4335    /// RTX 3090, 1000-token prompt: 51 tok/s at 32, 82 at 64, 95 at 128,
4336    /// 97 at 256; greedy output byte-identical to the 32-row chunks
4337    /// (every GEMM row and the k-looped recurrences are per position).
4338    pub fn generation_batch_k(&self) -> usize {
4339        if let Some(k) = std::env::var("CMF_BATCH_K")
4340            .ok()
4341            .and_then(|v| v.parse::<usize>().ok())
4342        {
4343            return k;
4344        }
4345        // Gemma-4 (`g4_batch_prefill`): 128-row chunks, the GEMM tile's
4346        // two 64-row halves full.
4347        if self.g4_batch_prefill() {
4348            return 128;
4349        }
4350        #[cfg(not(target_os = "macos"))]
4351        if self.graph_prefill_preferred() && !self.o1_active() {
4352            // 128 is measured on the dense hybrids; MoE stacks other than
4353            // the two expert codecs below keep the 32 their expert-grouped
4354            // prefill was measured at.
4355            let moe = self
4356                .weights
4357                .layers
4358                .iter()
4359                .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4360            // q8_2f experts (moe_q82's grouped prefill) read each expert
4361            // once per group of up to 16 of its entries, so the chunk that
4362            // fills those groups is the faster ingest: Mellum2.1 q8_2f on an
4363            // RTX 3090, 4000-token prompt, 503 tok/s at 64 rows, 542 at 128
4364            // (and 542 at 256).
4365            let q82 = crate::gpu::moe_q82_graph_on()
4366                && self.weights.layers.iter().any(|lw| {
4367                    matches!(&lw.ffn, FfnKind::Moe(m)
4368                        if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4369                });
4370            // q4tp experts: the grouped GEMM reads every routed expert once
4371            // per chunk, and 32 rows (256 entries over 256 experts, about
4372            // one row each) re-read nearly the whole expert set every 32
4373            // tokens. Apex-35B q4tp on an A40, 4096-token prompt, MTP off:
4374            // 295 tok/s at 32 rows, 357 at 64, 385 at 128, 401 at 256,
4375            // 396 at 512.
4376            let q4tp_moe = self.weights.layers.iter().any(|lw| {
4377                matches!(&lw.ffn, FfnKind::Moe(m)
4378                    if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q4tp().is_some()))
4379            });
4380            return if q82 {
4381                128
4382            } else if q4tp_moe {
4383                256
4384            } else if moe {
4385                32
4386            } else {
4387                128
4388            };
4389        }
4390        0
4391    }
4392
4393    pub fn generation_graph_prefill(&self) -> bool {
4394        let graph = self.graph_prefill_preferred();
4395        // On wgpu, an active MTP head now consumes the trunk's graph batches
4396        // and warms its own block from those returned rows.  The selected
4397        // generation measurement is therefore the batched path, even though
4398        // the underlying GDN model still satisfies the graph-prefill
4399        // predicate.  Keep the CLI label tied to the actual route.  Native
4400        // Metal has a separate prefill-batch arm and retains its historical
4401        // label here.
4402        // A batched prompt (`generation_batch_k` > 0) is the batched graph
4403        // for every model on the graph route, not only those with an MTP
4404        // head — the label follows the route.
4405        #[cfg(not(target_os = "macos"))]
4406        if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4407            return false;
4408        }
4409        graph
4410    }
4411
4412    /// Device-side O(1) mirrors currently uploaded for this pipeline's
4413    /// sequence.  The count/bytes are zero before seal or after a fresh
4414    /// reset; callers use this to distinguish logical host state from the
4415    /// GPU allocation that actually serves decode.
4416    pub fn o1_device_stats(&self) -> (usize, u64) {
4417        crate::gpu::o1_device_stats(self.graph_kv_id)
4418    }
4419
4420    /// Arm query collection on the o1 layers (fresh prompt pass).
4421    /// Reset the o1 layers to Collecting for a fresh sequence. Pub for the
4422    /// network split: each side runs the o1 lifecycle over ITS OWN layers
4423    /// (begin before prefill, seal at the prefill barrier).
4424    pub fn o1_begin(&mut self) {
4425        self.o1_begin_with_prefix(None);
4426    }
4427
4428    /// Arm collection and optionally request a positive calibration prefix.
4429    /// The effective barrier is always at least the skeleton-safe floor, so
4430    /// a short requested prefix cannot create an exact-only runtime state.
4431    pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4432        if let Some(c) = &self.o1_cfg {
4433            let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4434            let boundary = requested_prefix.map(|p| {
4435                p.max(
4436                    crate::nystrom::o1_deferred_boundary(w, sink)
4437                        .expect("o1 config boundary validated in set_o1"),
4438                )
4439            });
4440            for (li, &f) in self.o1_flags.iter().enumerate() {
4441                if f {
4442                    self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4443                }
4444            }
4445        }
4446    }
4447
4448    /// Effective deferred boundary for a positive prefix request.
4449    fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4450        self.o1_cfg.as_ref().and_then(|c| {
4451            crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4452                .map(|floor| requested_prefix.max(floor))
4453        })
4454    }
4455
4456    fn o1_note_transition(&mut self) {
4457        // Drain every layer's one-shot bit before publishing one pipeline
4458        // epoch. `any()` would short-circuit on the first layer and leak the
4459        // remaining bits into later forwards, causing one epoch per layer.
4460        let mut transitioned = false;
4461        for (li, &flagged) in self.o1_flags.iter().enumerate() {
4462            if flagged {
4463                transitioned |= self.kv_cache.layers[li].take_o1_transition();
4464            }
4465        }
4466        if transitioned {
4467            self.o1_epoch = self.o1_epoch.wrapping_add(1);
4468        }
4469    }
4470
4471    fn o1_pending(&self) -> bool {
4472        self.o1_flags.iter().enumerate().any(|(li, &f)| {
4473            f && self.kv_cache.layers[li].seq_len > 0
4474                && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4475        })
4476    }
4477
4478    fn o1_fail(&mut self, err: String) {
4479        tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4480        self.clear_sequence_state();
4481        self.graph_failed
4482            .store(true, std::sync::atomic::Ordering::Relaxed);
4483        self.cancel
4484            .store(true, std::sync::atomic::Ordering::Relaxed);
4485    }
4486
4487    /// Seal participating layers while retaining the exact state when the
4488    /// prompt is below the deferred boundary. A split worker may have
4489    /// collecting layers outside its owned span; zero-depth layers remain
4490    /// armed and are intentionally skipped until their peer runs them.
4491    pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4492        if self.o1_cfg.is_none() {
4493            return Ok(false);
4494        }
4495        let mut participating = false;
4496        for li in 0..self.num_layers {
4497            if !self.o1_flags.get(li).copied().unwrap_or(false) {
4498                continue;
4499            }
4500            if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4501                return Err(err);
4502            }
4503            if self.kv_cache.layers[li].seq_len == 0 {
4504                continue;
4505            }
4506            participating = true;
4507            let num_heads = self.layer_num_heads(li);
4508            self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4509        }
4510        self.o1_note_transition();
4511        for li in 0..self.num_layers {
4512            if self.o1_flags.get(li).copied().unwrap_or(false) {
4513                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4514                    return Err(err);
4515                }
4516            }
4517        }
4518        Ok(participating
4519            && (0..self.num_layers).all(|li| {
4520                !self.o1_flags.get(li).copied().unwrap_or(false)
4521                    || self.kv_cache.layers[li].seq_len == 0
4522                    || self.kv_cache.layers[li].o1_sealed()
4523            }))
4524    }
4525
4526    /// Complete a deferred boundary after a full position/span forward.
4527    /// This is the pipeline owner for epoch publication and failure cleanup.
4528    fn o1_progress(&mut self) {
4529        if !self.o1_active() {
4530            return;
4531        }
4532        for li in 0..self.num_layers {
4533            if self.o1_flags.get(li).copied().unwrap_or(false) {
4534                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4535                    self.o1_fail(err);
4536                    return;
4537                }
4538            }
4539        }
4540        // A qwen_attention row can seal in the middle of a complete layer
4541        // walk. Consume its transition even though the pending boundary has
4542        // already disappeared from the cache.
4543        self.o1_note_transition();
4544        if !self.o1_pending() {
4545            return;
4546        }
4547        if let Err(err) = self.o1_seal_checked() {
4548            self.o1_fail(err);
4549        }
4550    }
4551
4552    /// Turn a deferred O(1) failure raised by a hidden-only forward into the
4553    /// Result error its public batch/span caller must return. The failure
4554    /// path already cleared host/device sequence state; consume only the
4555    /// side-channel marker here and leave the pipeline reusable.
4556    fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4557        if self
4558            .graph_failed
4559            .swap(false, std::sync::atomic::Ordering::Relaxed)
4560        {
4561            self.cancel
4562                .store(false, std::sync::atomic::Ordering::Relaxed);
4563            self.clear_sequence_state();
4564            return Err(format!("{phase}: deferred O(1) transition failed"));
4565        }
4566        Ok(())
4567    }
4568
4569    /// Freeze landmarks + skeleton state after the prompt pass and drop
4570    /// the o1 layers' full KV; decode then runs `step()` per token.
4571    /// Pub for the network split (see `o1_begin`).
4572    pub fn o1_seal(&mut self) {
4573        if let Err(err) = self.o1_seal_checked() {
4574            self.o1_fail(err);
4575        }
4576    }
4577
4578    /// Enable/disable the structured per-token telemetry trace (B4).
4579    pub fn set_trace(&mut self, on: bool) {
4580        self.trace = on;
4581    }
4582
4583    /// Replace all request-scoped sampler options and reset the random stream.
4584    /// This is required for deterministic `seed` semantics in pooled servers.
4585    pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4586        self.rng = match config.seed {
4587            Some(seed) => SplitMix64::new(seed),
4588            None => SplitMix64::from_entropy(),
4589        };
4590        self.sampler_config = config;
4591    }
4592
4593    /// Toggle the per-token confidence reduction (a full-vocab
4594    /// softmax each token). `bench --core` turns it off so the timed
4595    /// loop matches llama-bench's core contract; the result's
4596    /// `confidence` vec is empty while off.
4597    pub fn set_confidence(&mut self, on: bool) {
4598        self.confidence_on = on;
4599    }
4600
4601    /// Set the confidence-calibration temperature (B1). Values ≤0 are
4602    /// clamped to raw (1.0).
4603    pub fn set_calib_temp(&mut self, t: f32) {
4604        self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4605    }
4606
4607    /// The active calibration temperature (1.0 = raw probability).
4608    pub fn calib_temp(&self) -> f32 {
4609        self.calib_temp
4610    }
4611
4612    /// The primary (full-attention) RoPE inverse-frequency table selected
4613    /// from the CMF architecture.  This is read-only diagnostic state for
4614    /// parity checks; local sliding layers may use `inv_freq_local` instead.
4615    pub fn rope_inv_freq(&self) -> &[f32] {
4616        self.inv_freq.as_slice()
4617    }
4618
4619    /// Partial rotary (Qwen3.5): rotate only the first `rotary_dim` dims;
4620    /// the frequency table is rebuilt over the rotary dims.
4621    pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4622        self.rotary_dim = rotary_dim.min(self.head_dim);
4623        self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4624        // The packed resident graph owns its own inverse-frequency plane;
4625        // changing RoPE after it was built must not leave a stale device
4626        // model behind the exact host configuration.
4627        self.embryo_graph = None;
4628    }
4629
4630    fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4631        QwenAttnCfg {
4632            num_heads: self.num_heads,
4633            num_kv_heads: self.num_kv_heads,
4634            head_dim: self.head_dim,
4635            hidden_size: self.hidden_size,
4636            position,
4637            inv_freq: &self.inv_freq,
4638            rotary_dim: self.rotary_dim,
4639            scale: self.attn_scale,
4640            softcap: self.attn_softcap,
4641            window: None,
4642            v_norm: false,
4643            qk_norm_after_rope: self.qk_norm_after_rope,
4644            gate_sigmoid: self.proj_gate_sigmoid,
4645            q_norm: None,
4646            k_norm: None,
4647            output_gate: false,
4648            softplus_gate: None,
4649            rope_scale: self.rope_scale,
4650            bias: None,
4651            rms_eps: self.rms_eps,
4652            norm_style: self.norm_style,
4653            pool: self.pool.as_deref(),
4654            v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4655        }
4656    }
4657
4658    /// Generate text from a plain-text prompt. Streams tokens via `on_token`.
4659    pub fn generate(
4660        &mut self,
4661        prompt: &str,
4662        max_tokens: usize,
4663        task_mask: Option<&TaskMask>,
4664        on_token: Option<TokenCallback>,
4665    ) -> Result<GenerateResult, String> {
4666        let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4667        self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4668    }
4669
4670    /// Generate from a V4.1 multimodal prompt prepared by the vision module.
4671    /// Vision rows are encoded once and fed through the same bounded token walk as text.
4672    pub fn generate_from_vl(
4673        &mut self,
4674        input: &crate::dsv41_vision::PreparedVlInputs,
4675        max_tokens: usize,
4676        task_mask: Option<&TaskMask>,
4677        on_token: Option<TokenCallback>,
4678    ) -> Result<GenerateResult, String> {
4679        let Some(dsv41) = &self.dsv41 else {
4680            return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4681        };
4682        if input.token_ids.is_empty() {
4683            return Err("empty V4.1 multimodal prompt".into());
4684        }
4685        if input.token_types.len() != input.token_ids.len() {
4686            return Err(format!(
4687                "V4.1 token type count {} != token count {}",
4688                input.token_types.len(),
4689                input.token_ids.len()
4690            ));
4691        }
4692        let dim = dsv41.2.dim;
4693        let mut embeddings = vec![None; input.token_ids.len()];
4694        let mut participates = vec![true; input.token_ids.len()];
4695        if !input.images.is_empty() {
4696            let vision = self
4697                .dsv41_vision
4698                .as_ref()
4699                .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4700            for image in &input.images {
4701                let end = image.start.saturating_add(image.types.len());
4702                if end > input.token_ids.len() {
4703                    return Err(format!(
4704                        "V4.1 image span {}..{} exceeds prompt length {}",
4705                        image.start,
4706                        end,
4707                        input.token_ids.len()
4708                    ));
4709                }
4710                let mut span = vec![0.0f32; image.types.len() * dim];
4711                vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4712                for (offset, &kind) in image.types.iter().enumerate() {
4713                    let pos = image.start + offset;
4714                    if input.token_types[pos] != kind {
4715                        return Err(format!(
4716                            "V4.1 image type mismatch at position {pos}: {} != {kind}",
4717                            input.token_types[pos]
4718                        ));
4719                    }
4720                    embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4721                    participates[pos] = false;
4722                }
4723            }
4724        }
4725        for (pos, &kind) in input.token_types.iter().enumerate() {
4726            if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4727                return Err(format!("V4.1 text position {pos} has an image embedding"));
4728            }
4729            if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4730                return Err(format!("V4.1 image position {pos} has no image embedding"));
4731            }
4732        }
4733        self.dsv41_prefill = Some((embeddings, participates));
4734        let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4735        self.dsv41_prefill = None;
4736        result
4737    }
4738
4739    /// `None` when the mask forbids nothing (see `TaskMask::fully_open`).
4740    fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4741        m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4742    }
4743
4744    /// Generate from prepared token ids (e.g. a chat template).
4745    ///
4746    /// With an MTP head, greedy generation without a task mask takes the
4747    /// speculative path: the MTP module drafts the token after next and
4748    /// the main model verifies both in one fused two-position forward
4749    /// (weights streamed once). The output is EXACTLY the vanilla greedy
4750    /// sequence — a rejected draft is rolled back — MTP only buys speed.
4751    pub fn generate_from_ids(
4752        &mut self,
4753        input_ids: &[u32],
4754        max_tokens: usize,
4755        task_mask: Option<&TaskMask>,
4756        on_token: Option<TokenCallback>,
4757    ) -> Result<GenerateResult, String> {
4758        self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4759    }
4760
4761    /// Generate from complete prompt embeddings [token_count, hidden_size].
4762    /// Text rows can be obtained with `embed_id`; media rows replace only
4763    /// their expanded placeholder positions. Rows are already scaled and
4764    /// enter `PrefillIn::Hidden`, so a device graph must not re-embed them.
4765    /// Token-only KV reuse is disabled both into and out of this request.
4766    pub fn generate_from_embeds(
4767        &mut self,
4768        input_ids: &[u32],
4769        prompt_rows: &[f32],
4770        max_tokens: usize,
4771        task_mask: Option<&TaskMask>,
4772        on_token: Option<TokenCallback>,
4773    ) -> Result<GenerateResult, String> {
4774        if input_ids.is_empty()
4775            || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4776        {
4777            return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4778        }
4779        if prompt_rows.iter().any(|x| !x.is_finite()) {
4780            return Err("embedded prompt contains non-finite values".into());
4781        }
4782        if !self.can_prefill_batched()
4783            || self.dyn_router.is_some()
4784            || self.o1_active()
4785            || self.mtp.is_some()
4786            || self.gpu_plan.is_some()
4787        {
4788            return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4789        }
4790        self.generate_with_prompt_rows(
4791            input_ids,
4792            Some(prompt_rows),
4793            max_tokens,
4794            task_mask,
4795            on_token,
4796        )
4797    }
4798
4799    fn generate_with_prompt_rows(
4800        &mut self,
4801        input_ids: &[u32],
4802        prompt_rows: Option<&[f32]>,
4803        max_tokens: usize,
4804        task_mask: Option<&TaskMask>,
4805        mut on_token: Option<TokenCallback>,
4806    ) -> Result<GenerateResult, String> {
4807        #[cfg(target_os = "macos")]
4808        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4809        if std::env::var("CMF_TRACE_H").is_ok() {
4810            eprintln!("input_ids: {input_ids:?}");
4811        }
4812        if input_ids.is_empty() {
4813            return Err("empty prompt: nothing to generate from".to_string());
4814        }
4815        // A prior graph failure is terminal for that sequence but must not
4816        // poison the next independent request.  Keep this flag separate from
4817        // the externally-owned cooperative cancel bit.
4818        self.graph_failed
4819            .store(false, std::sync::atomic::Ordering::Relaxed);
4820        // A mask that forbids nothing still costs every fused path and
4821        // whole-token graph, all of which are gated on `is_none()`. A
4822        // narrowed file whose one segment is always on carries exactly
4823        // such a mask — drop it here rather than pay 5x for a no-op.
4824        let task_mask = self.drop_open_mask(task_mask);
4825        // A block-diffusion model generates whole blocks by denoising
4826        // previews, never one causal token at a time.
4827        if self.clover.is_some() {
4828            if prompt_rows.is_some() || task_mask.is_some() {
4829                return Err("block-diffusion generation takes token prompts without task masks".into());
4830            }
4831            return self.clover_generate(input_ids, max_tokens, on_token);
4832        }
4833
4834        // Cross-turn KV reuse: a chat app resends the whole history
4835        // every turn; when the new ids strictly EXTEND what the cache
4836        // already holds, prefill only the tail — turn latency stays
4837        // proportional to the new text instead of the whole session.
4838        // Extension-only (no rollback), so it is exact for every layer
4839        // kind including recurrent state; MTP/o1/task-mask runs keep
4840        // the fresh-sequence path. CMF_KV_REUSE=0 disables.
4841        let mut reuse_from = {
4842            let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4843            if on
4844                && prompt_rows.is_none()
4845                && task_mask.is_none()
4846                && self.mtp.is_none()
4847                && !(self.mimo_mtp.is_some() && self.speculative)
4848                && self.o1_cfg.is_none()
4849                && self.dsv41.is_none()
4850            {
4851                self.cached_prefix_len(input_ids)
4852            } else {
4853                0
4854            }
4855        };
4856        // The device may own rows the host tail prefill needs (wgpu decode
4857        // writes only its mirror): hand them to the host, or start fresh.
4858        if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4859            reuse_from = 0;
4860        }
4861        // A diverging turn (`rollback_prefix_len`): drop every row past the
4862        // common prefix, so the tail prefill appends at `reuse_from`.
4863        if reuse_from > 0 && !self.bounded_native() && reuse_from < self.kv_history.len() {
4864            for l in &mut self.kv_cache.layers {
4865                let d = l.pos_len() - reuse_from;
4866                l.truncate_last(d);
4867            }
4868            if std::env::var("CMF_PREFILL_PROF").is_ok() {
4869                eprintln!(
4870                    "kv-reuse: rolled back {} answer position(s) to the common prefix",
4871                    self.kv_history.len() - reuse_from
4872                );
4873            }
4874            self.kv_history.truncate(reuse_from);
4875            self.graph_logits = None;
4876        }
4877        self.swa_trim_anchor = input_ids.len();
4878        self.last_prefill_tokens = input_ids.len() - reuse_from;
4879        let bounded_native = self.bounded_native();
4880        if reuse_from == 0 {
4881            // Fresh sequence — the cache holds absolute positions.
4882            // MTP never participates in cross-turn KV reuse, so a
4883            // high-vocabulary token from the previous request must not keep
4884            // this request on its expensive full draft head.
4885            self.reset_fresh_mtp_draft_state();
4886            self.clear_sequence_state();
4887        } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4888            eprintln!(
4889                "kv-reuse: {} of {} prompt positions already cached",
4890                reuse_from,
4891                input_ids.len()
4892            );
4893        }
4894        crate::gpu::graph_race_begin_generation();
4895        // Optional bounded calibration prefix. Keep the requested value
4896        // even when it is longer than the prompt; the collecting layer will
4897        // defer at the effective boundary and remain exact for short input.
4898        let o1_prefill = if self.o1_active() && task_mask.is_none() {
4899            std::env::var("CMF_O1_PREFILL")
4900                .ok()
4901                .and_then(|v| v.parse::<usize>().ok())
4902                .filter(|&p| p > 0)
4903        } else {
4904            None
4905        };
4906        if task_mask.is_none() {
4907            self.o1_begin_with_prefix(o1_prefill);
4908        }
4909
4910        // Speculative decode is off under o1: a rejected draft can't be
4911        // rolled back out of the far accumulators / ring window (the
4912        // Nyström insertion is irreversible by design).
4913        // The wgpu token graph owns a device K/V mirror that speculative
4914        // rollback would desync — the two are mutually exclusive.
4915        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4916        // Graph speculative decode (`CMF_GRAPH_SPEC=1`): the MTP head
4917        // drafts, ONE batched graph submit verifies the whole chain.
4918        //
4919        // It now PAYS on Qwen3.6-27B / RTX 5090 — 51.1 tok/s against a
4920        // plain 49.4 at k=3, medians of three, 89% of drafts accepted,
4921        // and the greedy continuation is byte-identical to the plain
4922        // path. That took the batch matvec sharing its nibble unpack
4923        // across the batch (`CMF_MV_BK=2`); before it, the same round
4924        // measured 43.6, an 11% LOSS, which is what the earlier note
4925        // here described.
4926        //
4927        // Still opt-in. One model's win is not a default: the verify
4928        // rides `gdn_spec_restore` and a batched frame whose numerics
4929        // are the batch kernels', and that has to be shown on more than
4930        // one architecture before every greedy decode takes it.
4931        // Greedy (with or without penalties) verifies by argmax equality.
4932        // Sampling (temperature > 0) can go through speculative SAMPLING —
4933        // draft from the MTP head's own post-chain distribution, accept
4934        // with min(1, p/q), correct from max(0, p − q); the emitted stream
4935        // is distributed exactly as the plain sampler's — but it is
4936        // OPT-IN (`CMF_GRAPH_SPEC_SAMPLE=1`): measured on Qwen3.8-27B /
4937        // RTX 5090 at the instruct row (0.7 / 0.80 / 20 / presence 1.5)
4938        // it decoded 19-22 tok/s against a plain 40 — nine post-chain
4939        // distributions a round plus a lower acceptance than greedy's,
4940        // against a verify that costs 2.7 single tokens. The greedy arms
4941        // pay +10%; the sampling arm needs a cheaper verify first.
4942        // Native Metal has a fast row-batch verify, but it is NOT admitted
4943        // by default: a real-model exactness check found an emitted greedy
4944        // divergence from the serial path.  Every native-Metal speculative
4945        // arm therefore requires the explicit `CMF_GRAPH_SPEC=1` experiment
4946        // gate until the batch kernels are numerically qualified.  If it is
4947        // explicitly enabled, its sampling arm is useful only for a config
4948        // the SPARSE chain serves (a top-k within `sparse_ok`): without it a
4949        // round builds nine 248k-float distributions on the host, which is
4950        // the 5090's measured loss and not a cost the round-token proxy
4951        // below can see. A top-k-less sampling config keeps the plain path
4952        // unless asked for by name.
4953        #[cfg(target_os = "macos")]
4954        let metal_graph = crate::gpu::q1_force()
4955            && crate::gpu::enabled_here()
4956            && std::env::var("CMF_GPU_BLOCK")
4957                .map(|v| v != "0")
4958                .unwrap_or(true);
4959        // `CMF_GPU_BLOCK=0` declines the ordinary native block route but
4960        // does not change the backend identity: a separately requested wgpu
4961        // graph can still reach `graph_spec_step`, whose native branch uses
4962        // VerifyGraph. Keep the quality policy tied to that identity, not to
4963        // the particular block-route admission above.
4964        #[cfg(target_os = "macos")]
4965        let native_metal_backend = crate::gpu::q1_force();
4966        #[cfg(not(target_os = "macos"))]
4967        let metal_graph = false;
4968        #[cfg(not(target_os = "macos"))]
4969        let native_metal_backend = false;
4970        let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4971        // A round whose cost is the MEASURED one: greedy (argmax rows), or
4972        // sampling through the sparse chain. Anything else pays the dense
4973        // chain's host time, which no proxy can price.
4974        let spec_cheap_round =
4975            self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4976        let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4977            || match spec_sample_env.as_deref() {
4978                Some("1") => true,
4979                Some(_) => false,
4980                None => metal_graph && spec_cheap_round,
4981            };
4982        // ON by default for greedy on the wgpu graph: with the draft on
4983        // the graph and the verify bit-exact, it measured 58.7 tok/s
4984        // against a plain 48.1 on Qwen3.8-27B q4tp / RTX 5090 (k=4) and
4985        // 51.1 against 49.4 on Qwen3.6-27B, and a round that stops
4986        // paying turns itself off below (acceptance watchdog).
4987        // `CMF_GRAPH_SPEC=0` disables; `=1` is the explicit opt-in spelling.
4988        // …but only where the batched verify has its register-blocked
4989        // kernel: q4tp dense FFNs (graph kind 6). q4t and q8_2f verify
4990        // through tile GEMMs today and measured a LOSS (q8_2f 22 against
4991        // 29 tok/s), the 2-bit plane the same; those stay opt-in
4992        // (`CMF_GRAPH_SPEC=1`).
4993        // …at least in nine dense FFNs of ten: a healed file carries its
4994        // last two layers at q8_2f, and two tile-GEMM verifies among 64 do
4995        // not change the arithmetic (measured: the healed q4tp file
4996        // decodes at the plain file's rate and would otherwise sit out).
4997        let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4998        for lw in &self.weights.layers {
4999            if let FfnKind::Dense(d) = &lw.ffn {
5000                dense_n += 1;
5001                if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
5002                    && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
5003                    && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
5004                {
5005                    dense_q4tp += 1;
5006                }
5007            }
5008        }
5009        // …and only where the MTP block itself runs on the graph. A MoE
5010        // draft block (Qwen3.6-35B-A3B, Apex-35B: 256 experts in the MTP
5011        // layer) fails `mtp_graph_ok`, so it drafts — and warms its cache
5012        // over the whole prompt — op by op on the host. Apex-35B q4tp on an
5013        // A40 + Xeon Gold 6342: greedy 51.8 against 65.6 tok/s plain (300
5014        // tokens, 57 % accepted), and 2.5 ms per prompt position (a
5015        // 4096-token ingest 171 against 295 tok/s); the published
5016        // Qwen3.6-35B-A3B 79.7 against 86.6. `CMF_GRAPH_SPEC=1` opts in.
5017        #[cfg(feature = "gpu")]
5018        let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| self.mtp_graph_ok(m));
5019        #[cfg(not(feature = "gpu"))]
5020        let mtp_on_graph = true;
5021        let spec_default_ok = (dense_n == 0 || dense_q4tp * 10 >= dense_n * 9) && mtp_on_graph;
5022        // Penalties break the draft head's agreement with the trunk (a
5023        // 1.1 repetition penalty measured 2 of 16 accepted): not by
5024        // default there either — off Metal that rule is untouched, and
5025        // suppressed ids keep counting as a penalty there, because no
5026        // measurement on a discrete card says otherwise.
5027        //
5028        // On native Metal the penalized arms DO pay: the draft applies
5029        // the same penalty and the verify scores the penalized rows
5030        // exactly (`greedy_pen`, the plain loop's arithmetic), so the
5031        // text is the plain path's and only the round's shape changes.
5032        // Measured on this M4 — see the report for the interleaved run.
5033        let penalized = !metal_graph
5034            && (self.sampler_config.repetition_penalty != 1.0
5035                || self.sampler_config.presence_penalty != 0.0
5036                || !self.sampler_config.suppress_tokens.is_empty());
5037        // …and not on wgpu-over-Metal: the batched verify graph there
5038        // returned 0 accepted drafts and garbage text on a GDN hybrid
5039        // (16.08, Qwen3.5-0.8B) while Vulkan is bit-exact; the Mac's
5040        // default backend is native Metal without a batch graph anyway.
5041        #[cfg(feature = "gpu")]
5042        let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
5043        #[cfg(not(feature = "gpu"))]
5044        let metal_wgpu = false;
5045        let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
5046        let spec_wanted = graph_spec_wanted_by_policy(
5047            spec_env.as_deref(),
5048            spec_default_ok,
5049            penalized,
5050            metal_wgpu,
5051            native_metal_backend,
5052        );
5053        if spec_wanted && metal_wgpu {
5054            tracing::warn!(
5055                "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
5056                 verified on this backend (garbage measured on Qwen3.5-0.8B)"
5057            );
5058        }
5059        // Native Metal: the b-row verify graph (`try_batch_graph_metal`)
5060        // stands where the wgpu batch graph stands on discrete cards
5061        // (`metal_graph`, above).
5062        let graph_spec = self.speculative
5063            && (graph_on || metal_graph)
5064            && self.mtp.is_some()
5065            && task_mask.is_none()
5066            && !self.o1_active()
5067            && spec_sampling_ok
5068            && spec_wanted;
5069        // The host fused pair is an optimization, not a verifier. On real
5070        // q4tp Qwen3.5 a near-tied row produced a different greedy argmax
5071        // from two serial target forwards. That makes a seemingly accepted
5072        // draft change text, which is worse than losing speculative speed.
5073        // Keep it explicitly experimental until a model/backend has passed
5074        // a long exact-ID gate; the native Metal batch verifier has its own
5075        // separate graph admission and is not controlled by this switch.
5076        //
5077        // GDN hybrids are also excluded by default: their recurrence is
5078        // sequential, so the pair lane cannot parallelize and measured 2x
5079        // slower on the 35B. `CMF_MTP_PAIR=1` is deliberately the only
5080        // opt-in for the host fused-pair experiment (not `CMF_MTP=1`).
5081        let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
5082        let pair_pays = host_pair_opt_in
5083            && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
5084        // Native Metal: say the route ONCE (RUST_LOG=info), including the
5085        // deliberate quality gate that keeps the unqualified batch verifier
5086        // off unless the user explicitly requested the experiment.
5087        #[cfg(target_os = "macos")]
5088        if metal_graph {
5089            static SAID: std::sync::Once = std::sync::Once::new();
5090            SAID.call_once(|| {
5091                let spec = if graph_spec {
5092                    let k = std::env::var("CMF_GRAPH_SPEC_K")
5093                        .ok()
5094                        .and_then(|v| v.parse::<usize>().ok())
5095                        .filter(|&v| (1..=8).contains(&v))
5096                        .unwrap_or(7);
5097                    let arm = if self.sampler_config.temperature < 1e-6 {
5098                        "greedy"
5099                    } else {
5100                        "sampling"
5101                    };
5102                    format!(
5103                        "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
5104                        Self::draft_vocab_rows(usize::MAX)
5105                    )
5106                } else if !self.speculative {
5107                    "spec off (CMF_MTP=0)".to_string()
5108                } else if self.mtp.is_none() {
5109                    "spec off (no MTP head)".to_string()
5110                } else if !spec_sampling_ok {
5111                    if spec_cheap_round {
5112                        "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
5113                    } else {
5114                        "spec off (sampling without a top-k: the dense chain \
5115                         costs more than it saves)"
5116                            .to_string()
5117                    }
5118                } else if !spec_wanted {
5119                    if spec_env.is_none() {
5120                        "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
5121                    } else {
5122                        "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
5123                    }
5124                } else if task_mask.is_some() {
5125                    "spec off (task mask)".to_string()
5126                } else if !graph_spec && !pair_pays {
5127                    "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
5128                        .to_string()
5129                } else {
5130                    "spec off (O(1) attention)".to_string()
5131                };
5132                let on = |var: &str| {
5133                    if std::env::var(var).as_deref() == Ok("0") {
5134                        "off"
5135                    } else {
5136                        "on"
5137                    }
5138                };
5139                tracing::info!(
5140                    "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
5141                     MTP graph {}, attend {}, probe {}",
5142                    if crate::gpu_metal::state4_on() {
5143                        "on"
5144                    } else {
5145                        "off"
5146                    },
5147                    if crate::gpu_metal::async_replay_on() {
5148                        "on"
5149                    } else {
5150                        "off"
5151                    },
5152                    on("CMF_METAL_PREFILL"),
5153                    on("CMF_MTP_GRAPH"),
5154                    std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
5155                    if crate::gpu::probe_enabled() {
5156                        "bypassed (q1 force)"
5157                    } else {
5158                        "off"
5159                    },
5160                );
5161            });
5162        }
5163        let spec_active = self.speculative
5164            && self.mtp.is_some()
5165            && task_mask.is_none()
5166            && !self.o1_active()
5167            && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
5168        // The MTP module is detached during generation so its mutable
5169        // state does not fight the borrow on `self`.
5170        let mut mtp = if spec_active { self.mtp.take() } else { None };
5171        if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
5172            eprintln!(
5173                "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
5174                mtp.is_some(),
5175                self.speculative,
5176                self.sampler_config.temperature < 1e-6,
5177            );
5178        }
5179        if let Some(m) = &mut mtp {
5180            m.kv.clear();
5181            // The MTP block's own device mirror starts over with its cache.
5182            crate::gpu::graph_kv_reset(self.mtp_kv_id());
5183            self.mtp_graph_mode = None;
5184        }
5185        // MiMo-V2's draft stack: greedy rounds (draft K with the chained
5186        // MTP layers, verify K+1 rows in one batched forward). Sampling
5187        // decodes plain; `CMF_MTP=0` / `CMF_MIMO_MTP=0` turn it off.
5188        let mimo_spec = self.speculative
5189            && self.mimo_mtp.is_some()
5190            && task_mask.is_none()
5191            && !self.o1_active()
5192            && self.dyn_router.is_none()
5193            && self.sampler_config.temperature < 1e-6
5194            && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
5195        if let Some(st) = self.mimo_mtp.as_mut() {
5196            st.reset();
5197            if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
5198                Self::mimo_mtp_hist_cap(st, input_ids.len());
5199            }
5200        }
5201        // Dynamic router detached during decode (same borrow trick as MTP).
5202        // Speculative decode and dynamic routing are mutually exclusive
5203        // for now — the fused-pair path doesn't carry per-token φ.
5204        let mut router = if mtp.is_none() {
5205            self.dyn_router.take()
5206        } else {
5207            None
5208        };
5209        let mut reuse_from = reuse_from;
5210        if let Some(r) = &mut router {
5211            r.reset(); // active=backbone, matching a fresh overlay
5212            self.dyn_phi_seen = 0; // fresh φ EMA per generation
5213            if self.dyn_active.is_some() {
5214                // A real switch back to the backbone invalidates the
5215                // cache the reuse key was computed against.
5216                let _ = self.set_active_skill(None);
5217                reuse_from = 0;
5218                self.last_prefill_tokens = input_ids.len();
5219            }
5220        }
5221
5222        let mut all_ids = input_ids.to_vec();
5223        let mut generated = 0usize;
5224        let mut finish_reason = "max_tokens".to_string();
5225        let mut drafted = 0usize;
5226        let mut accepted = 0usize;
5227        // DeepSeek-V4's draft quality is strongly content-dependent.  Two
5228        // consecutive paid rounds with no extra token put it on a bounded
5229        // cooldown; predictable text keeps batching, ordinary prose falls
5230        // back to the exact walk instead of paying a slow draft forever.
5231        // Local to one generation so one difficult request cannot poison the
5232        // next one, and deliberately automatic — this is not a user knob.
5233        let mut dsv4_spec_bad = 0usize;
5234        let mut dsv4_spec_retry_at = 0usize;
5235        let mut confidence: Vec<f32> = Vec::new();
5236        let trace_on = self.trace;
5237        let calib_temp = self.calib_temp;
5238        let mut traces: Vec<TokenTrace> = Vec::new();
5239
5240        // ── Prefill: forward each prompt token once, KEEP the last hidden.
5241        //    Dense prefill runs in fused pairs (weights streamed once per
5242        //    two positions — bit-identical to sequential, proven by the
5243        //    pair tests). With MTP: warm the draft head on
5244        //    (hidden_p, token_{p+1}) pairs.
5245        let mut hidden = vec![0.0f32; self.hidden_size];
5246        let mut pos = reuse_from;
5247        // lm_head-in-graph is only sound when the very next logits
5248        // consumer is this loop's own (MTP and skill routing interleave
5249        // other forwards / can swap lm_head between forward and sample).
5250        // CMF_GPU_LMHEAD=0 keeps lm_head off the graph: the token reads back
5251        // the 8 KB hidden instead of ~1 MB of logits, and the head runs on
5252        // the host. A probe for how much of the graph's fixed per-token cost
5253        // is the logits readback (the layer sweep puts that fixed part at
5254        // 3.88 ms of an 18.5 ms frame).
5255        let fuse_lm = mtp.is_none()
5256            && router.is_none()
5257            && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5258        self.graph_logits = None;
5259        self.graph_want_logits = false;
5260        let _tpf = std::time::Instant::now();
5261        let batch_k = self.generation_batch_k();
5262        if let Some(rows) = prompt_rows {
5263            let hs = self.hidden_size;
5264            let chunk = self.prefill_chunk().max(1);
5265            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5266                let end = (pos + chunk).min(input_ids.len());
5267                let hb = match self.prefill_input_rows(
5268                    PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5269                    pos,
5270                    task_mask,
5271                ) {
5272                    Ok(hb) => hb,
5273                    Err(err) => {
5274                        self.finish_generation(&mut mtp, &mut router, true);
5275                        return Err(err);
5276                    }
5277                };
5278                if mimo_spec {
5279                    self.mimo_note_rows(&hb, pos);
5280                }
5281                hidden.copy_from_slice(&hb[hb.len() - hs..]);
5282                pos = end;
5283            }
5284        }
5285        // DeepSeek-V4 owns a separate hyper-connection stack. Route it
5286        // before the generic prefill choices: those correctly reject an
5287        // empty `weights.layers`, but their final per-position fallback used
5288        // to consume the whole prompt before `dsv4::forward_chunk` could see
5289        // it. The batch implementation therefore existed without a live
5290        // production entry point.
5291        //
5292        // Bounded chunks preserve cancellation responsiveness. Only the
5293        // prompt's final chunk asks for logits; every earlier head projection
5294        // would produce 129 280 values that no caller reads.
5295        while self.qwen4_exp.is_some()
5296            && mtp.is_none()
5297            && pos < input_ids.len()
5298            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5299        {
5300            // The device path takes several prompt tokens per layer frame;
5301            // the host path runs them one by one inside the same call.
5302            let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5303            let want_logits = end == input_ids.len();
5304            let mut lg = Vec::new();
5305            if let Some(b) = &mut self.qwen4_exp {
5306                crate::qwen4_exp::forward_tokens(
5307                    &b.0,
5308                    &b.1,
5309                    &b.2,
5310                    &mut b.3,
5311                    &input_ids[pos..end],
5312                    pos,
5313                    &self.inv_freq,
5314                    self.pool.as_deref(),
5315                    &mut lg,
5316                    want_logits,
5317                );
5318            }
5319            if want_logits {
5320                self.graph_logits = Some(lg);
5321            }
5322            pos = end;
5323            hidden.fill(0.0);
5324        }
5325        while self.dsv4.is_some()
5326            && mtp.is_none()
5327            && pos < input_ids.len()
5328            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5329        {
5330            let end = (pos + prefill_chunk()).min(input_ids.len());
5331            let ids: Vec<u32> = input_ids[pos..end].to_vec();
5332            let mut lg = Vec::new();
5333            if let Some(b) = &mut self.dsv4 {
5334                let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5335                crate::dsv4::forward_chunk(
5336                    g,
5337                    layers,
5338                    &cfg,
5339                    st,
5340                    &ids,
5341                    pos,
5342                    &self.inv_freq,
5343                    self.pool.as_deref(),
5344                    &mut lg,
5345                    end == input_ids.len(),
5346                );
5347            }
5348            if end == input_ids.len() {
5349                self.graph_logits = Some(lg);
5350            }
5351            pos = end;
5352            hidden = vec![0.0; self.hidden_size];
5353        }
5354        let dsv41_prefill = self.dsv41_prefill.take();
5355        while self.dsv41.is_some()
5356            && mtp.is_none()
5357            && pos < input_ids.len()
5358            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5359        {
5360            let end = (pos + prefill_chunk()).min(input_ids.len());
5361            let ids: Vec<u32> = input_ids[pos..end].to_vec();
5362            let mut lg = Vec::new();
5363            if let Some(b) = &mut self.dsv41 {
5364                let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5365                if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5366                    crate::dsv41::forward_chunk_masked_with_embeddings(
5367                        g,
5368                        layers,
5369                        cfg,
5370                        st,
5371                        &ids,
5372                        pos,
5373                        &embeddings[pos..end],
5374                        &participates[pos..end],
5375                        self.pool.as_deref(),
5376                        &mut lg,
5377                    );
5378                } else {
5379                    crate::dsv41::forward_chunk(
5380                        g,
5381                        layers,
5382                        cfg,
5383                        st,
5384                        &ids,
5385                        pos,
5386                        self.pool.as_deref(),
5387                        &mut lg,
5388                    );
5389                }
5390            }
5391            if end == input_ids.len() {
5392                self.graph_logits = Some(lg);
5393            }
5394            pos = end;
5395            hidden = vec![0.0; self.hidden_size];
5396        }
5397        // With dynamic routing, prefill sequentially so the φ hook fires
5398        // over the PROMPT — the router enters decode with a warm φ (the
5399        // fused-pair path skips the per-layer φ capture). o1 layers
5400        // collect their query trace in both the single and pair paths.
5401        let dyn_prefill = router.is_some();
5402        // Optional bounded calibration prefix for generation.  The normal
5403        // O(1) path seals after the full prompt; this explicit knob instead
5404        // runs only the requested prefix through exact attention, seals the
5405        // Nyström state, and streams the rest of the prompt through the same
5406        // O(1) step used by decode.  It keeps the O(1) layers' Q trace and
5407        // temporary full KV bounded by the prefix while leaving the default
5408        // full-prompt quality profile untouched.
5409        let o1_prefill_limit = o1_prefill
5410            .and_then(|requested| self.o1_effective_boundary(requested))
5411            .map(|boundary| boundary.min(input_ids.len()));
5412        let mut o1_sealed = false;
5413        if let Some(limit) = o1_prefill_limit {
5414            // Reuse the exact batched prefix machinery when available; it
5415            // records the same per-position Q trace as the full prefill.
5416            if self.can_prefill_batched() && limit > 2 {
5417                let chunk = self.prefill_chunk();
5418                let hs = self.hidden_size;
5419                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5420                    let end = (pos + chunk).min(limit);
5421                    let hb = self.prefill_batch(&input_ids[pos..end], pos);
5422                    hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5423                    pos = end;
5424                }
5425            } else {
5426                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5427                    hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5428                    pos += 1;
5429                }
5430            }
5431            if pos >= limit {
5432                o1_sealed = match self.o1_seal_checked() {
5433                    Ok(sealed) => sealed,
5434                    Err(err) => {
5435                        self.finish_generation(&mut mtp, &mut router, true);
5436                        return Err(err);
5437                    }
5438                };
5439                tracing::info!(
5440                    "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5441                    o1_prefill.unwrap_or(0),
5442                    self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5443                        .unwrap_or(limit),
5444                    limit,
5445                    input_ids.len()
5446                );
5447            }
5448        }
5449        // q1 hybrids on Metal: the per-position GPU token graph beats
5450        // the CPU chunk-GEMM (whose wall is the sequential scalar GDN
5451        // recurrence), so prefill goes position-by-position through the
5452        // same graph as decode. Pure-attention models keep the batched
5453        // path — there the chunk-GEMM amortization wins.
5454        let graph_prefill = self.graph_prefill_preferred();
5455        // Native Metal, q4tp GDN hybrids: the prompt through the b-row
5456        // rows graph — projections as GEMMs over up to 512 positions, the
5457        // GDN recurrence in registers on the device, K/V rows appended by
5458        // the chunk — instead of one token-graph submit per position (the
5459        // 27B: 8 tok/s → GEMM-bound). The MTP warm-up rows come out of one
5460        // batched run of the block per chunk. Any refusal leaves the rest
5461        // of the prompt to the sequential paths below.
5462        #[cfg(target_os = "macos")]
5463        if task_mask.is_none()
5464            && !dyn_prefill
5465            && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5466            && crate::gpu::enabled_here()
5467            && self.gdn_cfg.is_some()
5468            && !self.per_token_stack()
5469            && input_ids.len() > 8
5470            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5471            && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5472        {
5473            let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5474                .ok()
5475                .and_then(|v| v.parse().ok())
5476                .filter(|&v| (16..=512).contains(&v))
5477                .unwrap_or(256);
5478            let hs = self.hidden_size;
5479            let _tp = std::time::Instant::now();
5480            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5481                let end = (pos + chunk).min(input_ids.len());
5482                let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5483                    MetalPrefillOutcome::Completed(hb) => hb,
5484                    MetalPrefillOutcome::Declined => break,
5485                    MetalPrefillOutcome::Failed => {
5486                        self.finish_generation(&mut mtp, &mut router, true);
5487                        return Err("ordinary Metal prefill failed after admission".into());
5488                    }
5489                };
5490                if let Some(m) = &mut mtp {
5491                    let n_pairs = if end < input_ids.len() {
5492                        end - pos
5493                    } else {
5494                        end - pos - 1
5495                    };
5496                    if n_pairs > 0 {
5497                        let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5498                            .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5499                            .collect();
5500                        if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5501                            for (j, (h, t)) in pairs.iter().enumerate() {
5502                                let h = h.to_vec();
5503                                let _ = self.mtp_step(m, &h, *t, pos + j);
5504                            }
5505                        }
5506                    }
5507                }
5508                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5509                pos = end;
5510            }
5511            if std::env::var("CMF_PREFILL_PROF").is_ok() {
5512                eprintln!(
5513                    "metal-prefill: {} of {} tokens in {:.1} ms",
5514                    pos,
5515                    input_ids.len(),
5516                    _tp.elapsed().as_secs_f64() * 1e3
5517                );
5518            }
5519        }
5520        // Gemma-4 on a discrete card: the prompt through the batched wgpu
5521        // graph (`g4_batch_ingest`); its K/V land in the device mirrors
5522        // decode reads. A refusal leaves the rest of the prompt to the
5523        // chunked host prefill below.
5524        #[cfg(not(target_os = "macos"))]
5525        if task_mask.is_none()
5526            && !dyn_prefill
5527            && mtp.is_none()
5528            && o1_prefill.is_none()
5529            && batch_k > 0
5530            && pos + 1 < input_ids.len()
5531            && self.g4_batch_prefill()
5532        {
5533            let t_g4 = std::time::Instant::now();
5534            match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5535                Ok((n, last)) => {
5536                    if n > 0 {
5537                        hidden.copy_from_slice(&last);
5538                    }
5539                    if std::env::var("CMF_PREFILL_PROF").is_ok() {
5540                        eprintln!(
5541                            "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5542                            input_ids.len() - pos,
5543                            t_g4.elapsed().as_secs_f64() * 1e3
5544                        );
5545                    }
5546                    pos += n;
5547                }
5548                Err(e) => {
5549                    self.finish_generation(&mut mtp, &mut router, true);
5550                    return Err(e);
5551                }
5552            }
5553        }
5554        self.mimo_moe_prepare();
5555        // A MoE stack larger than the card (MiMo-V2 q4tp on 96 GB): the
5556        // batched wgpu graph runs the device prefix of every chunk — its
5557        // experts resident — and the host's batched layer walk finishes
5558        // the chunk. Any refusal leaves the rest of the prompt to the
5559        // chunked prefill below.
5560        #[cfg(not(target_os = "macos"))]
5561        if task_mask.is_none()
5562            && !dyn_prefill
5563            && !graph_prefill
5564            && mtp.is_none()
5565            && o1_prefill.is_none()
5566            && !self.o1_active()
5567            && input_ids.len() > 2
5568            && self.batch_prefix_prefill()
5569        {
5570            let chunk = self.prefill_chunk().max(1);
5571            let hs = self.hidden_size;
5572            let t_bp = std::time::Instant::now();
5573            let pos0 = pos;
5574            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5575                let end = (pos + chunk).min(input_ids.len());
5576                let bk = end - pos;
5577                let mut hiddens = vec![0f32; bk * hs];
5578                for (j, &id) in input_ids[pos..end].iter().enumerate() {
5579                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5580                }
5581                let positions: Vec<usize> = (pos..end).collect();
5582                let mut run = 0usize;
5583                let outcome = self.try_batch_graph_wgpu_prefix(
5584                    &mut hiddens,
5585                    &positions,
5586                    bk,
5587                    None,
5588                    Some(&mut run),
5589                );
5590                match outcome {
5591                    crate::gpu::BatchGraphOutcome::Completed => {
5592                        let hb = if run < self.num_layers {
5593                            self.prefill_batch_span(
5594                                PrefillIn::Hidden(&hiddens),
5595                                pos,
5596                                None,
5597                                run,
5598                                self.num_layers,
5599                            )
5600                        } else {
5601                            hiddens
5602                        };
5603                        if mimo_spec {
5604                            self.mimo_note_rows(&hb, pos);
5605                        }
5606                        hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5607                        pos = end;
5608                    }
5609                    crate::gpu::BatchGraphOutcome::Failed => {
5610                        self.finish_generation(&mut mtp, &mut router, true);
5611                        return Err("batched prefix prefill failed after admission".into());
5612                    }
5613                    crate::gpu::BatchGraphOutcome::Declined => {
5614                        // Earlier chunks left their prefix rows on the
5615                        // device only: the host walk below needs them.
5616                        #[cfg(feature = "gpu")]
5617                        if pos > pos0 {
5618                            self.pull_lagging_host_kv(0, self.num_layers, pos);
5619                        }
5620                        break;
5621                    }
5622                }
5623            }
5624            if std::env::var("CMF_PREFILL_PROF").is_ok() {
5625                eprintln!(
5626                    "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5627                    pos - pos0,
5628                    input_ids.len(),
5629                    t_bp.elapsed().as_secs_f64() * 1e3
5630                );
5631            }
5632        }
5633        if task_mask.is_none()
5634            && !dyn_prefill
5635            && !graph_prefill
5636            && self.can_prefill_batched()
5637            && !self.per_token_stack()
5638            && o1_prefill.is_none()
5639            && input_ids.len() > 2
5640        {
5641            // Production prefill = the same chunked prefill-GEMM that
5642            // bench/PPL measure (roadmap §3 P0: generation used to warm
5643            // the prompt with the slower pair path — the published
5644            // prefill number didn't match real TTFT). MTP warm-up reads
5645            // each position's hidden straight from the chunk result.
5646            let chunk = self.prefill_chunk();
5647            let hs = self.hidden_size;
5648            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5649                let end = (pos + chunk).min(input_ids.len());
5650                let hb = self.prefill_batch(&input_ids[pos..end], pos);
5651                if mimo_spec {
5652                    self.mimo_note_rows(&hb, pos);
5653                }
5654                if let Some(m) = &mut mtp {
5655                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5656                        .ok()
5657                        .and_then(|v| v.parse().ok())
5658                        .unwrap_or(0);
5659                    for p in pos..end {
5660                        if p + 1 < input_ids.len() {
5661                            if probe >= 1 && p + 2 < input_ids.len() {
5662                                // Teacher-forced chain acceptance (see the
5663                                // tail loop's twin): the warm-up row stays,
5664                                // the chain's rows roll back.
5665                                let (d1, mut hx) = self.mtp_step_h(
5666                                    m,
5667                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
5668                                    input_ids[p + 1],
5669                                    p,
5670                                );
5671                                let mut ok = d1 == input_ids[p + 2];
5672                                Self::chain_probe_note(0, ok);
5673                                let mut d_prev = d1;
5674                                let mut extra = 0usize;
5675                                for j in 1..probe {
5676                                    if p + 2 + j >= input_ids.len() {
5677                                        break;
5678                                    }
5679                                    let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5680                                    extra += 1;
5681                                    ok = ok && dj == input_ids[p + 2 + j];
5682                                    Self::chain_probe_note(j, ok);
5683                                    d_prev = dj;
5684                                    hx = hj;
5685                                }
5686                                m.kv.truncate_last(extra);
5687                            } else {
5688                                let _ = self.mtp_step(
5689                                    m,
5690                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
5691                                    input_ids[p + 1],
5692                                    p,
5693                                );
5694                            }
5695                        }
5696                    }
5697                }
5698                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5699                pos = end;
5700            }
5701        }
5702        let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5703        if task_mask.is_none()
5704            && !dyn_prefill
5705            && !graph_prefill
5706            && !pair_off
5707            && self.pair_supported()
5708            && o1_prefill.is_none()
5709        {
5710            while pos + 1 < input_ids.len()
5711                && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5712            {
5713                let e1 = self.embed_single(input_ids[pos]);
5714                let e2 = self.embed_single(input_ids[pos + 1]);
5715                let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5716                if mimo_spec {
5717                    self.mimo_note_rows(&h1, pos);
5718                    self.mimo_note_rows(&h2, pos + 1);
5719                }
5720                // Both prefill tokens are real → commit lane-2 states.
5721                self.commit_linear_scratch();
5722                if let Some(m) = &mut mtp {
5723                    let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5724                    if pos + 2 < input_ids.len() {
5725                        let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5726                            .ok()
5727                            .and_then(|v| v.parse().ok())
5728                            .unwrap_or(0);
5729                        if probe >= 1 && pos + 3 < input_ids.len() {
5730                            // Same teacher-forced chain table as the tail
5731                            // loop below, fed from the pair path that owns
5732                            // most prefill positions.
5733                            let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5734                            let mut ok = d1 == input_ids[pos + 3];
5735                            Self::chain_probe_note(0, ok);
5736                            let mut d_prev = d1;
5737                            let mut extra = 0usize;
5738                            for j in 1..probe {
5739                                if pos + 3 + j >= input_ids.len() {
5740                                    break;
5741                                }
5742                                let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5743                                extra += 1;
5744                                ok = ok && dj == input_ids[pos + 3 + j];
5745                                Self::chain_probe_note(j, ok);
5746                                d_prev = dj;
5747                                hx = hj;
5748                            }
5749                            m.kv.truncate_last(extra);
5750                        } else {
5751                            let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5752                        }
5753                    }
5754                }
5755                hidden = h2;
5756                pos += 2;
5757            }
5758        }
5759        // Batched GPU prefill for the wgpu decode graph (GDN hybrids): K prompt
5760        // positions per submit — projections/FFN as GEMMs (weight once per K),
5761        // attention/GDN looped inside — instead of one whole-graph submit per
5762        // position. Falls through to the per-position graph on any refusal.
5763        // Batched prefill is opt-in (CMF_BATCH_K>0). Default 0 = per-position
5764        // graph prefill. (Steady-state decode is provably identical either way —
5765        // token-graph submit and lm_head both unchanged — so this only trades
5766        // prefill wall.)
5767        // A bounded O(1) prefix is the one post-seal prompt interval: only
5768        // admit its batch when the device O(1) route is explicitly enabled and
5769        // every sealed layer exposes a portable view. The same batch size and
5770        // refusal behavior remain the ordinary controls/comparator.
5771        let o1_batch_ready = o1_sealed
5772            && o1_prefill.is_some()
5773            && mtp.is_none()
5774            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5775            && (0..self.num_layers).all(|li| {
5776                let cache = &self.kv_cache.layers[self.phys_layer(li)];
5777                cache.o1.is_none() || cache.o1_views().is_some()
5778            });
5779        // The ordinary graph-prefill route can share each completed trunk
5780        // chunk with an attached MTP head.  Keep chain probing on its
5781        // established per-position path: the probe deliberately needs every
5782        // teacher-forced draft row and its rollback table.
5783        let mtp_batch_prefill = mtp.is_some()
5784            && graph_prefill
5785            && task_mask.is_none()
5786            && !dyn_prefill
5787            && !self.o1_active()
5788            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5789        if batch_k > 0
5790            && (graph_prefill || o1_batch_ready)
5791            && task_mask.is_none()
5792            && (!self.o1_active() || o1_batch_ready)
5793            && (mtp.is_none() || mtp_batch_prefill)
5794            && !dyn_prefill
5795            && pos + 1 < input_ids.len()
5796        {
5797            let hs = self.hidden_size;
5798            let chunk = batch_k;
5799            while pos < input_ids.len() {
5800                let end = (pos + chunk).min(input_ids.len());
5801                let bk = end - pos;
5802                let mut hiddens = vec![0f32; bk * hs];
5803                for (j, &id) in input_ids[pos..end].iter().enumerate() {
5804                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5805                }
5806                let positions: Vec<usize> = (pos..end).collect();
5807                let t_chunk = std::time::Instant::now();
5808                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5809                let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5810                if std::env::var("CMF_GRAPH_PROF").is_ok() {
5811                    let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5812                    eprintln!(
5813                        "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5814                        if o1_batch_ready {
5815                            "o1"
5816                        } else if mtp_batch_prefill {
5817                            "ordinary_mtp"
5818                        } else {
5819                            "ordinary"
5820                        },
5821                        bk as f64 / (ms / 1000.0)
5822                    );
5823                }
5824                {
5825                    use std::sync::atomic::{AtomicBool, Ordering};
5826                    static SAID: AtomicBool = AtomicBool::new(false);
5827                    if !SAID.swap(true, Ordering::Relaxed) {
5828                        if ok_b {
5829                            tracing::info!(
5830                                "batched prefill: ACTIVE mode={} (k={bk})",
5831                                if o1_batch_ready {
5832                                    "o1"
5833                                } else if mtp_batch_prefill {
5834                                    "ordinary_mtp"
5835                                } else {
5836                                    "ordinary"
5837                                }
5838                            );
5839                        } else {
5840                            tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5841                        }
5842                    }
5843                }
5844                if ok_b {
5845                    if mimo_spec {
5846                        self.mimo_note_rows(&hiddens, pos);
5847                    }
5848                    if mtp_batch_prefill {
5849                        let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5850                        if n_pairs > 0 {
5851                            // `hiddens` is owned by this chunk, so materialize
5852                            // row slices before borrowing the detached MTP
5853                            // module.  The last prompt row has no successor;
5854                            // the helper above is the single source of that
5855                            // boundary rule.
5856                            let rows: Vec<Vec<f32>> = (0..n_pairs)
5857                                .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5858                                .collect();
5859                            let pairs: Vec<(&[f32], u32)> = rows
5860                                .iter()
5861                                .enumerate()
5862                                .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5863                                .collect();
5864                            if std::env::var("CMF_GRAPH_PROF").is_ok() {
5865                                eprintln!(
5866                                    "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5867                                    pos,
5868                                    n_pairs,
5869                                    pos + n_pairs - 1,
5870                                );
5871                            }
5872                            let warm_error = if let Some(m) = mtp.as_mut() {
5873                                self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5874                            } else {
5875                                None
5876                            };
5877                            if let Some(err) = warm_error {
5878                                // The trunk batch was already admitted.  A
5879                                // failed MTP warm-up therefore clears both
5880                                // mirrors and exits; continuing would pair a
5881                                // current trunk state with a stale MTP cache.
5882                                self.finish_generation(&mut mtp, &mut router, true);
5883                                return Err(err.to_string());
5884                            }
5885                        }
5886                    }
5887                    hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5888                    pos = end;
5889                } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5890                    // A failed batch may have advanced a device recurrent
5891                    // state (ordinary GDN or sealed O(1)). A CPU fallback
5892                    // would then observe stale accumulators, so clear the
5893                    // request state and make the failure explicit.
5894                    self.finish_generation(&mut mtp, &mut router, true);
5895                    return Err(if o1_batch_ready {
5896                        "sealed O(1) batch graph failed after admission".to_string()
5897                    } else {
5898                        "ordinary recurrent batch graph failed after admission".to_string()
5899                    });
5900                } else {
5901                    break; // unsupported → per-position graph handles the rest
5902                }
5903            }
5904        }
5905        // Resident Embryo graph: the prompt in chunks of one submit each
5906        // instead of one whole-graph submit per position; the last chunk
5907        // carries the logits exactly as the per-position walk would.
5908        if graph_prefill
5909            && task_mask.is_none()
5910            && mtp.is_none()
5911            && !dyn_prefill
5912            && pos == 0
5913            && input_ids.len() > 1
5914            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5915        {
5916            if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5917                self.graph_logits = Some(lg);
5918                hidden = vec![0.0; self.hidden_size];
5919                pos = input_ids.len();
5920            }
5921        }
5922        while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5923            self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5924            hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5925            if mimo_spec {
5926                self.mimo_note_rows(&hidden, pos);
5927            }
5928            if let Some(m) = &mut mtp {
5929                if pos + 1 < input_ids.len() {
5930                    // `CMF_MTP_CHAIN_PROBE=k`: teacher-forced acceptance of a
5931                    // CHAINED draft — iterate the head on its own hidden k
5932                    // deep and score every depth against the prompt's real
5933                    // continuation. The economics of a k-token speculative
5934                    // round stand or fall on this table.
5935                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5936                        .ok()
5937                        .and_then(|v| v.parse().ok())
5938                        .unwrap_or(0);
5939                    if probe >= 1 && pos + 2 < input_ids.len() {
5940                        let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5941                        let mut ok = d1 == input_ids[pos + 2];
5942                        Self::chain_probe_note(0, ok);
5943                        let mut d_prev = d1;
5944                        let mut extra = 0usize;
5945                        for j in 1..probe {
5946                            if pos + 2 + j >= input_ids.len() {
5947                                break;
5948                            }
5949                            let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5950                            extra += 1;
5951                            ok = ok && dj == input_ids[pos + 2 + j];
5952                            Self::chain_probe_note(j, ok);
5953                            d_prev = dj;
5954                            hx = hj;
5955                        }
5956                        // The chain's rows are speculation, not the prompt —
5957                        // keep only the warmup row the plain path would add.
5958                        m.kv.truncate_last(extra);
5959                    } else {
5960                        let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5961                    }
5962                }
5963            }
5964            pos += 1;
5965        }
5966        if std::env::var("CMF_PREFILL_PROF").is_ok() {
5967            eprintln!(
5968                "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5969                input_ids.len(),
5970                _tpf.elapsed().as_secs_f64() * 1000.0
5971            );
5972        }
5973        if self
5974            .graph_failed
5975            .swap(false, std::sync::atomic::Ordering::Relaxed)
5976        {
5977            // MTP is detached for speculative generation.  Restore the
5978            // module before returning the terminal graph error; otherwise a
5979            // failed request would silently remove the head from a pooled
5980            // pipeline and the next request would lose its configured route.
5981            self.finish_generation(&mut mtp, &mut router, true);
5982            return Err("GPU token graph failed during prefill".to_string());
5983        }
5984        // Cancelled mid-prefill: the cache holds a partial prompt —
5985        // drop the reuse history and return an empty generation.
5986        if self
5987            .cancel
5988            .swap(false, std::sync::atomic::Ordering::Relaxed)
5989        {
5990            // A cancelled prefill can already have advanced the device
5991            // mirror. Drop the whole partial sequence so a pooled pipeline
5992            // cannot carry that state into its next request.
5993            self.finish_generation(&mut mtp, &mut router, true);
5994            return Ok(GenerateResult {
5995                text: String::new(),
5996                token_ids: Vec::new(),
5997                prompt_tokens: input_ids.len(),
5998                tokens_generated: 0,
5999                finish_reason: "cancelled".to_string(),
6000                mtp_drafted: 0,
6001                mtp_accepted: 0,
6002                token_confidence: Vec::new(),
6003                traces: Vec::new(),
6004            });
6005        }
6006
6007        // Prompt absorbed → freeze the o1 layers' skeletons; from here
6008        // every decode step on those layers is O(W + m·dv + m²).
6009        if !o1_sealed {
6010            match self.o1_seal_checked() {
6011                Ok(_) => {}
6012                Err(err) => {
6013                    self.finish_generation(&mut mtp, &mut router, true);
6014                    return Err(err);
6015                }
6016            }
6017        }
6018
6019        // Commit one token: push, check EOS, stream. Returns false = stop.
6020        macro_rules! commit {
6021            ($id:expr) => {{
6022                all_ids.push($id);
6023                generated += 1;
6024                self.note_draft_id($id);
6025                if self.tokenizer.is_eos($id) && !self.ignore_eos {
6026                    finish_reason = "stop".to_string();
6027                    false
6028                } else {
6029                    let token_text = self.tokenizer.decode_token($id);
6030                    let mut go = true;
6031                    if let Some(ref mut cb) = on_token {
6032                        if !cb(&token_text) {
6033                            finish_reason = "cancelled".to_string();
6034                            go = false;
6035                        }
6036                    }
6037                    go
6038                }
6039            }};
6040        }
6041
6042        // Speculation is decided by MEASUREMENT, not by an acceptance
6043        // model. A k=4 round costs ~3.8 plain tokens on the 5090 (draft
6044        // 6.6 + verify 66.6 + commit 4.8 ms against a 20.6 ms token), so it
6045        // pays only when the head lands ~2.8 of 4 — predictable text (code,
6046        // structured output) does, free prose often does not, and the
6047        // ratio at which the two cross depends on the card and the context
6048        // depth. So: four speculative rounds timed, then eight plain
6049        // tokens timed, and the faster arm runs until a re-check 256
6050        // tokens later (context growth moves the balance). The trial
6051        // costs at most a few tokens of the slower arm per 256.
6052        let mut spec_trial = SpecTrial::Spec {
6053            t0: std::time::Instant::now(),
6054            gen0: generated,
6055            rounds: 0,
6056        };
6057        // The token-count proxy prices a round at ~1.9 plain tokens. That
6058        // holds for the Metal rounds whose cost was measured — greedy and
6059        // the sparse sampling chain — so an expensive round (the dense
6060        // chain, reachable only by `CMF_GRAPH_SPEC_SAMPLE=1`) still times
6061        // the plain path before it decides.
6062        let mut spec_mon = SpecMon {
6063            metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
6064            ..SpecMon::default()
6065        };
6066        let mut spec_watchdog_off = false;
6067        // CMF_GRAPH_SPEC_TIME: the round walls so far (round 1 excluded —
6068        // it pays the scratch), for the outlier test on each new one
6069        let mut spec_walls: Vec<f32> = Vec::new();
6070        // ... and the end of the last round: the host time between rounds
6071        // (token commits, streaming, the loop top) is printed at level 2
6072        let mut spec_round_end: Option<std::time::Instant> = None;
6073        if mimo_spec {
6074            if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
6075                if let Some(mut st) = self.mimo_mtp.take() {
6076                    self.mimo_mtp_probe(&mut st, input_ids, &path);
6077                    self.mimo_mtp = Some(st);
6078                }
6079            }
6080        }
6081        // ── Decode ──
6082        let mut next_pos = input_ids.len();
6083        'decode: while generated < max_tokens {
6084            if self
6085                .graph_failed
6086                .swap(false, std::sync::atomic::Ordering::Relaxed)
6087            {
6088                // Keep the detached MTP module attached after a terminal
6089                // graph error so the pipeline can be reused for a fresh
6090                // sequence.  `clear_sequence_state` only clears mirrors and
6091                // host KV; it cannot recover a module dropped here.
6092                self.finish_generation(&mut mtp, &mut router, true);
6093                return Err("GPU token graph failed during decode".to_string());
6094            }
6095            if self
6096                .cancel
6097                .swap(false, std::sync::atomic::Ordering::Relaxed)
6098            {
6099                finish_reason = "cancelled".to_string();
6100                break 'decode;
6101            }
6102            // A rejected speculative draft already drew this position's
6103            // token from the residual distribution (graph_spec_step); it
6104            // is committed as-is — sampling again from the row's logits
6105            // would bias the stream toward the target's mode.
6106            if mimo_spec && next_pos > 0 {
6107                // Every path leaves `hidden` = the backbone output at
6108                // next_pos-1; the draft layers read it (idempotent).
6109                self.mimo_note_rows(&hidden, next_pos - 1);
6110            }
6111            let forced = self.spec_forced.take();
6112            let mut logits = match (forced, self.graph_logits.take()) {
6113                (Some(_), _) => Vec::new(),
6114                (None, Some(lg)) => lg,
6115                (None, None) => {
6116                    let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
6117                    // Pooled-tail mode: the head as one layer-less device
6118                    // pass (final-norm + matvec + readback). Any decline —
6119                    // no pool, unresolvable weight, backend off — keeps the
6120                    // host head exactly as before.
6121                    #[cfg(feature = "gpu")]
6122                    let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
6123                        lg
6124                    } else {
6125                        inference::rms_norm_into(
6126                            &hidden,
6127                            &self.weights.final_norm,
6128                            self.rms_eps,
6129                            self.norm_style,
6130                            &mut self.ws.n1,
6131                        );
6132                        self.lm_head_forward(&self.ws.n1)
6133                    };
6134                    #[cfg(not(feature = "gpu"))]
6135                    let logits_out = {
6136                        inference::rms_norm_into(
6137                            &hidden,
6138                            &self.weights.final_norm,
6139                            self.rms_eps,
6140                            self.norm_style,
6141                            &mut self.ws.n1,
6142                        );
6143                        self.lm_head_forward(&self.ws.n1)
6144                    };
6145                    logits_out
6146                }
6147            };
6148            // CMF_LOGIT_DUMP=<path>: the first decode step's hidden + logits
6149            // as raw f32 (hidden first) — cross-backend numerics diffing.
6150            if generated
6151                == std::env::var("CMF_LOGIT_DUMP_STEP")
6152                    .ok()
6153                    .and_then(|v| v.parse().ok())
6154                    .unwrap_or(0)
6155            {
6156                if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
6157                    let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
6158                    for v in hidden.iter().chain(logits.iter()) {
6159                        bytes.extend_from_slice(&v.to_le_bytes());
6160                    }
6161                    if let Err(e) = std::fs::write(&path, &bytes) {
6162                        eprintln!("logit dump: failed to write {path}: {e}");
6163                        self.finish_generation(&mut mtp, &mut router, true);
6164                        return Err(format!("logit dump write failed: {e}"));
6165                    }
6166                }
6167            }
6168            // CMF_LOGIT_DUMP_ALL=<dir>: every decode step's logits as raw
6169            // f32, `<dir>/step{n:05}.f32` — step-by-step backend diffing
6170            // (a greedy run on two backends compares until they diverge).
6171            if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
6172                if !logits.is_empty() {
6173                    let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
6174                    let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
6175                    if let Err(e) =
6176                        std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
6177                    {
6178                        eprintln!("logit dump: failed to write {}: {e}", path.display());
6179                    }
6180                }
6181            }
6182            let t_next = match forced.or_else(|| forced_ids_at(generated)) {
6183                Some(c) => c,
6184                None => {
6185                    let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
6186                    sampler::sample_with_scratch_pool(
6187                        &logits,
6188                        &self.sampler_config,
6189                        self.sampler_config.penalty_past(&all_ids, bounded_native),
6190                        &mut self.rng,
6191                        &mut self.sampler_scratch,
6192                        self.pool.as_deref(),
6193                    )
6194                }
6195            };
6196            if self.confidence_on {
6197                confidence.push(if logits.is_empty() {
6198                    0.0
6199                } else {
6200                    sampler::top1_prob_pool(
6201                        self.pool.as_deref(),
6202                        &mut self.sampler_scratch,
6203                        &logits,
6204                        t_next,
6205                        calib_temp,
6206                    )
6207                });
6208            }
6209            if !logits.is_empty() {
6210                attention::recycle_buf(&mut logits);
6211            }
6212            if trace_on {
6213                // active_skill = the overlay in force while this token was
6214                // generated; recon/switched are filled after the post-emit
6215                // routing eval below (freshest coherence for this token).
6216                let skill = router.as_ref().and_then(|r| r.active_id());
6217                traces.push(TokenTrace {
6218                    t: generated,
6219                    token_id: t_next,
6220                    confidence: confidence.last().copied().unwrap_or(0.0),
6221                    active_skill: skill,
6222                    recon: None,
6223                    switched: false,
6224                });
6225            }
6226            if !commit!(t_next) {
6227                break 'decode;
6228            }
6229            if generated >= max_tokens {
6230                break 'decode;
6231            }
6232
6233            // Catch-all for decode paths that bypass the walks' own trim
6234            // (a graph step, a speculative round).
6235            self.swa_trim_tails();
6236            if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
6237                // Say it ONCE, loudly: past this point the model keeps
6238                // talking but has lost half its context, and on a GDN
6239                // hybrid the graph's device state goes stale on top. The
6240                // Qwen3.8 bring-up spent a day reading this cliff as
6241                // three different model bugs.
6242                static SAID: std::sync::Once = std::sync::Once::new();
6243                SAID.call_once(|| {
6244                    tracing::warn!(
6245                        "KV cache full at {} positions — evicting half; quality \
6246                         will degrade. Raise CMF_MAX_SEQ.",
6247                        self.kv_cache.max_seq_len,
6248                    );
6249                });
6250                let keep = (self.kv_cache.max_seq_len / 2).max(1);
6251                self.kv_cache.evict(keep);
6252            }
6253
6254            // Advance the speculation trial: plain-phase accounting and
6255            // the periodic re-check happen here, on every token.
6256            if graph_spec {
6257                match spec_trial {
6258                    SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6259                        spec_mon.plain_ms =
6260                            t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6261                        let keep = spec_mon.pays();
6262                        tracing::info!(
6263                            "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6264                            spec_mon.tokens,
6265                            spec_mon.round_ms,
6266                            spec_mon.plain_ms,
6267                            if keep { "speculating" } else { "plain" }
6268                        );
6269                        spec_mon.fails = 0;
6270                        spec_trial = SpecTrial::Decided {
6271                            spec: keep,
6272                            recheck_at: if keep { usize::MAX } else { generated + 128 },
6273                        };
6274                    }
6275                    SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6276                        spec_mon.n = 0;
6277                        spec_mon.zero_accept_rounds = 0;
6278                        spec_trial = SpecTrial::Spec {
6279                            t0: std::time::Instant::now(),
6280                            gen0: generated,
6281                            rounds: 0,
6282                        };
6283                    }
6284                    _ => {}
6285                }
6286                spec_watchdog_off = matches!(
6287                    spec_trial,
6288                    SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6289                );
6290            }
6291            // ── MiMo-V2 draft stack: draft K, verify K+1 rows in one batch ──
6292            if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6293                let budget = max_tokens - generated - 1;
6294                if let Some(mut st) = self.mimo_mtp.take() {
6295                    let k = st.depth.min(budget);
6296                    let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6297                    self.mimo_mtp = Some(st);
6298                    let r = match r {
6299                        Ok(r) => r,
6300                        Err(err) => {
6301                            self.finish_generation(&mut mtp, &mut router, true);
6302                            return Err(err);
6303                        }
6304                    };
6305                    if let Some(r) = r {
6306                        drafted += r.drafted;
6307                        accepted += r.accepted.len();
6308                        let mut stopped = false;
6309                        for &id in &r.accepted {
6310                            if self.confidence_on {
6311                                confidence.push(0.0);
6312                            }
6313                            if !commit!(id) {
6314                                stopped = true;
6315                                break;
6316                            }
6317                        }
6318                        if stopped {
6319                            break 'decode;
6320                        }
6321                        next_pos += r.accepted.len() + 1;
6322                        hidden = r.hidden;
6323                        // The loop top chooses the round's own token from
6324                        // these logits — the same sampler, same history.
6325                        self.graph_logits = Some(r.logits);
6326                        continue 'decode;
6327                    }
6328                }
6329            }
6330            // ── Qwen3.8-Flash-Next draft head: k greedy drafts from the MTP
6331            //    sidecar, one batched verify window on the device ──
6332            #[cfg(feature = "gpu")]
6333            if self.speculative
6334                && self.qwen4_exp.is_some()
6335                && task_mask.is_none()
6336                && self.sampler_config.temperature < 1e-6
6337                && generated + 1 < max_tokens
6338                && next_pos > 0
6339                && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6340            {
6341                let r = match &mut self.qwen4_exp {
6342                    Some(b) => crate::qwen4_exp::spec_round(
6343                        &b.0,
6344                        &b.1,
6345                        &b.2,
6346                        &mut b.3,
6347                        next_pos,
6348                        &all_ids,
6349                        &self.inv_freq,
6350                        self.pool.as_deref(),
6351                    ),
6352                    None => None,
6353                };
6354                if let Some(r) = r {
6355                    drafted += r.drafted;
6356                    accepted += r.accepted.len();
6357                    let mut stopped = false;
6358                    for &id in &r.accepted {
6359                        if self.confidence_on {
6360                            confidence.push(0.0);
6361                        }
6362                        if !commit!(id) {
6363                            stopped = true;
6364                            break;
6365                        }
6366                    }
6367                    if stopped {
6368                        break 'decode;
6369                    }
6370                    next_pos += r.accepted.len() + 1;
6371                    hidden.fill(0.0);
6372                    self.graph_logits = Some(r.logits);
6373                    continue 'decode;
6374                }
6375            }
6376            match &mut mtp {
6377                // ── Graph speculation: chain-draft, batch-verify on device ──
6378                #[cfg(feature = "gpu")]
6379                Some(m)
6380                    if graph_spec
6381                        && !spec_watchdog_off
6382                        && generated + 1 < max_tokens
6383                        && next_pos > 0 =>
6384                {
6385                    let t_round = std::time::Instant::now();
6386                    if spec_time_level() >= 2 {
6387                        if let Some(t) = spec_round_end.take() {
6388                            eprintln!(
6389                                "spec-gap {:.2} ms (host between rounds)",
6390                                t.elapsed().as_secs_f64() * 1e3
6391                            );
6392                        }
6393                    }
6394                    spec_stamps_begin();
6395                    // device buffers allocated during this round: a
6396                    // first-touch Shared allocation is zero-filled inside
6397                    // the command buffer that uses it, which is what the
6398                    // long outlier rounds were
6399                    #[cfg(target_os = "macos")]
6400                    let allocs0 =
6401                        crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6402                    #[cfg(not(target_os = "macos"))]
6403                    let allocs0 = 0u64;
6404                    if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6405                        m,
6406                        &hidden,
6407                        t_next,
6408                        next_pos,
6409                        &mut drafted,
6410                        &mut accepted,
6411                        &mut all_ids,
6412                        max_tokens - generated,
6413                    ) {
6414                        next_pos = n_pos;
6415                        hidden = new_h;
6416                        let level = spec_time_level();
6417                        if level > 0 {
6418                            let wall = t_round.elapsed().as_secs_f32() * 1e3;
6419                            let stamps = spec_stamps_take();
6420                            // the running median of the rounds before this
6421                            // one (round 1 pays the scratch: not a sample)
6422                            let median = if spec_walls.len() >= 3 {
6423                                let mut s = spec_walls.clone();
6424                                s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6425                                Some(s[s.len() / 2])
6426                            } else {
6427                                None
6428                            };
6429                            let outlier = median.is_some_and(|m| wall > 1.4 * m);
6430                            #[cfg(target_os = "macos")]
6431                            let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6432                                .load(std::sync::atomic::Ordering::Relaxed)
6433                                - allocs0;
6434                            #[cfg(not(target_os = "macos"))]
6435                            let allocs = allocs0;
6436                            eprintln!(
6437                                "spec-round wall {wall:.1} ms → {} tokens{}{}",
6438                                extra.len() + 1,
6439                                if allocs > 0 {
6440                                    format!(" [{allocs} new device buffers]")
6441                                } else {
6442                                    String::new()
6443                                },
6444                                match (outlier, median) {
6445                                    (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6446                                    _ => String::new(),
6447                                }
6448                            );
6449                            if level >= 2 || outlier {
6450                                let sum: f32 = stamps.iter().map(|s| s.1).sum();
6451                                eprintln!(
6452                                    "spec-stamps: {}| untracked {:.1}",
6453                                    spec_stamps_format(&stamps),
6454                                    wall - sum
6455                                );
6456                            }
6457                            if spec_mon.n >= 1 {
6458                                spec_walls.push(wall);
6459                            }
6460                        }
6461                        // One speculative round done: the monitor counts it
6462                        // (round 1 untimed — it pays the batch scratch and
6463                        // the draft mirror), and the trial advances.
6464                        spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6465                        // the round's tokens land in `generated` below; the
6466                        // plain phase must start counting AFTER them
6467                        spec_trial = Self::spec_trial_round(
6468                            spec_trial,
6469                            &mut spec_mon,
6470                            generated + extra.len() + 1,
6471                        );
6472                        let mut stopped = false;
6473                        for &id in &extra {
6474                            if self.confidence_on {
6475                                confidence.push(0.0);
6476                            }
6477                            // `graph_spec_step` commits the accepted MTP
6478                            // drafts as a batch.  They do not pass through
6479                            // the ordinary loop-top sampling block below,
6480                            // which is normally where the telemetry row is
6481                            // created.  Keep the public trace contract
6482                            // (one row per emitted token) here as well.
6483                            //
6484                            // The graph-spec path does not surface a
6485                            // per-row probability for accepted extras.  It
6486                            // deliberately matches the zero placed in the
6487                            // confidence channel above instead of inventing
6488                            // one or adding a separate probability pass.
6489                            if trace_on {
6490                                traces.push(TokenTrace {
6491                                    t: generated,
6492                                    token_id: id,
6493                                    confidence: confidence.last().copied().unwrap_or(0.0),
6494                                    active_skill: None,
6495                                    recon: None,
6496                                    switched: false,
6497                                });
6498                            }
6499                            if !commit!(id) {
6500                                stopped = true;
6501                                break;
6502                            }
6503                        }
6504                        if stopped {
6505                            break 'decode;
6506                        }
6507                        if spec_time_level() >= 2 {
6508                            spec_round_end = Some(std::time::Instant::now());
6509                        }
6510                        continue 'decode;
6511                    }
6512                    if self
6513                        .graph_failed
6514                        .swap(false, std::sync::atomic::Ordering::Relaxed)
6515                    {
6516                        // `graph_spec_step` may have detached MTP while a
6517                        // warm-up was in flight.  Do not reinterpret its
6518                        // terminal device failure as a plain decode step;
6519                        // restore the head, clear both mirrors, and surface
6520                        // one explicit error to the caller.
6521                        self.finish_generation(&mut mtp, &mut router, true);
6522                        return Err("GPU MTP graph failed during speculative decode".to_string());
6523                    }
6524                    // Declined (batch graph refused): plain forward below —
6525                    // and a round that produced one token for the trial's
6526                    // ledger, so a graph that keeps refusing is measured out
6527                    // like a head that keeps missing (it was spinning
6528                    // forever on a file whose batch graph declines).
6529                    // A declined round is not a cheap one-token round — it
6530                    // is a verify that does not exist for this file (a
6531                    // healed q8_2f tail measured 760 drafts, 0 accepted, 33
6532                    // against 48.8 tok/s while the monitor called the draft
6533                    // alone "paying"). Count it as the losing streak in one.
6534                    spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6535                    spec_mon.tokens = 0.0;
6536                    spec_mon.fails = 3;
6537                    spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6538                    hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6539                    next_pos += 1;
6540                    continue 'decode;
6541                }
6542                // ── Speculative: draft t+2, verify in a fused pair ──
6543                Some(m) if !graph_spec && generated + 1 < max_tokens => {
6544                    let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6545                    drafted += 1;
6546                    let emb1 = self.embed_single(t_next);
6547                    let emb2 = self.embed_single(draft);
6548                    let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6549
6550                    inference::rms_norm_into(
6551                        &h1,
6552                        &self.weights.final_norm,
6553                        self.rms_eps,
6554                        self.norm_style,
6555                        &mut self.ws.n1,
6556                    );
6557                    let mut logits1 = self.lm_head_forward(&self.ws.n1);
6558                    let t_after = sampler::sample_with_scratch_pool(
6559                        &logits1,
6560                        &self.sampler_config,
6561                        self.sampler_config.penalty_past(&all_ids, bounded_native),
6562                        &mut self.rng,
6563                        &mut self.sampler_scratch,
6564                        self.pool.as_deref(),
6565                    );
6566                    if self.confidence_on {
6567                        confidence.push(sampler::top1_prob_pool(
6568                            self.pool.as_deref(),
6569                            &mut self.sampler_scratch,
6570                            &logits1,
6571                            t_after,
6572                            calib_temp,
6573                        ));
6574                    }
6575                    attention::recycle_buf(&mut logits1);
6576                    if trace_on {
6577                        // Speculative decode is mutually exclusive with
6578                        // dynamic routing (router is None here) — no skill.
6579                        traces.push(TokenTrace {
6580                            t: generated,
6581                            token_id: t_after,
6582                            confidence: confidence.last().copied().unwrap_or(0.0),
6583                            active_skill: None,
6584                            recon: None,
6585                            switched: false,
6586                        });
6587                    }
6588                    let stop = !commit!(t_after);
6589
6590                    if t_after == draft {
6591                        accepted += 1;
6592                        self.commit_linear_scratch();
6593                        let _ = self.mtp_step(m, &h1, t_after, next_pos);
6594                        hidden = h2;
6595                        next_pos += 2;
6596                    } else {
6597                        // The draft lane is wrong: roll its KV entry back.
6598                        for layer in &mut self.kv_cache.layers {
6599                            layer.truncate_last(1);
6600                        }
6601                        if !stop {
6602                            let _ = self.mtp_step(m, &h1, t_after, next_pos);
6603                            hidden = self.forward_layers(
6604                                &self.embed_single(t_after),
6605                                next_pos + 1,
6606                                None,
6607                            );
6608                        }
6609                        next_pos += 2;
6610                    }
6611                    if stop {
6612                        break 'decode;
6613                    }
6614                }
6615                // ── Vanilla: forward the sampled token ──
6616                _ => {
6617                    // ── DeepSeek-V4 speculative decode (CMF_DSV4_SPEC=1):
6618                    // draft five on the card, verify batched, commit the
6619                    // accepted prefix. Greedy only; a rejected token's state
6620                    // is restored and replayed, so output equals the walk. ──
6621                    #[cfg(feature = "gpu")]
6622                    if Self::dsv4_spec_on() && self.dsv4.is_some() {
6623                        static SAID: std::sync::Once = std::sync::Once::new();
6624                        SAID.call_once(|| {
6625                            eprintln!(
6626                                "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6627                                !self.dsv4_mtp.is_empty(),
6628                                task_mask.is_none(),
6629                                router.is_none(),
6630                                !trace_on,
6631                                self.sampler_config.temperature < 1e-6,
6632                                self.sampler_config.repetition_penalty == 1.0,
6633                            );
6634                        });
6635                    }
6636                    #[cfg(feature = "gpu")]
6637                    if Self::dsv4_spec_on()
6638                        && self.dsv4.is_some()
6639                        && !self.dsv4_mtp.is_empty()
6640                        && task_mask.is_none()
6641                        && router.is_none()
6642                        && !trace_on
6643                        && self.sampler_config.temperature < 1e-6
6644                        && self.sampler_config.repetition_penalty == 1.0
6645                        && generated + 1 < max_tokens
6646                        && all_ids.len() >= 2
6647                        && generated >= dsv4_spec_retry_at
6648                    {
6649                        let tip_token = all_ids[all_ids.len() - 2];
6650                        let drafted0 = drafted;
6651                        let round = self.dsv4_spec_step(
6652                            tip_token,
6653                            t_next,
6654                            next_pos,
6655                            max_tokens.saturating_sub(generated),
6656                            &mut drafted,
6657                            &mut accepted,
6658                        );
6659                        if drafted > drafted0 {
6660                            let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6661                            if useful {
6662                                dsv4_spec_bad = 0;
6663                            } else {
6664                                dsv4_spec_bad += 1;
6665                                if dsv4_spec_bad >= 2 {
6666                                    dsv4_spec_bad = 0;
6667                                    dsv4_spec_retry_at = generated.saturating_add(32);
6668                                    tracing::info!(
6669                                        "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6670                                    );
6671                                }
6672                            }
6673                        }
6674                        if let Some((extra, n_pos)) = round {
6675                            next_pos = n_pos;
6676                            let mut stopped = false;
6677                            for &id in &extra {
6678                                if self.confidence_on {
6679                                    confidence.push(0.0);
6680                                }
6681                                if !commit!(id) {
6682                                    stopped = true;
6683                                    break;
6684                                }
6685                            }
6686                            if stopped {
6687                                break 'decode;
6688                            }
6689                            continue 'decode;
6690                        }
6691                    }
6692                    self.graph_want_logits = fuse_lm;
6693                    // Greedy burst (CMF_MULTISTEP, off unless explicitly set): while
6694                    // nothing observes per-token state — pure argmax sampling,
6695                    // no router/trace/confidence/mask — decode k tokens per
6696                    // submit and commit them wholesale. The trailing normal
6697                    // forward leaves logits for the loop top, as always.
6698                    let mut t_fwd = t_next;
6699                    let pure_greedy = self.sampler_config.temperature < 1e-6
6700                        && self.sampler_config.repetition_penalty == 1.0
6701                        && self.sampler_config.presence_penalty == 0.0
6702                        && self.sampler_config.suppress_tokens.is_empty();
6703                    // Off by default: at every k the burst measured at or
6704                    // below the plain path on this graph shape (k=1 loses
6705                    // the argmax dispatches vs a 1 MB readback, k>=8 loses
6706                    // inter-step drains vs the saved sync). Experimental.
6707                    let burst_k = std::env::var("CMF_MULTISTEP")
6708                        .ok()
6709                        .and_then(|v| v.parse::<usize>().ok())
6710                        .unwrap_or(0);
6711                    if pure_greedy
6712                        && burst_k >= 1
6713                        && fuse_lm
6714                        && task_mask.is_none()
6715                        && router.is_none()
6716                        && !trace_on
6717                        && !self.confidence_on
6718                        // Diagnostics and teacher forcing observe every
6719                        // individual logit row.  A device-only burst returns
6720                        // only ids, so decline it rather than silently
6721                        // skipping requested evidence or forced ids.
6722                        && std::env::var_os("CMF_FORCE_IDS").is_none()
6723                        && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6724                        && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6725                    {
6726                        let mut stopped = false;
6727                        loop {
6728                            // The outer loop checks cancellation too, but a
6729                            // long run can stay in this burst loop for many
6730                            // submits.  Bound cancellation latency to one
6731                            // exact k-token frame.
6732                            if self
6733                                .cancel
6734                                .swap(false, std::sync::atomic::Ordering::Relaxed)
6735                            {
6736                                finish_reason = "cancelled".to_string();
6737                                stopped = true;
6738                                break;
6739                            }
6740                            let room = max_tokens.saturating_sub(generated);
6741                            if room <= 2 {
6742                                break;
6743                            }
6744                            let k = burst_k.min(room - 1);
6745                            if k < 1 {
6746                                break;
6747                            }
6748                            let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6749                                if self
6750                                    .graph_failed
6751                                    .swap(false, std::sync::atomic::Ordering::Relaxed)
6752                                {
6753                                    self.finish_generation(&mut mtp, &mut router, true);
6754                                    return Err(
6755                                        "GPU token graph failed during greedy burst".to_string()
6756                                    );
6757                                }
6758                                break;
6759                            };
6760                            next_pos += k;
6761                            for &id in &ids {
6762                                if !commit!(id) {
6763                                    stopped = true;
6764                                    break;
6765                                }
6766                            }
6767                            if stopped {
6768                                break;
6769                            }
6770                            t_fwd = *ids.last().unwrap();
6771                        }
6772                        if stopped {
6773                            break 'decode;
6774                        }
6775                    }
6776                    // Metal: keep the draft head's cache in step through
6777                    // the trial's plain phase and a paused speculation —
6778                    // the pair (hidden, t_fwd) at next_pos−1, the step the
6779                    // round's draft 0 would take. Without it the head's
6780                    // cache lagged the trunk by every plain token for the
6781                    // rest of the generation: the batched warm-up declined
6782                    // every later round and its rows went one by one (a
6783                    // whole MTP step per accepted token), and the drafts
6784                    // attended a context with those tokens missing.
6785                    #[cfg(target_os = "macos")]
6786                    if graph_spec
6787                        && spec_watchdog_off
6788                        && next_pos > 0
6789                        && self.mtp_graph_mode == Some(true)
6790                        && crate::gpu::q1_force()
6791                    {
6792                        if let Some(m) = mtp.as_mut() {
6793                            let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6794                        }
6795                    }
6796                    hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6797                    next_pos += 1;
6798                    // Dynamic routing: the forward updated φ; ask the
6799                    // router whether to switch skills before the next token.
6800                    if let Some(r) = &mut router {
6801                        let phi = self.dyn_phi_ema.clone();
6802                        let decision = r.step(&phi, generated);
6803                        if let Some(new_active) = decision {
6804                            let _ = self.set_active_skill(new_active);
6805                        }
6806                        // Backfill this token's coherence + switch flag from
6807                        // the just-run eval (freshest measured values).
6808                        if trace_on {
6809                            if let Some(last) = traces.last_mut() {
6810                                let e = r.last_best_e();
6811                                last.recon = e.is_finite().then_some(e);
6812                                last.switched = decision.is_some();
6813                            }
6814                        }
6815                    }
6816                }
6817            }
6818        }
6819
6820        let cancelled = finish_reason == "cancelled";
6821        // A generation during which the router switched weights holds no
6822        // state any single overlay would produce (each switch cleared the
6823        // cache mid-sequence), so it leaves no reuse key behind either.
6824        let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6825        if mimo_spec {
6826            if let Some(st) = self.mimo_mtp.as_ref() {
6827                let line = st.stats.line();
6828                tracing::info!("{line}");
6829                if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6830                    eprintln!("{line}");
6831                }
6832            }
6833        }
6834        self.finish_generation(&mut mtp, &mut router, cancelled);
6835
6836        let output_ids = &all_ids[input_ids.len()..];
6837        // Forwarded = prompt + all generated but the LAST sampled token
6838        // (emitted without being fed back). Exact only without MTP —
6839        // reuse is gated off when MTP is active.
6840        let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6841        // A MiMo speculative round that stopped on an accepted draft (EOS,
6842        // cancel) leaves verify rows past the committed stream in the cache:
6843        // never offer that cache for reuse. Neither does a router that
6844        // switched weights mid-sequence (no single overlay produced it).
6845        let consumed = std::mem::take(&mut all_ids);
6846        if dyn_switched {
6847            self.clear_sequence_state();
6848        } else if cancelled || mimo_spec || prompt_rows.is_some() {
6849            self.clear_history();
6850        } else {
6851            self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6852        }
6853        all_ids = consumed;
6854        let output_ids = &all_ids[input_ids.len()..];
6855        confidence.truncate(output_ids.len()); // guard against any overshoot
6856        traces.truncate(output_ids.len());
6857        Ok(GenerateResult {
6858            text: self.tokenizer.decode(output_ids),
6859            token_ids: output_ids.to_vec(),
6860            prompt_tokens: input_ids.len(),
6861            tokens_generated: generated,
6862            finish_reason,
6863            mtp_drafted: drafted,
6864            mtp_accepted: accepted,
6865            token_confidence: confidence,
6866            traces,
6867        })
6868    }
6869
6870    /// One MTP step: feed `(hidden_p, token_{p+1})` into the draft head,
6871    /// advance its KV cache at position `p`, return the drafted token
6872    /// for position `p+2`.
6873    fn mtp_step(
6874        &mut self,
6875        m: &mut MtpModule,
6876        hidden: &[f32],
6877        next_token: u32,
6878        position: usize,
6879    ) -> u32 {
6880        self.mtp_step_h(m, hidden, next_token, position).0
6881    }
6882
6883    /// Tally for `CMF_MTP_CHAIN_PROBE`: per depth, how often the CHAIN is
6884    /// still an exact prefix of the real continuation. Printed every 128
6885    /// depth-0 samples so a killed run still shows its table.
6886    fn chain_probe_note(depth: usize, prefix_ok: bool) {
6887        use std::sync::Mutex;
6888        static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6889        let mut t = T.lock().unwrap();
6890        if t.len() <= depth {
6891            t.resize(depth + 1, (0, 0));
6892        }
6893        t[depth].0 += 1;
6894        t[depth].1 += prefix_ok as u64;
6895        if depth == 0 && t[0].0 % 128 == 0 {
6896            let line: Vec<String> = t
6897                .iter()
6898                .enumerate()
6899                .map(|(d, (n, k))| {
6900                    format!(
6901                        "d{}={:.0}%({n})",
6902                        d + 1,
6903                        100.0 * *k as f64 / (*n).max(1) as f64
6904                    )
6905                })
6906                .collect();
6907            eprintln!("mtp-chain: {}", line.join(" "));
6908        }
6909    }
6910
6911    /// `mtp_step` that also hands back the block's own output hidden — the
6912    /// state a CHAINED draft feeds the next step, the way a multi-token
6913    /// speculative round iterates the head on itself.
6914    /// One MTP block step from (trunk hidden, token): the head's LOGITS
6915    /// and the block's own hidden for chaining. The draft is argmax of the
6916    /// logits on the greedy path and a draw from their post-chain
6917    /// distribution on the sampling path.
6918    fn mtp_step_hl(
6919        &mut self,
6920        m: &mut MtpModule,
6921        hidden: &[f32],
6922        next_token: u32,
6923        position: usize,
6924    ) -> (Vec<f32>, Vec<f32>) {
6925        // The graph arm: the MTP block as a one-layer token graph with the
6926        // head fused — device attention over the block's own KV mirror,
6927        // one submit for block + head, hidden and logits back together.
6928        // Decided once per generation (see `mtp_graph_mode`).
6929        #[cfg(target_os = "macos")]
6930        if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6931            if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6932                self.mtp_graph_mode = Some(true);
6933                return r;
6934            }
6935            if self.mtp_graph_mode == Some(true) {
6936                tracing::error!("mtp Metal graph failed after admission");
6937                self.clear_sequence_state();
6938                self.graph_failed
6939                    .store(true, std::sync::atomic::Ordering::Relaxed);
6940                self.cancel
6941                    .store(true, std::sync::atomic::Ordering::Relaxed);
6942                return (Vec::new(), Vec::new());
6943            }
6944            self.mtp_graph_mode = Some(false);
6945        }
6946        #[cfg(feature = "gpu")]
6947        if self.mtp_graph_mode != Some(false) {
6948            if !self.mtp_graph_ok(m) {
6949                if self.mtp_graph_mode == Some(true) {
6950                    // A mirror was already admitted, so a capability change
6951                    // cannot safely switch this request to the stale CPU
6952                    // cache.  Keep the same terminal contract as a failed
6953                    // token graph.
6954                    tracing::error!("mtp graph became unavailable after admission");
6955                    self.clear_sequence_state();
6956                    self.graph_failed
6957                        .store(true, std::sync::atomic::Ordering::Relaxed);
6958                    self.cancel
6959                        .store(true, std::sync::atomic::Ordering::Relaxed);
6960                    return (Vec::new(), Vec::new());
6961                }
6962                self.mtp_graph_mode = Some(false);
6963            } else {
6964                if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6965                    self.mtp_graph_mode = Some(true);
6966                    return r;
6967                }
6968                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6969                    // A token graph can have admitted a persistent MTP/GDN
6970                    // mirror before its readback failed.  The CPU MTP cache
6971                    // is not a valid continuation in that state; leave the
6972                    // flag set so the generation caller returns through its
6973                    // terminal error path instead of silently switching
6974                    // arithmetic.
6975                    return (Vec::new(), Vec::new());
6976                }
6977                // `mtp_graph_ok` was true, so a None here means a refusal or
6978                // failure after graph admission.  Do not fall through to a
6979                // CPU cache whose rows may lag the device mirror.
6980                tracing::error!("mtp graph failed or declined after admission");
6981                self.clear_sequence_state();
6982                self.graph_failed
6983                    .store(true, std::sync::atomic::Ordering::Relaxed);
6984                self.cancel
6985                    .store(true, std::sync::atomic::Ordering::Relaxed);
6986                return (Vec::new(), Vec::new());
6987            }
6988        }
6989        // fc concat order is [enorm(embed); hnorm(hidden)] — EMBEDDING
6990        // FIRST. Verified by the oracle (converter/mtp_oracle.py):
6991        // [emb;hid] → 45.8% acceptance, [hid;emb] → 0.00%.
6992        let e = self.embed_single(next_token);
6993        let mut cat = vec![0.0f32; 2 * self.hidden_size];
6994        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6995        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6996        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6997        let mut x = vec![0.0f32; self.hidden_size];
6998        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6999
7000        // One standard transformer block over the MTP's own cache.
7001        let lw = &m.layer;
7002        inference::rms_norm_into(
7003            &x,
7004            &lw.input_norm,
7005            self.rms_eps,
7006            self.norm_style,
7007            &mut self.ws.n1,
7008        );
7009        let attn = match &lw.attn {
7010            // MLA models carry no MTP head; this path cannot see them.
7011            AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
7012            AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
7013            AttnKind::Full {
7014                wq,
7015                wk,
7016                wv,
7017                wo,
7018                q_norm,
7019                k_norm,
7020                output_gate,
7021                softplus_gate,
7022                bias,
7023            } => {
7024                let mut cfg = self.attn_cfg(position);
7025                cfg.q_norm = q_norm.as_deref();
7026                cfg.k_norm = k_norm.as_deref();
7027                cfg.output_gate = *output_gate;
7028                cfg.softplus_gate = softplus_gate
7029                    .as_ref()
7030                    .map(|(gate, per_head)| (gate, *per_head));
7031                cfg.bias = bias
7032                    .as_ref()
7033                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7034                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7035            }
7036            AttnKind::Linear(_)
7037            | AttnKind::LinearGdn(_)
7038            | AttnKind::ShortConv(_)
7039            | AttnKind::Bounded(_) => {
7040                unreachable!("MTP block is full attention")
7041            }
7042        };
7043        for (i, &a) in attn.iter().enumerate() {
7044            x[i] += a;
7045        }
7046        inference::rms_norm_into(
7047            &x,
7048            &lw.post_norm,
7049            self.rms_eps,
7050            self.norm_style,
7051            &mut self.ws.p1,
7052        );
7053        let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
7054        for (i, &f) in ffn.iter().enumerate() {
7055            x[i] += f;
7056        }
7057
7058        inference::rms_norm_into(
7059            &x,
7060            &m.final_norm,
7061            self.rms_eps,
7062            self.norm_style,
7063            &mut self.ws.n1,
7064        );
7065        let lg = self.lm_head_forward(&self.ws.n1);
7066        (lg, x)
7067    }
7068
7069    /// `mtp_step_hl` reduced to the greedy draft: argmax of the head.
7070    fn mtp_step_h(
7071        &mut self,
7072        m: &mut MtpModule,
7073        hidden: &[f32],
7074        next_token: u32,
7075        position: usize,
7076    ) -> (u32, Vec<f32>) {
7077        let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
7078        let draft = sampler::argmax(&lg);
7079        attention::recycle_buf(&mut lg);
7080        (draft, x)
7081    }
7082
7083    /// One speculative round for the trial: rounds 1..5 of a `Spec` phase
7084    /// advance it (the monitor already averaged this round); after five,
7085    /// the plain phase runs (once — a known plain rate decides at once);
7086    /// a decided speculation keeps re-checking the rule every round and
7087    /// stops after four losing rounds in a row.
7088    fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
7089        match trial {
7090            SpecTrial::Spec { t0, gen0, rounds } => {
7091                let rounds = rounds + 1;
7092                // A round that accepts no draft still has to run the MTP
7093                // chain and a K+1 verifier, but emits only its verifier tip.
7094                // After two such real rounds the speculative arm cannot win
7095                // on any backend; time plain immediately rather than spend
7096                // the remaining exploratory rounds on the same one-token
7097                // result. A later 128-token re-check still gives changing
7098                // context a chance to re-admit it.
7099                if mon.repeated_zero_acceptance() {
7100                    tracing::info!(
7101                        "speculation trial: no accepted drafts in two rounds — timing plain"
7102                    );
7103                    return SpecTrial::Plain {
7104                        t0: std::time::Instant::now(),
7105                        gen0: generated,
7106                    };
7107                }
7108                if rounds >= 5 {
7109                    if mon.plain_ms > 0.0 {
7110                        let keep = mon.pays();
7111                        mon.fails = 0;
7112                        tracing::info!(
7113                            "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
7114                            mon.tokens,
7115                            mon.round_ms,
7116                            mon.plain_ms,
7117                            if keep { "speculating" } else { "plain" }
7118                        );
7119                        SpecTrial::Decided {
7120                            spec: keep,
7121                            recheck_at: if keep { usize::MAX } else { generated + 128 },
7122                        }
7123                    } else if mon.pays() {
7124                        // Metal: the rounds land enough tokens each that no
7125                        // plain measurement is needed — keep speculating,
7126                        // and re-check every round (a losing streak sends
7127                        // the loop to the plain phase, below).
7128                        mon.fails = 0;
7129                        tracing::info!(
7130                            "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
7131                            mon.tokens,
7132                            mon.round_ms,
7133                        );
7134                        SpecTrial::Decided {
7135                            spec: true,
7136                            recheck_at: usize::MAX,
7137                        }
7138                    } else {
7139                        SpecTrial::Plain {
7140                            t0: std::time::Instant::now(),
7141                            gen0: generated,
7142                        }
7143                    }
7144                } else {
7145                    SpecTrial::Spec { t0, gen0, rounds }
7146                }
7147            }
7148            SpecTrial::Decided { spec: true, .. } => {
7149                if mon.pays() {
7150                    mon.fails = 0;
7151                    trial
7152                } else {
7153                    mon.fails += 1;
7154                    if mon.fails >= 4 {
7155                        if mon.plain_ms <= 0.0 {
7156                            // Metal, plain never timed: four doubtful rounds
7157                            // buy the (bounded) plain measurement, and the
7158                            // exact rule decides from it.
7159                            tracing::info!(
7160                                "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
7161                                mon.tokens,
7162                                mon.round_ms,
7163                            );
7164                            return SpecTrial::Plain {
7165                                t0: std::time::Instant::now(),
7166                                gen0: generated,
7167                            };
7168                        }
7169                        tracing::info!(
7170                            "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
7171                            mon.tokens,
7172                            mon.round_ms,
7173                            mon.plain_ms
7174                        );
7175                        SpecTrial::Decided {
7176                            spec: false,
7177                            recheck_at: generated + 128,
7178                        }
7179                    } else {
7180                        trial
7181                    }
7182                }
7183            }
7184            other => other,
7185        }
7186    }
7187
7188    /// The MTP block's device-mirror id: the trunk's id with a high bit,
7189    /// so the (kv_id, layer) mirror keys never collide.
7190    fn mtp_kv_id(&self) -> u64 {
7191        self.graph_kv_id | (1u64 << 40)
7192    }
7193
7194    /// The MTP block's mirror layer index: 0 — its own kv_id keeps it
7195    /// apart from the trunk, and the BATCH graph (the warm-up path) keys
7196    /// its mirrors at layer 0 with no base of its own, so the draft's
7197    /// token graph must key the same slot.
7198    const MTP_LAYER_BASE: usize = 0;
7199
7200    /// The wgpu MTP draft writes speculative rows straight into its device
7201    /// mirror while the CPU owner retains only the real prompt/decode anchor.
7202    /// After verification, move that mirror cursor back to the anchor before
7203    /// replaying accepted pairs.  The next graph append then sees the same
7204    /// contiguous position as the CPU/Metal path without uploading stale
7205    /// speculative rows.
7206    #[cfg(feature = "gpu")]
7207    fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
7208        self.mtp_graph_mode != Some(true)
7209            || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
7210    }
7211
7212    /// A speculative verify graph appends the full `k+1` trunk rows before
7213    /// the acceptance count is known.  GDN state already has a snapshot
7214    /// restore; Full-attention mirrors need the matching logical cursor
7215    /// rewind so the next graph call does not reject an ahead-of-position KV
7216    /// cache after a partial acceptance.
7217    #[cfg(feature = "gpu")]
7218    fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
7219        let mut ok = true;
7220        let mut expected = false;
7221        for li in 0..self.num_layers {
7222            if matches!(
7223                self.weights.layers[self.phys_layer(li)].attn,
7224                AttnKind::Full { .. }
7225            ) {
7226                expected = true;
7227                ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
7228            }
7229        }
7230        !expected || ok
7231    }
7232
7233    /// Count the recurrent layers participating in the trunk verify graph.
7234    /// Snapshot restore is all-or-nothing across that set; deriving the count
7235    /// from the model keeps the restore contract valid for looped models too.
7236    fn graph_gdn_layer_count(&self) -> usize {
7237        (0..self.num_layers)
7238            .filter(|&li| {
7239                matches!(
7240                    &self.weights.layers[self.phys_layer(li)].attn,
7241                    AttnKind::LinearGdn(_)
7242                )
7243            })
7244            .count()
7245    }
7246
7247    /// The block's input from (trunk hidden, token): eh_proj · [enorm(e);
7248    /// hnorm(h)] — the same arithmetic the per-op path starts with.
7249    fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
7250        let e = self.embed_single(next_token);
7251        let mut cat = vec![0.0f32; 2 * self.hidden_size];
7252        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7253        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7254        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7255        let mut x = vec![0.0f32; self.hidden_size];
7256        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7257        x
7258    }
7259
7260    /// Is the MTP block graphable at all (device up, full attention
7261    /// without softplus, dense FFN)? The plan itself is built per call.
7262    #[cfg(feature = "gpu")]
7263    fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7264        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7265            return false;
7266        }
7267        if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7268            || !crate::gpu::enabled_here()
7269            || self.attn_softcap > 0.0
7270            || self.attention_heads_per_layer.is_some()
7271            // The block graph caches V as wide as K and feeds o_proj
7272            // nh·head_dim; a narrow-V model keeps its MTP block per-op.
7273            || self.v_head_dim.is_some()
7274        {
7275            return false;
7276        }
7277        matches!(
7278            &m.layer.attn,
7279            AttnKind::Full {
7280                softplus_gate: None,
7281                ..
7282            }
7283        ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7284    }
7285
7286    /// Full MTP token-graph eligibility, including the fused lm-head and all
7287    /// block projection weights.  Keep this distinct from the block-only
7288    /// check: prompt warm-up does not need the head, while a draft step does.
7289    #[cfg(feature = "gpu")]
7290    fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7291        if !self.mtp_block_graph_ok(m) {
7292            return false;
7293        }
7294        let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7295            return false;
7296        };
7297        let FfnKind::Dense(d) = &m.layer.ffn else {
7298            return false;
7299        };
7300        d.segs.is_empty()
7301            && wq.graph_weight().is_some()
7302            && wk.graph_weight().is_some()
7303            && wv.graph_weight().is_some()
7304            && wo.graph_weight().is_some()
7305            && d.gate_proj.graph_weight().is_some()
7306            && d.up_proj.graph_weight().is_some()
7307            && d.down_proj.graph_weight().is_some()
7308            && self.weights.lm_head.graph_weight().is_some()
7309    }
7310
7311    /// One MTP block step on the wgpu token graph: block + fused head in
7312    /// one submit, the block hidden and the logits read back together.
7313    /// None = the graph cannot take this block (softplus gate, non-dense
7314    /// FFN, unquantized head, no device) — the caller keeps the per-op
7315    /// path for the whole generation.
7316    #[cfg(feature = "gpu")]
7317    fn mtp_step_graph(
7318        &mut self,
7319        m: &mut MtpModule,
7320        hidden: &[f32],
7321        next_token: u32,
7322        position: usize,
7323    ) -> Option<(Vec<f32>, Vec<f32>)> {
7324        if !self.mtp_graph_ok(m) {
7325            return None;
7326        }
7327        let lw = &m.layer;
7328        let AttnKind::Full {
7329            wq,
7330            wk,
7331            wv,
7332            wo,
7333            q_norm,
7334            k_norm,
7335            output_gate,
7336            softplus_gate,
7337            bias,
7338        } = &lw.attn
7339        else {
7340            return None;
7341        };
7342        if softplus_gate.is_some() {
7343            return None;
7344        }
7345        let FfnKind::Dense(d) = &lw.ffn else {
7346            return None;
7347        };
7348        if !d.segs.is_empty() {
7349            return None; // tube layers run on the segmented path
7350        }
7351        // The block's input first: it borrows `self` mutably (embed scratch,
7352        // pool), the plan below borrows the weights immutably.
7353        let mut x = self.mtp_block_input(m, hidden, next_token);
7354        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7355            let (_, i, kind, rs) = t.graph_weight()?;
7356            Some(crate::gpu::GraphW {
7357                idx: i,
7358                kind,
7359                row_scale: rs,
7360                data: &[],
7361                prism: crate::gpu::GraphPrismOp::None,
7362                affine: false,
7363            })
7364        }
7365        let (model, _, _, _) = wq.graph_weight()?;
7366        let model = model.clone();
7367        let (lm_gw, lm_rows) = {
7368            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7369            // The draft's head over the CMF_DRAFT_VOCAB shortlist (the same
7370            // cut the native Metal draft takes): 662 MB a step on Qwen3.8
7371            // becomes 170 MB at 65536; the verify keeps the full head.
7372            let rows = if kind == 6 {
7373                self.draft_head_rows(self.weights.lm_head.rows())
7374            } else {
7375                self.weights.lm_head.rows()
7376            };
7377            (
7378                crate::gpu::GraphW {
7379                    idx: i,
7380                    kind,
7381                    row_scale: rs,
7382                    data: &[],
7383                    prism: crate::gpu::GraphPrismOp::None,
7384                    affine: false,
7385                },
7386                rows,
7387            )
7388        };
7389        let layer = crate::gpu::GraphLayer {
7390            input_norm: &lw.input_norm,
7391            attn: crate::gpu::GraphAttn::Full {
7392                wq: gw(wq)?,
7393                wk: gw(wk)?,
7394                wv: gw(wv)?,
7395                wo: gw(wo)?,
7396                q_norm: q_norm.as_deref(),
7397                k_norm: k_norm.as_deref(),
7398                late_qk_norm: self.qk_norm_after_rope,
7399                bias: bias
7400                    .as_ref()
7401                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7402                output_gate: *output_gate,
7403                cpu_k: m.kv.k_heads(),
7404                cpu_v: m.kv.v_heads(),
7405                cpu_base: m.kv.base(),
7406                geom: None,
7407                head_gate: None,
7408            },
7409            post_norm: &lw.post_norm,
7410            attn_out_norm: None,
7411            ffn_out_norm: None,
7412            layer_scale: None,
7413            ffn: crate::gpu::GraphFfn::Dense {
7414                gate: gw(&d.gate_proj)?,
7415                up: gw(&d.up_proj)?,
7416                down: gw(&d.down_proj)?,
7417                act: d.act.graph_act()?,
7418            },
7419        };
7420        let nh = self.num_heads;
7421        let (nkv, hd, rd) = self.layer_geom(0);
7422        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7423        let mut logits = Vec::new();
7424        let ok = crate::gpu::forward_token_graph(
7425            &model,
7426            self.mtp_kv_id(),
7427            std::slice::from_ref(&layer),
7428            &[None],
7429            self.o1_epoch,
7430            &self.inv_freq,
7431            &mut x,
7432            nh,
7433            nkv,
7434            hd,
7435            self.attn_scale,
7436            rd,
7437            self.hidden_size,
7438            // An MTP draft can have an independently compacted FFN. The
7439            // graph must use the checked local tensor width, not the trunk
7440            // arch-wide intermediate size.
7441            d.gate_proj.rows(),
7442            position,
7443            self.kv_cache.max_seq_len,
7444            gemma,
7445            self.rms_eps as f32,
7446            Some((&lm_gw, lm_rows)),
7447            &m.final_norm,
7448            &mut logits,
7449            &[],
7450            1,
7451            None,
7452            None,
7453            None,
7454            Self::MTP_LAYER_BASE,
7455            true,
7456        );
7457        match ok {
7458            crate::gpu::TokenGraphOutcome::Completed => {}
7459            crate::gpu::TokenGraphOutcome::Declined => return None,
7460            crate::gpu::TokenGraphOutcome::Failed => {
7461                // The backend has already admitted persistent state.  Keep
7462                // this distinct from a capability refusal so the caller
7463                // cannot switch to the stale CPU MTP cache.
7464                self.clear_sequence_state();
7465                self.graph_failed
7466                    .store(true, std::sync::atomic::Ordering::Relaxed);
7467                self.cancel
7468                    .store(true, std::sync::atomic::Ordering::Relaxed);
7469                return None;
7470            }
7471        }
7472        // The graph may deliberately evaluate only a prefix of the draft
7473        // head.  Do not turn every omitted token into a zero-logit candidate:
7474        // that can win an all-negative shortlist and collapses acceptance.
7475        pad_mtp_draft_logits(&mut logits, self.vocab_size);
7476        Some((logits, x))
7477    }
7478
7479    /// The warm-ups of one speculative round on the device: every accepted
7480    /// (hidden, token) pair as ONE batched graph run over the MTP block
7481    /// (no head) — its kv_append lands the pairs in the block's mirror.
7482    /// `pairs` are consecutive positions from `first_pos`.  The tri-state
7483    /// result is intentional: a refusal before admission may use the
7484    /// per-row/CPU route, while a failure after admission must terminate the
7485    /// sequence rather than fall through to a stale CPU cache.
7486    #[cfg(feature = "gpu")]
7487    fn mtp_warm_graph(
7488        &mut self,
7489        m: &mut MtpModule,
7490        pairs: &[(&[f32], u32)],
7491        first_pos: usize,
7492    ) -> crate::gpu::BatchGraphOutcome {
7493        if pairs.is_empty() {
7494            return crate::gpu::BatchGraphOutcome::Completed;
7495        }
7496        if !self.mtp_block_graph_ok(m) {
7497            return crate::gpu::BatchGraphOutcome::Declined;
7498        }
7499        let hs = self.hidden_size;
7500        // Block inputs for every pair (eh_proj on the per-op path, one
7501        // matvec each — the plan's own prologue).
7502        let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7503        for (h, t) in pairs {
7504            hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7505        }
7506        let lw = &m.layer;
7507        let AttnKind::Full {
7508            wq,
7509            wk,
7510            wv,
7511            wo,
7512            q_norm,
7513            k_norm,
7514            output_gate,
7515            bias,
7516            ..
7517        } = &lw.attn
7518        else {
7519            return crate::gpu::BatchGraphOutcome::Declined;
7520        };
7521        let FfnKind::Dense(d) = &lw.ffn else {
7522            return crate::gpu::BatchGraphOutcome::Declined;
7523        };
7524        if !d.segs.is_empty() {
7525            return crate::gpu::BatchGraphOutcome::Declined; // tube layers run on the segmented path
7526        }
7527        // The graph has no arm for a projected output gate, and only the
7528        // activations `graph_act` names.
7529        let (
7530            AttnKind::Full {
7531                softplus_gate: None,
7532                ..
7533            },
7534            Some(gact),
7535        ) = (&lw.attn, d.act.graph_act())
7536        else {
7537            return crate::gpu::BatchGraphOutcome::Declined;
7538        };
7539        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7540            let (_, i, kind, rs) = t.graph_weight()?;
7541            Some(crate::gpu::GraphW {
7542                idx: i,
7543                kind,
7544                row_scale: rs,
7545                data: &[],
7546                prism: crate::gpu::GraphPrismOp::None,
7547                affine: false,
7548            })
7549        }
7550        let Some((model, _, _, _)) = wq.graph_weight() else {
7551            return crate::gpu::BatchGraphOutcome::Declined;
7552        };
7553        let model = model.clone();
7554        let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7555            gw(wq),
7556            gw(wk),
7557            gw(wv),
7558            gw(wo),
7559            gw(&d.gate_proj),
7560            gw(&d.up_proj),
7561            gw(&d.down_proj),
7562        ) else {
7563            return crate::gpu::BatchGraphOutcome::Declined;
7564        };
7565        let layer = crate::gpu::GraphLayer {
7566            input_norm: &lw.input_norm,
7567            attn: crate::gpu::GraphAttn::Full {
7568                wq: gwq,
7569                wk: gwk,
7570                wv: gwv,
7571                wo: gwo,
7572                q_norm: q_norm.as_deref(),
7573                k_norm: k_norm.as_deref(),
7574                late_qk_norm: self.qk_norm_after_rope,
7575                bias: bias
7576                    .as_ref()
7577                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7578                output_gate: *output_gate,
7579                cpu_k: m.kv.k_heads(),
7580                cpu_v: m.kv.v_heads(),
7581                cpu_base: m.kv.base(),
7582                geom: None,
7583                head_gate: None,
7584            },
7585            post_norm: &lw.post_norm,
7586            attn_out_norm: None,
7587            ffn_out_norm: None,
7588            layer_scale: None,
7589            ffn: crate::gpu::GraphFfn::Dense {
7590                gate: gg,
7591                up: gu,
7592                down: gd,
7593                act: gact,
7594            },
7595        };
7596        let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7597        let nh = self.num_heads;
7598        let (nkv, hd, rd) = self.layer_geom(0);
7599        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7600        crate::gpu::forward_batch_graph(
7601            &model,
7602            self.mtp_kv_id(),
7603            std::slice::from_ref(&layer),
7604            &self.inv_freq,
7605            &mut hiddens,
7606            nh,
7607            nkv,
7608            hd,
7609            rd,
7610            hs,
7611            // See `mtp_step_graph`: draft FFN geometry is local to this
7612            // block after independent MTP compaction.
7613            d.gate_proj.rows(),
7614            &positions,
7615            self.kv_cache.max_seq_len,
7616            gemma,
7617            self.rms_eps as f32,
7618            self.attn_scale,
7619            pairs.len(),
7620            &[],
7621            0,
7622            None,
7623            None,
7624        )
7625    }
7626
7627    /// Complete an MTP warm-up after the batched graph has refused.  A
7628    /// graphable block is retried one row at a time; once any device row has
7629    /// been admitted, a CPU fallback would observe a stale mirror, so every
7630    /// token-graph refusal is terminal.  If the block is not graphable and no
7631    /// mirror exists yet, warming on the CPU is safe and records the CPU mode
7632    /// for the rest of the generation.
7633    #[cfg(feature = "gpu")]
7634    fn mtp_warm_graph_fallback(
7635        &mut self,
7636        m: &mut MtpModule,
7637        pairs: &[(&[f32], u32)],
7638        first_pos: usize,
7639    ) -> bool {
7640        if pairs.is_empty() {
7641            return true;
7642        }
7643        let graphable = self.mtp_block_graph_ok(m);
7644        if !graphable {
7645            // A previously admitted mirror cannot be made coherent by
7646            // appending to the host cache.  The caller turns this into a
7647            // terminal generation error and clears both mirrors.
7648            if self.mtp_graph_mode == Some(true) {
7649                return false;
7650            }
7651            self.mtp_graph_mode = Some(false);
7652            for (j, (h, t)) in pairs.iter().enumerate() {
7653                self.mtp_warm(m, h, *t, first_pos + j);
7654            }
7655            return true;
7656        }
7657
7658        // The batch refusal is recoverable only through the same device
7659        // state.  Keep rows owned until each token graph has completed; a
7660        // None is treated as unsafe because the token-graph API deliberately
7661        // collapses its backend refusal/failure into that result.
7662        for (j, (h, t)) in pairs.iter().enumerate() {
7663            if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7664                return false;
7665            }
7666        }
7667        self.mtp_graph_mode = Some(true);
7668        true
7669    }
7670
7671    /// Warm a contiguous set of MTP pairs using the existing graph seam, with
7672    /// an all-or-nothing error contract for callers that already admitted the
7673    /// trunk batch.  The non-GPU build keeps the same pair accounting while
7674    /// using the established CPU warm path.
7675    #[cfg(feature = "gpu")]
7676    fn mtp_warm_prefill_pairs(
7677        &mut self,
7678        m: &mut MtpModule,
7679        pairs: &[(&[f32], u32)],
7680        first_pos: usize,
7681    ) -> Result<(), &'static str> {
7682        // Keep unsupported token-graph heads on the established CPU MTP
7683        // route before admitting any block mirror.  Once a device mirror is
7684        // active, the same condition is terminal because CPU rows cannot
7685        // repair its state.
7686        if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7687            if self.mtp_graph_mode == Some(true) {
7688                return Err("MTP token graph became unavailable after admission");
7689            }
7690            self.mtp_graph_mode = Some(false);
7691            for (j, (h, t)) in pairs.iter().enumerate() {
7692                self.mtp_warm(m, h, *t, first_pos + j);
7693            }
7694            return Ok(());
7695        }
7696        match self.mtp_warm_graph(m, pairs, first_pos) {
7697            crate::gpu::BatchGraphOutcome::Completed => {
7698                if !pairs.is_empty() {
7699                    self.mtp_graph_mode = Some(true);
7700                }
7701                Ok(())
7702            }
7703            crate::gpu::BatchGraphOutcome::Declined => {
7704                if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7705                    Ok(())
7706                } else {
7707                    Err("MTP warm-up fallback failed after device admission")
7708                }
7709            }
7710            crate::gpu::BatchGraphOutcome::Failed => {
7711                Err("MTP warm batch graph failed after admission")
7712            }
7713        }
7714    }
7715
7716    #[cfg(not(feature = "gpu"))]
7717    fn mtp_warm_prefill_pairs(
7718        &mut self,
7719        m: &mut MtpModule,
7720        pairs: &[(&[f32], u32)],
7721        first_pos: usize,
7722    ) -> Result<(), &'static str> {
7723        for (j, (h, t)) in pairs.iter().enumerate() {
7724            self.mtp_warm(m, h, *t, first_pos + j);
7725        }
7726        Ok(())
7727    }
7728
7729    /// The MTP block alone — advance its KV with a (hidden, token) pair the
7730    /// verify just proved, without paying the head. What keeps the draft's
7731    /// attention context warm between speculative rounds.
7732    fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7733        let e = self.embed_single(next_token);
7734        let mut cat = vec![0.0f32; 2 * self.hidden_size];
7735        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7736        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7737        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7738        let mut x = vec![0.0f32; self.hidden_size];
7739        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7740        inference::rms_norm_into(
7741            &x,
7742            &m.layer.input_norm,
7743            self.rms_eps,
7744            self.norm_style,
7745            &mut self.ws.n1,
7746        );
7747        let attn = match &m.layer.attn {
7748            AttnKind::Full {
7749                wq,
7750                wk,
7751                wv,
7752                wo,
7753                q_norm,
7754                k_norm,
7755                output_gate,
7756                softplus_gate,
7757                bias,
7758            } => {
7759                let mut cfg = self.attn_cfg(position);
7760                cfg.q_norm = q_norm.as_deref();
7761                cfg.k_norm = k_norm.as_deref();
7762                cfg.output_gate = *output_gate;
7763                cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7764                cfg.bias = bias
7765                    .as_ref()
7766                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7767                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7768            }
7769            _ => return,
7770        };
7771        let _ = attn;
7772    }
7773
7774    /// Speculative decode ON the wgpu whole-token graph: draft k with the
7775    /// MTP head, verify all of them plus the tip in ONE batched graph
7776    /// submit whose tail folds the head, commit the accepted prefix and
7777    /// roll the GDN state back to the last real position. Greedy only —
7778    /// output equals the plain graph's token for token, the way the DSV4
7779    /// verify equals the walk.
7780    #[cfg(feature = "gpu")]
7781    #[allow(clippy::too_many_arguments)]
7782    fn graph_spec_step(
7783        &mut self,
7784        m: &mut MtpModule,
7785        hidden: &[f32],
7786        t_next: u32,
7787        next_pos: usize,
7788        drafted: &mut usize,
7789        accepted: &mut usize,
7790        // The committed stream (prompt + generated so far, `t_next`
7791        // included): the sampler chain's penalties read it, and the
7792        // sampling arm extends it with the drafts position by position.
7793        all_ids: &mut Vec<u32>,
7794        // Tokens left before `max_tokens`. A round commits up to k
7795        // accepted drafts, and those positions are already in the cache,
7796        // so the depth is capped here — trimming the output afterwards
7797        // would leave cache rows the committed stream does not have.
7798        room: usize,
7799    ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7800        // 3 is the measured optimum on Qwen3.6-27B / RTX 5090 (medians
7801        // of three, greedy): 51.1 tok/s against a plain 49.4, where k=2
7802        // gives 46.1, k=4 50.0, k=5 47.4, k=6 45.2. Acceptance is 89-91%
7803        // throughout — what turns the curve over is the verify, which
7804        // costs ~7.4 ms per extra position, and the draft ~3 ms a step.
7805        // 4 since the draft moved onto the graph (Qwen3.8-27B / 5090:
7806        // k=3 51.2, k=4 51.8 with the per-op draft; the graph draft
7807        // halves the draft cost, so the extra draft is cheaper still).
7808        // 5 with the int8 verify (the default: measured 76.5 against
7809        // k=4's 72-74 and k=6's 74 on the 5090), 4 with the f32 one.
7810        #[cfg(target_os = "macos")]
7811        let metal_native = crate::gpu::q1_force();
7812        #[cfg(not(target_os = "macos"))]
7813        let metal_native = false;
7814        #[cfg(feature = "gpu")]
7815        let k_default = if metal_native {
7816            // the Metal verify's GEMM tile is 8 rows wide and flat in b:
7817            // seven drafts + the tip fill it for free
7818            7
7819        } else if crate::gpu_wgpu::verify_i8_on() {
7820            5
7821        } else {
7822            4
7823        };
7824        #[cfg(not(feature = "gpu"))]
7825        let k_default = 4;
7826        let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7827            .ok()
7828            .and_then(|v| v.parse().ok())
7829            .filter(|&v| (1..=8).contains(&v));
7830        // Adaptive depth: start below the card's flat-verify optimum and
7831        // let the accepted fraction move it — predictable text climbs to
7832        // the old default within a few rounds, prose settles at 2-3 where
7833        // the shorter verify pays.
7834        let (k_start, k_max) = if metal_native {
7835            (7, 7)
7836        } else {
7837            (3, k_default.max(5))
7838        };
7839        let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7840        let k_spec = k_full.min(room).max(1);
7841        // a tail round cut short by `room` says nothing about the text:
7842        // it must not move the adaptive depth the next request starts at
7843        let k_capped = k_spec < k_full;
7844        if next_pos == 0 {
7845            return None;
7846        }
7847        let t_round = std::time::Instant::now();
7848        // Submissions per phase — and they say where the round's money is.
7849        // Qwen3.6-27B on an RTX 5090, k=3:
7850        //
7851        //   draft   9.3 ms / 12 submissions   (four per MTP step)
7852        //   verify 52.8 ms /  1               (the batched graph)
7853        //   commit  5.4 ms /  6               (two per warm)
7854        //
7855        // The verify is already one submit. The draft's own work is 834 MB
7856        // a step — 0.8 ms at this card's measured 1056 GB/s — against 3.1
7857        // ms measured, so ~0.58 ms of every step is round trip, not
7858        // arithmetic, and the same holds for the warms. Eighteen round
7859        // trips a round at roughly half a millisecond each is ~11 ms of a
7860        // 68 ms round: fusing the MTP block into ONE submit the way the
7861        // trunk already is projects to ~64 tok/s against today's 50.9.
7862        // That is the largest measured item left on this path.
7863        let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7864        let sub0 = subs();
7865        // Greedy without penalties verifies by argmax equality (bit-exact
7866        // against the plain path). Anything else is speculative SAMPLING:
7867        // each draft is a DRAW from the MTP head's post-chain distribution
7868        // q_j, kept for the accept test; the verify's rows give p_j.
7869        let cfg = self.sampler_config.clone();
7870        let penalized = !(cfg.repetition_penalty == 1.0
7871            && cfg.presence_penalty == 0.0
7872            && cfg.suppress_tokens.is_empty());
7873        // Three verify regimes: plain greedy (argmax of the raw rows),
7874        // greedy WITH penalties (argmax of the penalized rows — a single
7875        // pass each, no distributions), and sampling (draw / accept /
7876        // correct on post-chain distributions).
7877        let greedy_pen = cfg.temperature < 1e-6 && penalized;
7878        let sampling = cfg.temperature >= 1e-6;
7879        // Sampling with a top-k goes through the SPARSE chain: the dense
7880        // one builds nine 248k-float distributions a round (four drafts,
7881        // five verify rows) and measured 19-22 tok/s against a plain 40 —
7882        // the host, not the card. Sparse, the same nine cost tens of
7883        // microseconds each.
7884        let sparse = sampling && sampler::sparse_ok(&cfg);
7885        let base_len = all_ids.len();
7886        if sampling && !sparse && self.spec_q.len() < k_spec {
7887            self.spec_q.resize_with(k_spec, Vec::new);
7888        }
7889        if sparse && self.spec_qs.len() < k_spec {
7890            self.spec_qs.resize_with(k_spec, Vec::new);
7891        }
7892        // Draft the chain: first from the trunk's tip hidden, then the head
7893        // iterating on itself. Rows land in the MTP KV; the chain rows past
7894        // the first are speculation over speculative state and roll back
7895        // below, replaced by verified pairs.
7896        let mut drafts = Vec::with_capacity(k_spec);
7897        let mut hx = hidden.to_vec();
7898        // CMF_SPEC_DBG=1: draft 0 through BOTH MTP arms (graph and per-op)
7899        // from the same inputs — are the arms the difference, or the inputs?
7900        let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7901        spec_stamp("pro");
7902        // Plain greedy on native Metal: the whole chain as one command
7903        // buffer (device argmax + embedding gather between the steps).
7904        // A decline before commit hands the round to the per-step loop
7905        // below; a failure after commit is terminal, like any graph
7906        // failure after admission.
7907        #[cfg(target_os = "macos")]
7908        if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7909            match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7910                Ok(ids) => {
7911                    self.mtp_graph_mode = Some(true);
7912                    drafts = ids;
7913                }
7914                Err(true) => {
7915                    tracing::error!("mtp Metal draft chain failed after commit");
7916                    self.clear_sequence_state();
7917                    self.graph_failed
7918                        .store(true, std::sync::atomic::Ordering::Relaxed);
7919                    self.cancel
7920                        .store(true, std::sync::atomic::Ordering::Relaxed);
7921                    return None;
7922                }
7923                Err(false) => {}
7924            }
7925        }
7926        for j in drafts.len()..k_spec {
7927            let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7928            let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7929            if spec_dbg {
7930                let saved = self.mtp_graph_mode;
7931                self.mtp_graph_mode = Some(false);
7932                let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7933                self.mtp_graph_mode = saved;
7934                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7935                    return None;
7936                }
7937                m.kv.truncate_last(1);
7938                dbg_ref = Some(r);
7939            }
7940            let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7941            if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7942                return None;
7943            }
7944            if let Some((lg_cpu, h_cpu)) = dbg_ref {
7945                let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7946                let dl = lg
7947                    .iter()
7948                    .zip(&lg_cpu)
7949                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7950                let dh = hj
7951                    .iter()
7952                    .zip(&h_cpu)
7953                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7954                eprintln!(
7955                    "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 {}",
7956                    next_pos - 1 + j,
7957                    sampler::argmax(&lg_cpu),
7958                    sampler::argmax(&lg),
7959                    n(&h_cpu),
7960                    n(&hj),
7961                    m.kv.seq_len
7962                );
7963            }
7964            let dj = if sparse {
7965                let mut q = std::mem::take(&mut self.spec_qs[j]);
7966                let ok = sampler::sparse_distribution_into(
7967                    &lg,
7968                    &cfg,
7969                    all_ids,
7970                    &mut self.sampler_scratch,
7971                    self.pool.as_deref(),
7972                    &mut q,
7973                );
7974                let d = if ok {
7975                    sampler::draw_sparse(&q, &mut self.rng)
7976                } else {
7977                    // everything filtered: the dense chain's greedy fallback
7978                    let t = sampler::argmax(&lg);
7979                    q.clear();
7980                    q.push((t, 1.0));
7981                    t
7982                };
7983                self.spec_qs[j] = q;
7984                all_ids.push(d);
7985                d
7986            } else if sampling {
7987                let mut q = std::mem::take(&mut self.spec_q[j]);
7988                sampler::distribution_into(
7989                    &lg,
7990                    &cfg,
7991                    all_ids,
7992                    &mut self.sampler_scratch,
7993                    self.pool.as_deref(),
7994                    &mut q,
7995                );
7996                let d = sampler::draw(&q, &mut self.rng);
7997                self.spec_q[j] = q;
7998                all_ids.push(d); // the next draft's penalties see this one
7999                d
8000            } else if greedy_pen {
8001                let d = sampler::argmax_penalized(
8002                    &lg,
8003                    &cfg,
8004                    all_ids,
8005                    &mut self.sampler_scratch,
8006                    self.pool.as_deref(),
8007                );
8008                all_ids.push(d);
8009                d
8010            } else {
8011                sampler::argmax(&lg)
8012            };
8013            attention::recycle_buf(&mut lg);
8014            drafts.push(dj);
8015            hx = hj;
8016            spec_stamp("d.pick");
8017        }
8018        all_ids.truncate(base_len);
8019        *drafted += k_spec;
8020        let t_draft = t_round.elapsed();
8021        let sub_draft = subs();
8022        // Verify batch: [t_next, d1 .. d_{k-1}] at next_pos.. — every row's
8023        // logits come back from the graph's own head.
8024        let b = k_spec + 1;
8025        let mut hiddens = vec![0.0f32; b * self.hidden_size];
8026        for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
8027            let e = self.embed_single(t);
8028            hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
8029        }
8030        let positions: Vec<usize> = (next_pos..next_pos + b).collect();
8031        spec_stamp("v.emb");
8032        let (lm_gw, lm_rows) = {
8033            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
8034            (
8035                crate::gpu::GraphW {
8036                    idx: i,
8037                    kind,
8038                    row_scale: rs,
8039                    data: &[],
8040                    prism: crate::gpu::GraphPrismOp::None,
8041                    affine: false,
8042                },
8043                self.weights.lm_head.rows(),
8044            )
8045        };
8046        let mut logits = Vec::new();
8047        let final_norm = self.weights.final_norm.clone();
8048        // Plain greedy on Metal: the b argmaxes come from the device
8049        // (`argmax_rows` after the head) and the 7.9 MB logits plane is
8050        // never read back — the round's decision needs only the ids, and
8051        // the loop top takes the last verified id as `spec_forced`, which
8052        // is exactly what its argmax of the row would give. The full rows
8053        // stay for anything that reads them: sampling, penalties,
8054        // confidence, the verify oracle, the logit dump.
8055        // `CMF_METAL_DEV_ARGMAX=0` keeps the host path.
8056        #[cfg(target_os = "macos")]
8057        let greedy_dev = metal_native
8058            && !sampling
8059            && !greedy_pen
8060            && !self.confidence_on
8061            && self.final_softcap.is_none()
8062            // The host acceptance argmax scans the WHOLE head row
8063            // (`lm_rows`), the sampler's own row only `vocab_size`: they
8064            // coincide exactly when the head has no padding rows, and
8065            // only then is the device argmax (which scores `vocab_size`)
8066            // bit-identical to both.
8067            && self.vocab_size == lm_rows
8068            && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
8069            && std::env::var_os("CMF_LOGIT_DUMP").is_none()
8070            && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
8071        #[cfg(not(target_os = "macos"))]
8072        let greedy_dev = false;
8073        let mut dev_ids: Vec<u32> = Vec::new();
8074        #[cfg(target_os = "macos")]
8075        let verify_outcome = if metal_native {
8076            let lm = self.weights.lm_head.q1_parts()?;
8077            let n_score = self.vocab_size.min(lm_rows);
8078            self.try_batch_graph_metal(
8079                &mut hiddens,
8080                &positions,
8081                b,
8082                Some((lm, &final_norm, &mut logits)),
8083                if greedy_dev {
8084                    Some((n_score, &mut dev_ids))
8085                } else {
8086                    None
8087                },
8088            )
8089        } else {
8090            self.try_batch_graph_wgpu(
8091                &mut hiddens,
8092                &positions,
8093                b,
8094                Some(crate::gpu::SpecTail {
8095                    lm: lm_gw,
8096                    lm_rows,
8097                    final_norm: &final_norm,
8098                    logits_out: &mut logits,
8099                    pick: None,
8100                }),
8101            )
8102        };
8103        #[cfg(not(target_os = "macos"))]
8104        let verify_outcome = self.try_batch_graph_wgpu(
8105            &mut hiddens,
8106            &positions,
8107            b,
8108            Some(crate::gpu::SpecTail {
8109                lm: lm_gw,
8110                lm_rows,
8111                final_norm: &final_norm,
8112                logits_out: &mut logits,
8113                pick: None,
8114            }),
8115        );
8116        match verify_outcome {
8117            crate::gpu::BatchGraphOutcome::Completed => {}
8118            crate::gpu::BatchGraphOutcome::Declined => {
8119                // The verifier refused before admission.  Its draft MTP
8120                // rows are still device-resident, so rewind the separate
8121                // mirror before the caller takes the exact one-token path.
8122                m.kv.truncate_last(k_spec);
8123                if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
8124                    self.clear_sequence_state();
8125                    self.graph_failed
8126                        .store(true, std::sync::atomic::Ordering::Relaxed);
8127                    self.cancel
8128                        .store(true, std::sync::atomic::Ordering::Relaxed);
8129                    tracing::error!("MTP graph mirror rewind failed after verify decline");
8130                }
8131                return None;
8132            }
8133            crate::gpu::BatchGraphOutcome::Failed => {
8134                // A failed batch may have advanced trunk/GDN state.  Clear
8135                // both mirrors and preserve the terminal outcome rather than
8136                // falling through to stale CPU state.
8137                self.clear_sequence_state();
8138                self.graph_failed
8139                    .store(true, std::sync::atomic::Ordering::Relaxed);
8140                self.cancel
8141                    .store(true, std::sync::atomic::Ordering::Relaxed);
8142                tracing::error!("MTP verify batch graph failed after admission");
8143                return None;
8144            }
8145        }
8146        // `CMF_METAL_VERIFY_CHECK=1`: run the same b tokens through the
8147        // plain per-token path and compare each row's argmax + logits with
8148        // the verify's — the bring-up oracle for the batched graph. The
8149        // plain forwards mutate the CPU state; it is snapshotted and put
8150        // back, and the K/V mirrors re-pointed, before the round goes on.
8151        #[cfg(target_os = "macos")]
8152        if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
8153            let snap: Vec<Vec<f32>> = self
8154                .kv_cache
8155                .layers
8156                .iter()
8157                .map(|l| l.linear_state.clone())
8158                .collect();
8159            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8160            let toks: Vec<u32> = std::iter::once(t_next)
8161                .chain(drafts.iter().copied())
8162                .collect();
8163            let want_save = self.graph_want_logits;
8164            self.graph_want_logits = false;
8165            for (i, &t) in toks.iter().enumerate() {
8166                let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8167                let _ = self.graph_logits.take();
8168                // CMF_SPEC_PLAIN_HIDDEN=1: the next round drafts from the
8169                // plain path's hidden instead of the verify's (an experiment
8170                // on the chain's sensitivity to the half-GEMM noise)
8171                if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
8172                    hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
8173                }
8174                let ref_lg = self.logits_from_hidden(&hi);
8175                let row = &logits[i * lm_rows..(i + 1) * lm_rows];
8176                let ra = sampler::argmax(&ref_lg);
8177                let va = sampler::argmax(row);
8178                let mut md = 0f32;
8179                let mut rms = 0f64;
8180                for j in 0..lm_rows.min(ref_lg.len()) {
8181                    let d = (ref_lg[j] - row[j]).abs();
8182                    md = md.max(d);
8183                    rms += (d as f64) * (d as f64);
8184                }
8185                let mut hd = 0f32;
8186                for j in 0..self.hidden_size {
8187                    hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
8188                }
8189                eprintln!(
8190                    "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
8191                    next_pos + i,
8192                    if ra == va { "OK" } else { "MISMATCH" },
8193                    (rms / lm_rows as f64).sqrt()
8194                );
8195            }
8196            self.graph_want_logits = want_save;
8197            // restore IN PLACE: the pending verify graph wraps these very
8198            // allocations (zero-copy) — replacing the Vec would strand it
8199            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8200                if l.linear_state.len() == st.len() {
8201                    l.linear_state.copy_from_slice(&st);
8202                } else {
8203                    l.linear_state = st;
8204                }
8205            }
8206            for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
8207                let extra = l.seq_len.saturating_sub(n0);
8208                if extra > 0 {
8209                    l.truncate_last(extra);
8210                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
8211                }
8212            }
8213        }
8214        let t_verify = t_round.elapsed();
8215        let sub_verify = subs();
8216        // Acceptance. Greedy: row i's argmax is the trunk's token after
8217        // input i. Sampling: accept draft i with min(1, p_i/q_i), and on
8218        // the first rejection draw the correction from max(0, p_i − q_i)
8219        // — that token is committed by the loop top as-is (spec_forced).
8220        let mut a = 0usize;
8221        let mut forced: Option<u32> = None;
8222        let ids: Vec<u32> = if sparse {
8223            let mut p = std::mem::take(&mut self.spec_ps);
8224            let mut res = std::mem::take(&mut self.spec_ress);
8225            while a < k_spec {
8226                let ok = sampler::sparse_distribution_into(
8227                    &logits[a * lm_rows..(a + 1) * lm_rows],
8228                    &cfg,
8229                    all_ids,
8230                    &mut self.sampler_scratch,
8231                    self.pool.as_deref(),
8232                    &mut p,
8233                );
8234                if !ok {
8235                    let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
8236                    p.clear();
8237                    p.push((t, 1.0));
8238                }
8239                match sampler::spec_accept_or_correct_sparse(
8240                    &p,
8241                    &self.spec_qs[a],
8242                    drafts[a],
8243                    &mut self.rng,
8244                    &mut res,
8245                ) {
8246                    None => {
8247                        all_ids.push(drafts[a]);
8248                        a += 1;
8249                    }
8250                    Some(c) => {
8251                        forced = Some(c);
8252                        break;
8253                    }
8254                }
8255            }
8256            all_ids.truncate(base_len);
8257            self.spec_ps = p;
8258            self.spec_ress = res;
8259            drafts.clone()
8260        } else if sampling {
8261            let mut p = std::mem::take(&mut self.spec_p);
8262            let mut res = std::mem::take(&mut self.spec_res);
8263            while a < k_spec {
8264                sampler::distribution_into(
8265                    &logits[a * lm_rows..(a + 1) * lm_rows],
8266                    &cfg,
8267                    all_ids,
8268                    &mut self.sampler_scratch,
8269                    self.pool.as_deref(),
8270                    &mut p,
8271                );
8272                match sampler::spec_accept_or_correct(
8273                    &p,
8274                    &self.spec_q[a],
8275                    drafts[a],
8276                    &mut self.rng,
8277                    &mut res,
8278                    self.pool.as_deref(),
8279                ) {
8280                    None => {
8281                        all_ids.push(drafts[a]);
8282                        a += 1;
8283                    }
8284                    Some(c) => {
8285                        forced = Some(c);
8286                        break;
8287                    }
8288                }
8289            }
8290            all_ids.truncate(base_len);
8291            self.spec_p = p;
8292            self.spec_res = res;
8293            // the accepted drafts ARE the verified tokens after inputs 0..a
8294            drafts.clone()
8295        } else if greedy_pen {
8296            // Row i's penalized argmax, penalties over the stream that
8297            // includes the accepted drafts before it — the plain loop's
8298            // exact arithmetic, one pass per row, no working copy.
8299            let mut ids: Vec<u32> = Vec::with_capacity(b);
8300            for i in 0..b {
8301                let t = sampler::argmax_penalized(
8302                    &logits[i * lm_rows..(i + 1) * lm_rows],
8303                    &cfg,
8304                    all_ids,
8305                    &mut self.sampler_scratch,
8306                    self.pool.as_deref(),
8307                );
8308                ids.push(t);
8309                if i < k_spec && t == drafts[i] {
8310                    all_ids.push(t);
8311                } else {
8312                    break;
8313                }
8314            }
8315            all_ids.truncate(base_len);
8316            while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8317                a += 1;
8318            }
8319            // rows past the first mismatch were never scored; the loop
8320            // top re-samples the last verified row itself.
8321            ids
8322        } else if greedy_dev && dev_ids.len() == b {
8323            let ids = std::mem::take(&mut dev_ids);
8324            while a < k_spec && ids[a] == drafts[a] {
8325                a += 1;
8326            }
8327            ids
8328        } else {
8329            if logits.len() < b * lm_rows {
8330                // the device argmax was asked for and came back short:
8331                // no rows to fall back on — terminal like a failed batch
8332                self.clear_sequence_state();
8333                self.graph_failed
8334                    .store(true, std::sync::atomic::Ordering::Relaxed);
8335                self.cancel
8336                    .store(true, std::sync::atomic::Ordering::Relaxed);
8337                tracing::error!("Metal verify returned neither logits nor argmax ids");
8338                return None;
8339            }
8340            let ids: Vec<u32> = (0..b)
8341                .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8342                .collect();
8343            while a < k_spec && ids[a] == drafts[a] {
8344                a += 1;
8345            }
8346            ids
8347        };
8348        spec_stamp("acc");
8349        if spec_dbg {
8350            eprintln!(
8351                "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8352                drafts, ids
8353            );
8354        }
8355        // CMF_METAL_VERIFY_CHECK=2: the commit oracle — plain-forward the
8356        // a+1 accepted tokens from a snapshot, then diff the replayed GDN
8357        // states and the appended K/V rows against that.
8358        #[cfg(target_os = "macos")]
8359        let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8360            && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8361        {
8362            let snap: Vec<Vec<f32>> = self
8363                .kv_cache
8364                .layers
8365                .iter()
8366                .map(|l| l.linear_state.clone())
8367                .collect();
8368            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8369            let toks: Vec<u32> = std::iter::once(t_next)
8370                .chain(drafts.iter().copied())
8371                .collect();
8372            let want_save = self.graph_want_logits;
8373            self.graph_want_logits = false;
8374            for (i, &t) in toks.iter().take(a + 1).enumerate() {
8375                let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8376                let _ = self.graph_logits.take();
8377            }
8378            self.graph_want_logits = want_save;
8379            let plain_states: Vec<Vec<f32>> = self
8380                .kv_cache
8381                .layers
8382                .iter()
8383                .map(|l| l.linear_state.clone())
8384                .collect();
8385            let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8386            let mut rows = Vec::new();
8387            for (li, (l, n0)) in self
8388                .kv_cache
8389                .layers
8390                .iter_mut()
8391                .zip(attn_lens.iter())
8392                .enumerate()
8393            {
8394                let extra = l.seq_len.saturating_sub(*n0);
8395                if extra > 0 {
8396                    let mut kk = Vec::new();
8397                    let mut vv = Vec::new();
8398                    for g in 0..nkv {
8399                        kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8400                        vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8401                    }
8402                    rows.push((li, kk, vv));
8403                    l.truncate_last(extra);
8404                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8405                }
8406            }
8407            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8408                if l.linear_state.len() == st.len() {
8409                    l.linear_state.copy_from_slice(&st);
8410                } else {
8411                    l.linear_state = st;
8412                }
8413            }
8414            Some((plain_states, rows))
8415        } else {
8416            None
8417        };
8418        let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8419        // Metal: the MTP cache cut and the round's warm-up SUBMIT come
8420        // BEFORE the trunk commit, so the warm-up's command buffer is
8421        // queued ahead of the GDN replay (second queue) and its wait
8422        // below no longer sits behind the replay — measured: the warm-up's
8423        // wait grew with the accepted count exactly like the replay does
8424        // (8 ms at a=1, 17 ms at a=3, 25 ms at a=5 for ~2 ms of its own
8425        // work). The replay now overlaps the warm-up's readback, the
8426        // round's return and the next draft chain.
8427        #[cfg(target_os = "macos")]
8428        let mut warm_pending: Option<MetalWarmPending> = None;
8429        #[cfg(target_os = "macos")]
8430        if metal_native {
8431            m.kv.truncate_last(k_spec.saturating_sub(1));
8432            if self.mtp_graph_mode == Some(true) {
8433                // the mirror rows below the cut are the CPU rows: re-point,
8434                // no re-upload
8435                crate::gpu_metal::kv_mirror_set_stored(
8436                    self.mtp_kv_id(),
8437                    Self::MTP_LAYER_BASE,
8438                    m.kv.seq_len,
8439                );
8440                if !warm_off && a > 0 {
8441                    let pairs: Vec<(&[f32], u32)> = (0..a)
8442                        .map(|j| {
8443                            (
8444                                &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8445                                ids[j],
8446                            )
8447                        })
8448                        .collect();
8449                    warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8450                }
8451            }
8452            spec_stamp("c.wsub");
8453        }
8454        // a fully-accepted round needs no restore: every input was real.
8455        #[cfg(target_os = "macos")]
8456        if metal_native {
8457            // the Metal verify never wrote its states: the commit replays the
8458            // accepted prefix into the CPU owners and appends the K/V rows
8459            if !self.metal_verify_commit(a) {
8460                self.clear_sequence_state();
8461                self.graph_failed
8462                    .store(true, std::sync::atomic::Ordering::Relaxed);
8463                self.cancel
8464                    .store(true, std::sync::atomic::Ordering::Relaxed);
8465                tracing::error!("Metal verify state/KV handoff failed after admission");
8466                return None;
8467            }
8468            if let Some((plain_states, rows)) = commit_ref {
8469                crate::gpu_metal::queue_fence();
8470                // the commit's replay runs on the second queue: collect it
8471                // before the oracle reads the CPU owners it writes into
8472                let _ = crate::gpu_metal::wait_replay();
8473                let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8474                let mut worst_s = 0f32;
8475                let mut worst_li = 0usize;
8476                for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8477                    if l.linear_state.len() != ps.len() || ps.is_empty() {
8478                        continue;
8479                    }
8480                    let d = l
8481                        .linear_state
8482                        .iter()
8483                        .zip(ps)
8484                        .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8485                    let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8486                    let rel = d / n.max(1e-6);
8487                    if rel > worst_s {
8488                        worst_s = rel;
8489                        worst_li = li;
8490                    }
8491                }
8492                let mut worst_k = 0f32;
8493                for (li, kk, vv) in &rows {
8494                    let l = &self.kv_cache.layers[*li];
8495                    let n0 = l.seq_len - (kk.len() / (nkv * hd));
8496                    let mut ck = Vec::new();
8497                    let mut cv = Vec::new();
8498                    for g in 0..nkv {
8499                        ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8500                        cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8501                    }
8502                    if ck.len() == kk.len() {
8503                        let dk = ck
8504                            .iter()
8505                            .zip(kk)
8506                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8507                        let dv = cv
8508                            .iter()
8509                            .zip(vv)
8510                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8511                        worst_k = worst_k.max(dk).max(dv);
8512                    } else {
8513                        eprintln!(
8514                            "commit-check L{li}: kv row count mismatch {} vs {}",
8515                            ck.len(),
8516                            kk.len()
8517                        );
8518                    }
8519                }
8520                eprintln!(
8521                    "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}"
8522                );
8523            }
8524        }
8525        if !metal_native && a + 1 < b {
8526            let expected_gdn_layers = self.graph_gdn_layer_count();
8527            if expected_gdn_layers > 0
8528                && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8529            {
8530                self.clear_sequence_state();
8531                self.graph_failed
8532                    .store(true, std::sync::atomic::Ordering::Relaxed);
8533                self.cancel
8534                    .store(true, std::sync::atomic::Ordering::Relaxed);
8535                tracing::error!("GDN speculative restore failed after verify");
8536                return None;
8537            }
8538        }
8539        if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8540            // The verify graph committed the full batch, but one of its
8541            // persistent Full-attention mirrors could not be re-pointed to
8542            // the accepted prefix.  Treat that as terminal state failure;
8543            // an exact CPU fallback would otherwise consume stale GDN/KV.
8544            self.clear_sequence_state();
8545            self.graph_failed
8546                .store(true, std::sync::atomic::Ordering::Relaxed);
8547            self.cancel
8548                .store(true, std::sync::atomic::Ordering::Relaxed);
8549            tracing::error!("trunk graph KV rewind failed after speculative verify");
8550            return None;
8551        }
8552        *accepted += a;
8553        // MTP cache: keep the first draft row (its inputs were real), drop
8554        // the chain's, then append the verified pairs the round produced.
8555        // Each of those is a whole MTP block on the per-op path and they
8556        // cost 5.8 ms of a 69 ms round at k=3 — a third of what the
8557        // round's own draft costs. PRICED, and they earn it: skipping
8558        // them (`CMF_SPEC_WARM=0`) drops acceptance from 89% to 81% at
8559        // k=3 and 85% to 74% at k=4, and the tok/s goes nowhere at k=3
8560        // (50.3 against 50.5) and backwards at k=4 (48.1 against 50.1).
8561        // The knob stays so the next person can re-price it after the
8562        // warms are batched instead of assuming either way.
8563        if !metal_native {
8564            // (Metal cut its MTP cache before the trunk commit, above)
8565            m.kv.truncate_last(k_spec.saturating_sub(1));
8566        }
8567        spec_stamp("c.trunc");
8568        if !metal_native
8569            && self.mtp_graph_mode == Some(true)
8570            && !self.rewind_mtp_graph_mirror(next_pos)
8571        {
8572            // The graph draft was admitted, so inability to move its cursor
8573            // back to the real anchor is a state failure, not a capability
8574            // refusal.  Do not warm or continue with a stale mirror.
8575            self.clear_sequence_state();
8576            self.graph_failed
8577                .store(true, std::sync::atomic::Ordering::Relaxed);
8578            self.cancel
8579                .store(true, std::sync::atomic::Ordering::Relaxed);
8580            tracing::error!("MTP graph mirror rewind failed after verify commit");
8581            return None;
8582        }
8583        if !warm_off && a > 0 {
8584            // Graph arm: all accepted pairs in ONE batched run over the
8585            // MTP block; the token graph one by one if the batch declines.
8586            let mut warmed = false;
8587            #[cfg(target_os = "macos")]
8588            if metal_native && self.mtp_graph_mode == Some(true) {
8589                // the batched warm-up was submitted before the trunk
8590                // commit: collect it here; one by one on the token graph
8591                // if it declined (or failed)
8592                warmed = match warm_pending.take() {
8593                    Some(p) => self.mtp_warm_batch_finish(m, p),
8594                    None => false,
8595                };
8596                if !warmed {
8597                    warmed = true;
8598                    for j in 0..a {
8599                        let row =
8600                            hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8601                        if self
8602                            .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8603                            .is_none()
8604                        {
8605                            warmed = false;
8606                            break;
8607                        }
8608                    }
8609                }
8610            }
8611            if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8612                let rows: Vec<Vec<f32>> = (0..a)
8613                    .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8614                    .collect();
8615                let pairs: Vec<(&[f32], u32)> = rows
8616                    .iter()
8617                    .zip(ids.iter())
8618                    .map(|(r, &t)| (r.as_slice(), t))
8619                    .collect();
8620                match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8621                    Ok(()) => warmed = true,
8622                    Err(err) => {
8623                        // A warm-up failure after graph admission cannot
8624                        // fall back to `mtp_warm`: the detached CPU cache is
8625                        // not authoritative for the device mirror.  Mark it
8626                        // terminal so the generation caller clears state and
8627                        // returns instead of drafting from stale attention.
8628                        tracing::error!("{err}");
8629                        self.clear_sequence_state();
8630                        self.graph_failed
8631                            .store(true, std::sync::atomic::Ordering::Relaxed);
8632                        self.cancel
8633                            .store(true, std::sync::atomic::Ordering::Relaxed);
8634                        return None;
8635                    }
8636                }
8637            }
8638            if !warmed {
8639                for j in 0..a {
8640                    let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8641                    let row = row.to_vec();
8642                    self.mtp_warm(m, &row, ids[j], next_pos + j);
8643                }
8644            }
8645        }
8646        // The sampler's contract: logits of the LAST verified position —
8647        // unless a rejected draft already drew the correction, in which
8648        // case the loop top commits that token and samples nothing.
8649        spec_stamp("c.warm");
8650        if let Some(c) = forced {
8651            self.spec_forced = Some(c);
8652            self.graph_logits = None;
8653        } else if greedy_dev && logits.is_empty() {
8654            // the row's argmax IS the token the loop top would pick from
8655            // it (plain greedy, no penalties): commit it as forced
8656            self.spec_forced = Some(ids[a]);
8657            self.graph_logits = None;
8658        } else {
8659            let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8660            row.resize(self.vocab_size, 0.0);
8661            if let Some(c) = self.final_softcap {
8662                for l in row.iter_mut() {
8663                    *l = c * (*l / c).tanh();
8664                }
8665            }
8666            self.graph_logits = Some(row);
8667        }
8668        let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8669        spec_stamp("c.row");
8670        // Three phases, not two. The round's wall clock was 4 ms longer
8671        // than draft+verify and the difference had nowhere to be seen:
8672        // the accepted prefix re-runs the MTP block once per token to
8673        // keep the draft head's attention cache warm, and the GDN state
8674        // rolls back on any rejection. Both live here, after the verify.
8675        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8676            let end = subs();
8677            eprintln!(
8678                "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8679                 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8680                t_draft.as_secs_f64() * 1e3,
8681                sub_draft - sub0,
8682                (t_verify - t_draft).as_secs_f64() * 1e3,
8683                sub_verify - sub_draft,
8684                (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8685                end - sub_verify,
8686                self.draft_full_streak,
8687            );
8688        }
8689        // Native Metal's verify tile is flat in b (eight rows for the price
8690        // of one), so a shorter round only forfeits tokens — measured on
8691        // the M4: an essay round at k=2 still verified in 260 ms. The
8692        // adaptation is for cards whose verify grows with the rows.
8693        if k_env.is_none() && !metal_native && !k_capped {
8694            // Slow average and a wide band: a fast one oscillated 2↔3 on
8695            // an essay every other round (measured), which forfeits the
8696            // draft it just paid for.
8697            let f = a as f32 / k_spec.max(1) as f32;
8698            self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8699            let mut k_next = k_spec;
8700            if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8701                k_next = k_spec + 1;
8702            } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8703                k_next = k_spec - 1;
8704            }
8705            if k_next != k_spec {
8706                self.spec_acc_ewma = 0.6;
8707                if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8708                    eprintln!("spec-k: {k_spec} → {k_next}");
8709                }
8710            }
8711            self.spec_k_adapt = Some(k_next);
8712        }
8713        spec_stamp("end");
8714        Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8715    }
8716
8717    /// Micro-benchmark: two single-position forwards vs one fused pair
8718    /// from the current cache state (KV rewound after each probe).
8719    /// Returns (two_singles_ms, fused_pair_ms) per probe, or the (0, 0)
8720    /// sentinel when this model has no pair path to measure — the same
8721    /// answer the o1 arm gives, and the bench prints it the same way.
8722    /// (An architecture that loads its own layers leaves `weights.layers`
8723    /// empty; walking it here was an index panic, found by `bench` on
8724    /// deepseek_v4.)
8725    pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8726        if !self.pair_supported() {
8727            return (0.0, 0.0);
8728        }
8729        // A GDN model that needs an automatic device prefix never uses this
8730        // fused-pair path in normal generation: its recurrence is serial and
8731        // the speculative pair is disabled by default.  More importantly,
8732        // this diagnostic suppresses the token graph, so measuring it would
8733        // stream the non-resident tail through per-op Vulkan just to print a
8734        // non-production number.  Leave inference untouched and report the
8735        // established unavailable sentinel instead.
8736        if self.gdn_cfg.is_some()
8737            && self.graph_prefill_preferred()
8738            && self.automatic_gpu_prefix().is_some()
8739        {
8740            tracing::info!(
8741                "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8742            );
8743            return (0.0, 0.0);
8744        }
8745        // This is a host-side pair micro-benchmark. It truncates the host KV
8746        // after every probe, so letting the whole-token graph participate
8747        // would leave its device GDN/KV mirror ahead of the next probe and
8748        // poison the process-wide graph verdict before the real generation
8749        // benchmark starts. Keep the existing per-op/GPU arithmetic while
8750        // suppressing only the stateful token graph for this measurement.
8751        let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8752        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8753        let emb1 = self.embed_single(1);
8754        let emb2 = self.embed_single(2);
8755        let pos = self.kv_cache.seq_len();
8756
8757        let t0 = std::time::Instant::now();
8758        for _ in 0..iters {
8759            let _ = self.forward_layers(&emb1, pos, None);
8760            let _ = self.forward_layers(&emb2, pos + 1, None);
8761            for l in &mut self.kv_cache.layers {
8762                l.truncate_last(2);
8763            }
8764        }
8765        let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8766
8767        let t1 = std::time::Instant::now();
8768        for _ in 0..iters {
8769            let _ = self.forward_pair(&emb1, &emb2, pos);
8770            for l in &mut self.kv_cache.layers {
8771                l.truncate_last(2);
8772            }
8773        }
8774        let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8775        match graph_env {
8776            Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8777            None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8778        }
8779        (singles_ms, pair_ms)
8780    }
8781
8782    /// Fused two-position forward: weight rows are streamed from memory
8783    /// once per layer for both positions. Full layers → fused GQA pair;
8784    /// linear layers → vmf_phase pair (lane 2 state is tentative in the
8785    /// per-layer scratch until the draft is accepted).
8786    /// Whether the fused two-position path covers every layer kind in
8787    /// this model. MLA and KDA run per position (their pair arms are
8788    /// unreachable); the seq prefill falls back to singles for them.
8789    fn pair_supported(&self) -> bool {
8790        // An EMPTY layer stack means the architecture loaded its own and
8791        // this path has nothing to walk. Checking that directly, rather
8792        // than naming each such architecture, is what makes the guard hold
8793        // for the next one: `any()` over no layers is false, so a
8794        // feature-by-feature test says "supported" for a model that has no
8795        // layers here at all.
8796        !self.weights.layers.is_empty()
8797            && !self.per_token_stack()
8798            && !self
8799                .weights
8800                .layers
8801                .iter()
8802                .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8803    }
8804
8805    fn forward_pair(
8806        &mut self,
8807        emb1: &[f32],
8808        emb2: &[f32],
8809        position: usize,
8810    ) -> (Vec<f32>, Vec<f32>) {
8811        // A two-token prompt starts here, not in the layer walk: decide the
8812        // MiMo placement before the pair's per-op MoE uploads any expert.
8813        self.mimo_moe_prepare();
8814        let mut h1 = emb1.to_vec();
8815        let mut h2 = emb2.to_vec();
8816        let (_nkv, _hd, hs, _rd, eps) = (
8817            self.num_kv_heads,
8818            self.head_dim,
8819            self.hidden_size,
8820            self.rotary_dim,
8821            self.rms_eps,
8822        );
8823        let pool = self.pool.clone();
8824        // `forward_pair` bypasses `forward_layers_span`, but its per-op
8825        // body must still obey the ordinary layer placement. In particular,
8826        // an over-capacity Vulkan model has a device prefix and a CPU tail;
8827        // letting the pair walk submit the tail's selected MoE experts would
8828        // stream the whole tail through the residency arena and defeat the
8829        // measured VRAM bound.
8830        let automatic_gpu_prefix = self.automatic_gpu_prefix();
8831
8832        for li in 0..self.num_layers {
8833            let _capacity_tail = automatic_gpu_prefix
8834                .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8835                .map(|_| crate::gpu::enter_cpu_scope());
8836            // Also makes an explicit CMF_GPU_LAYERS split apply to fused
8837            // pairs, just as it does to the ordinary layer walk.
8838            crate::gpu::set_layer(li as i64);
8839            let lw = &self.weights.layers[self.phys_layer(li)];
8840            // Norms into pipeline scratch (4 allocs/layer on the MTP
8841            // decode hot path before this).
8842            inference::rms_norm_into(
8843                &h1,
8844                &lw.input_norm,
8845                self.rms_eps,
8846                self.norm_style,
8847                &mut self.ws.n1,
8848            );
8849            inference::rms_norm_into(
8850                &h2,
8851                &lw.input_norm,
8852                self.rms_eps,
8853                self.norm_style,
8854                &mut self.ws.n2,
8855            );
8856
8857            let (a1, a2) = match &lw.attn {
8858                AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8859                AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8860                AttnKind::Bounded(w) => {
8861                    // Two sequential positions of the bounded operator
8862                    // (the ring's causal order is the pair's order).
8863                    let rope = self
8864                        .bounded_rope
8865                        .clone()
8866                        .expect("bounded layer without an installed rotation table");
8867                    let cfg = crate::bounded::BoundedAttnCfg {
8868                        num_heads: self.num_heads,
8869                        num_kv_heads: self.num_kv_heads,
8870                        head_dim: self.head_dim,
8871                        hidden_size: hs,
8872                        scale: self.attn_scale,
8873                        rope: &rope,
8874                        pool: pool.as_deref(),
8875                    };
8876                    let a1 = crate::bounded::bounded_attention(
8877                        &self.ws.n1,
8878                        w,
8879                        &mut self.kv_cache.layers[li],
8880                        &cfg,
8881                    );
8882                    let a2 = crate::bounded::bounded_attention(
8883                        &self.ws.n2,
8884                        w,
8885                        &mut self.kv_cache.layers[li],
8886                        &cfg,
8887                    );
8888                    (a1, a2)
8889                }
8890                AttnKind::Linear(w) => {
8891                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8892                    let layer = &mut self.kv_cache.layers[li];
8893                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8894                    vmf_phase_pair(
8895                        &self.ws.n1,
8896                        &self.ws.n2,
8897                        w,
8898                        &cfg,
8899                        state,
8900                        scratch,
8901                        self.pool.as_deref(),
8902                    )
8903                }
8904                AttnKind::LinearGdn(w) => {
8905                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8906                    let layer = &mut self.kv_cache.layers[li];
8907                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8908                    gdn_pair(
8909                        &self.ws.n1,
8910                        &self.ws.n2,
8911                        w,
8912                        &cfg,
8913                        state,
8914                        scratch,
8915                        self.pool.as_deref(),
8916                    )
8917                }
8918                AttnKind::ShortConv(w) => {
8919                    let cfg = self
8920                        .short_conv_cfg
8921                        .expect("short-conv layer without short_conv_cfg");
8922                    let layer = &mut self.kv_cache.layers[li];
8923                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8924                    short_conv_pair(
8925                        &self.ws.n1,
8926                        &self.ws.n2,
8927                        w,
8928                        &cfg,
8929                        state,
8930                        scratch,
8931                        self.pool.as_deref(),
8932                    )
8933                }
8934                AttnKind::Full {
8935                    wq,
8936                    wk,
8937                    wv,
8938                    wo,
8939                    q_norm,
8940                    k_norm,
8941                    output_gate,
8942                    softplus_gate,
8943                    bias,
8944                } => {
8945                    let inv_freq_l = self.layer_inv_freq(li);
8946                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8947                    let cfg = QwenAttnCfg {
8948                        num_heads: self.layer_num_heads(li),
8949                        num_kv_heads: nkv_l,
8950                        head_dim: hd_l,
8951                        hidden_size: hs,
8952                        position,
8953                        inv_freq: &inv_freq_l,
8954                        rotary_dim: rd_l,
8955                        scale: self.attn_scale,
8956                        softcap: self.attn_softcap,
8957                        window: self.layer_window(li),
8958                        v_norm: self.attn_v_norm,
8959                        qk_norm_after_rope: self.qk_norm_after_rope,
8960                        gate_sigmoid: self.proj_gate_sigmoid,
8961                        q_norm: q_norm.as_deref(),
8962                        k_norm: k_norm.as_deref(),
8963                        output_gate: *output_gate,
8964                        softplus_gate: softplus_gate
8965                            .as_ref()
8966                            .map(|(gate, per_head)| (gate, *per_head)),
8967                        rope_scale: self.layer_rope_scale(li),
8968                        bias: bias
8969                            .as_ref()
8970                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8971                        rms_eps: eps,
8972                        norm_style: self.norm_style,
8973                        pool: pool.as_deref(),
8974                        v_head_dim: self.layer_v_dim(li),
8975                    };
8976                    attention::qwen_attention_pair(
8977                        &self.ws.n1,
8978                        &self.ws.n2,
8979                        wq,
8980                        wk,
8981                        wv,
8982                        wo,
8983                        &mut self.kv_cache.layers[li],
8984                        &cfg,
8985                    )
8986                }
8987            };
8988            let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8989                Some(w) => (
8990                    inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8991                    inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8992                ),
8993                None => (a1, a2),
8994            };
8995            for i in 0..self.hidden_size {
8996                h1[i] += a1[i];
8997                h2[i] += a2[i];
8998            }
8999            let (mut a1, mut a2) = (a1, a2);
9000            attention::recycle_buf(&mut a1);
9001            attention::recycle_buf(&mut a2);
9002
9003            let lw = &self.weights.layers[self.phys_layer(li)];
9004            inference::rms_norm_into(
9005                &h1,
9006                &lw.post_norm,
9007                self.rms_eps,
9008                self.norm_style,
9009                &mut self.ws.p1,
9010            );
9011            inference::rms_norm_into(
9012                &h2,
9013                &lw.post_norm,
9014                self.rms_eps,
9015                self.norm_style,
9016                &mut self.ws.p2,
9017            );
9018            let (f1, f2) = match &lw.ffn {
9019                // Dual-branch layers need the raw residuals — run the
9020                // two positions through the same fn decode uses.
9021                FfnKind::DenseMoe(dm) => (
9022                    dense_moe_ffn(
9023                        dm,
9024                        &self.ws.p1,
9025                        &h1,
9026                        self.rms_eps,
9027                        self.norm_style,
9028                        self.pool.as_deref(),
9029                    ),
9030                    dense_moe_ffn(
9031                        dm,
9032                        &self.ws.p2,
9033                        &h2,
9034                        self.rms_eps,
9035                        self.norm_style,
9036                        self.pool.as_deref(),
9037                    ),
9038                ),
9039                FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
9040                    moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
9041                    moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
9042                ),
9043                _ => ffn_forward_pair(
9044                    &lw.ffn,
9045                    &self.ws.p1,
9046                    &self.ws.p2,
9047                    self.pool.as_deref(),
9048                    None,
9049                ),
9050            };
9051            let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
9052                Some(w) => (
9053                    inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
9054                    inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
9055                ),
9056                None => (f1, f2),
9057            };
9058            for i in 0..self.hidden_size {
9059                h1[i] += f1[i];
9060                h2[i] += f2[i];
9061            }
9062            let (mut f1, mut f2) = (f1, f2);
9063            attention::recycle_buf(&mut f1);
9064            attention::recycle_buf(&mut f2);
9065            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
9066                for i in 0..self.hidden_size {
9067                    h1[i] *= sc;
9068                    h2[i] *= sc;
9069                }
9070            }
9071            // Looped Transformer: apply final norm at the end of each loop iteration.
9072            if self.is_loop_end(li) && li + 1 < self.num_layers {
9073                h1 = inference::rms_norm(
9074                    &h1,
9075                    &self.weights.final_norm,
9076                    self.rms_eps,
9077                    self.norm_style,
9078                );
9079                h2 = inference::rms_norm(
9080                    &h2,
9081                    &self.weights.final_norm,
9082                    self.rms_eps,
9083                    self.norm_style,
9084                );
9085            }
9086        }
9087        crate::gpu::set_layer(-1); // final/O(1) work is outside layer placement
9088        // Real O(1) prefill pairs may also carry tentative lane-2 recurrent
9089        // state. Commit it before publishing the transition epoch so the
9090        // next serial/device row cannot observe a new attention epoch with an
9091        // old GDN state. Speculative pairs run only when O(1) is inactive and
9092        // retain their existing caller-controlled commit/rollback semantics.
9093        if self.o1_active() {
9094            self.commit_linear_scratch();
9095        }
9096        self.o1_progress();
9097        self.swa_trim_tails();
9098        (h1, h2)
9099    }
9100
9101    /// Commit lane-2 linear states after an accepted draft.
9102    fn commit_linear_scratch(&mut self) {
9103        for layer in &mut self.kv_cache.layers {
9104            if !layer.linear_scratch.is_empty() {
9105                std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
9106                layer.linear_scratch.clear();
9107            }
9108        }
9109    }
9110
9111    /// This is the encoder counterpart to `forward_ids`: it runs a fresh
9112    /// sequence through the existing full/GDN hybrid stack and returns the
9113    /// final RMS-normalized hidden row for *every* token.  It is used by
9114    /// component pipelines whose text tower intentionally has no LM head
9115    /// (such as an image-conditioner Qwen3.5 model).  The sequence state is
9116    /// always released before returning, so a large text tower can be dropped
9117    /// immediately after conditioning a subsequent stage.
9118    pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9119        #[cfg(target_os = "macos")]
9120        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9121        if ids.is_empty() {
9122            return Err("empty id sequence".to_string());
9123        }
9124
9125        self.nll_begin()?;
9126        let result = (|| {
9127            let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9128            self.nll_check_graph("encoder prefill", 0)?;
9129            let expected = ids.len() * self.hidden_size;
9130            if hidden.len() != expected {
9131                return Err(format!(
9132                    "encoder prefill returned {} values for {} rows of hidden size {}",
9133                    hidden.len(),
9134                    ids.len(),
9135                    self.hidden_size
9136                ));
9137            }
9138
9139            let mut out = vec![0.0f32; expected];
9140            for (src, dst) in hidden
9141                .chunks_exact(self.hidden_size)
9142                .zip(out.chunks_exact_mut(self.hidden_size))
9143            {
9144                inference::rms_norm_into(
9145                    src,
9146                    &self.weights.final_norm,
9147                    self.rms_eps,
9148                    self.norm_style,
9149                    dst,
9150                );
9151            }
9152            Ok(out)
9153        })();
9154        self.nll_end();
9155        result
9156    }
9157
9158    /// The last layer's hidden row (before the final norm) for every token
9159    /// of `ids`, from a fresh sequence, through the whole-stack prefill (the
9160    /// device chunk graph where there is one) — what a distillation cache of
9161    /// teacher outputs needs, at prefill speed.
9162    pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9163        #[cfg(target_os = "macos")]
9164        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9165        if ids.is_empty() {
9166            return Err("empty id sequence".to_string());
9167        }
9168        self.nll_begin()?;
9169        let result = (|| {
9170            let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9171            self.nll_check_graph("hidden prefill", 0)?;
9172            if hidden.len() != ids.len() * self.hidden_size {
9173                return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
9174            }
9175            Ok(hidden)
9176        })();
9177        self.nll_end();
9178        result
9179    }
9180
9181    /// Forward a full id sequence from a fresh cache and return the
9182    /// logits after the last position (golden-parity harness, bench).
9183    pub fn forward_ids(
9184        &mut self,
9185        ids: &[u32],
9186        task_mask: Option<&TaskMask>,
9187    ) -> Result<Vec<f32>, String> {
9188        #[cfg(target_os = "macos")]
9189        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9190        if ids.is_empty() {
9191            return Err("empty id sequence".to_string());
9192        }
9193        self.clear_sequence_state();
9194        self.check_forward_graph("forward_ids setup", 0)?;
9195        if task_mask.is_none() {
9196            self.o1_begin();
9197        }
9198        let mut hidden = vec![0.0f32; self.hidden_size];
9199        let mut pos = 0usize;
9200        if let Some(b) = &mut self.dsv41 {
9201            let pool = self.pool.clone();
9202            let mut logits = Vec::new();
9203            crate::dsv41::forward_chunk(
9204                &b.0,
9205                &b.1,
9206                &b.2,
9207                &mut b.3,
9208                ids,
9209                0,
9210                pool.as_deref(),
9211                &mut logits,
9212            );
9213            if let Err(err) = self.o1_seal_checked() {
9214                self.clear_sequence_state();
9215                return Err(err);
9216            }
9217            return Ok(logits);
9218        }
9219        // Same routing predicate generation uses. Two reasons it must be
9220        // the same one: (1) a GDN hybrid's recurrent state is GPU-
9221        // resident, and a batched CPU prefill would build it on the host
9222        // only — decode then reads buffers the prefill never wrote;
9223        // (2) bench times THIS function and calls the result "prefill",
9224        // so a different path here reports a number production never
9225        // sees (W2 on 2×5090: 8.7 tok/s reported against 125 real).
9226        // Gemma-4: generation's batched-graph ingest (`g4_batch_ingest`),
9227        // so bench and the parity tools time and score the route run/serve
9228        // take; a refusal leaves the rest to the chunked host prefill.
9229        #[cfg(not(target_os = "macos"))]
9230        if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
9231            let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
9232                Ok(r) => r,
9233                Err(e) => {
9234                    self.clear_sequence_state();
9235                    return Err(e);
9236                }
9237            };
9238            if n > 0 {
9239                hidden.copy_from_slice(&last);
9240                pos = n;
9241            }
9242        }
9243        if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
9244            // prefill-GEMM in chunks; only the last position's hidden is
9245            // needed. (o1-compatible: the batch path attends per position
9246            // through qwen_attention, which carries the collection hook.)
9247            let chunk = self.prefill_chunk();
9248            let hs = self.hidden_size;
9249            while pos < ids.len() {
9250                let end = (pos + chunk).min(ids.len());
9251                let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9252                self.check_forward_graph("forward_ids batched prefill", end - 1)?;
9253                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
9254                pos = end;
9255            }
9256        }
9257        // Same guards as generation's prefill — INCLUDING the graph one.
9258        // The CPU pair walk was intercepting positions that the resident
9259        // token graph would have run itself: on a GDN hybrid over wgpu
9260        // that is 89 ms of host forward against 7 ms of device submit,
9261        // and it made prefill look 12× slower than it is (W2 on an RTX
9262        // 5090, ctx 512: 11.2 tok/s with the walk, 136.6 without).
9263        // CMF_PAIR=0 opts out; a model whose layers live outside
9264        // `weights.layers` has no pair walk to take.
9265        if task_mask.is_none()
9266            && !self.graph_prefill_preferred()
9267            && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9268            && self.pair_supported()
9269        {
9270            while pos + 1 < ids.len() {
9271                let e1 = self.embed_single(ids[pos]);
9272                let e2 = self.embed_single(ids[pos + 1]);
9273                let (_, h2) = self.forward_pair(&e1, &e2, pos);
9274                self.check_forward_graph("forward_ids pair", pos + 1)?;
9275                self.commit_linear_scratch();
9276                hidden = h2;
9277                pos += 2;
9278            }
9279        }
9280        // Resident Embryo graph: the prompt in chunks of one submit each
9281        // (the same device state and logits as the per-position walk).
9282        if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9283            if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9284                self.graph_logits = Some(lg);
9285                hidden = vec![0.0; self.hidden_size];
9286                pos = ids.len();
9287            }
9288        }
9289        while pos < ids.len() {
9290            hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9291            self.check_forward_graph("forward_ids", pos)?;
9292            pos += 1;
9293        }
9294        if let Some(logits) = self.graph_logits.take() {
9295            // Resident stacks already applied final norm and their head in
9296            // the same submit; do not run a second norm/head over the zero
9297            // hidden sentinel returned by forward_layers_span.
9298            if let Err(err) = self.o1_seal_checked() {
9299                self.clear_sequence_state();
9300                return Err(err);
9301            }
9302            return Ok(logits);
9303        }
9304        // Harness contract: after forward_ids the cache is decode-ready —
9305        // under o1 that means sealed (bench measures the seal as part of
9306        // prefill, honestly).
9307        if let Err(err) = self.o1_seal_checked() {
9308            self.clear_sequence_state();
9309            return Err(err);
9310        }
9311        let normed = inference::rms_norm(
9312            &hidden,
9313            &self.weights.final_norm,
9314            self.rms_eps,
9315            self.norm_style,
9316        );
9317        Ok(self.lm_head_forward(&normed))
9318    }
9319
9320    /// Run the V4.1 stack one token at a time and retain logits for every
9321    /// position. This is a diagnostic surface for comparing a converted
9322    /// checkpoint with a tokenwise reference implementation.
9323    #[doc(hidden)]
9324    pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9325        #[cfg(target_os = "macos")]
9326        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9327        if ids.is_empty() {
9328            return Err("empty id sequence".to_string());
9329        }
9330        self.clear_sequence_state();
9331        self.dsv41
9332            .as_ref()
9333            .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9334        self.o1_begin();
9335        let rows = {
9336            let pool = self.pool.clone();
9337            let b = self
9338                .dsv41
9339                .as_mut()
9340                .expect("dsv41 checked above; state cannot change during forward");
9341            let mut rows = Vec::with_capacity(ids.len());
9342            for (position, &id) in ids.iter().enumerate() {
9343                let mut logits = Vec::new();
9344                crate::dsv41::forward_token(
9345                    &b.0,
9346                    &b.1,
9347                    &b.2,
9348                    &mut b.3,
9349                    id,
9350                    position,
9351                    pool.as_deref(),
9352                    &mut logits,
9353                );
9354                rows.push(logits);
9355            }
9356            rows
9357        };
9358        self.o1_seal();
9359        Ok(rows)
9360    }
9361
9362    /// GPTQ calibration of DeepSeek-V4.1 (`cortiq quantize-moe`): the MoE
9363    /// input rows and routed expert ids of layers `lo..hi` for `ids` (a fresh
9364    /// sequence), see [`crate::dsv41::Dsv41MoeCapture`].
9365    pub fn dsv41_moe_capture(
9366        &mut self,
9367        ids: &[u32],
9368        lo: usize,
9369        hi: usize,
9370    ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9371        #[cfg(target_os = "macos")]
9372        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9373        if ids.is_empty() {
9374            return Err("empty id sequence".to_string());
9375        }
9376        self.dsv41
9377            .as_ref()
9378            .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9379        self.clear_sequence_state();
9380        let res = {
9381            let pool = self.pool.clone();
9382            let b = self.dsv41.as_mut().expect("dsv41 checked above");
9383            crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9384        };
9385        self.clear_sequence_state();
9386        res
9387    }
9388
9389    /// Teacher-forced perplexity over a token sequence (phase-C gate:
9390    /// honest quant comparisons instead of prompt vibes).
9391    ///
9392    /// Attention is EXACT even on a model whose layers are flagged for
9393    /// the O(1) kernel — scoring the backbone is the default on purpose
9394    /// (it is the yardstick). `nll_ids_o1` scores the CONVERTED model.
9395    pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9396        let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9397        Ok((nll / cnt.max(1) as f64).exp())
9398    }
9399
9400    /// GPTQ calibration forward: `ids` through layers `0..upto` only, in
9401    /// layer-major chunks of `chunk` positions, with no vocabulary head and
9402    /// no scoring. The Hessian hook (`gptq_capture`) folds every
9403    /// projection's input on the way; the layers above the captured span
9404    /// and the head would only cost time. Falls back to a scored forward
9405    /// where the batched prefill does not apply.
9406    pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9407        if !self.can_prefill_batched() {
9408            return self.nll_ids_from(ids, 0).map(|_| ());
9409        }
9410        self.nll_begin()?;
9411        let result: Result<(), String> = (|| {
9412            let chunk = chunk.max(1);
9413            let mut pos = 0usize;
9414            while pos < ids.len() {
9415                let end = (pos + chunk).min(ids.len());
9416                let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9417                self.nll_check_graph("calibration prefill", pos)?;
9418                pos = end;
9419            }
9420            Ok(())
9421        })();
9422        self.nll_end();
9423        result
9424    }
9425
9426    /// Layer-streamed calibration of one fresh window: layers `from..mid` run
9427    /// on `input` (the hidden state entering layer `from`; `None` = the
9428    /// token embeddings of `ids`) and their output is returned, then layers
9429    /// `mid..upto` run on that output for the capture hooks (their output is
9430    /// dropped). A caller that keeps the returned state per window runs each
9431    /// layer once over the whole conversion instead of re-running the stack
9432    /// below every layer. With `mid == from` the input is returned as is
9433    /// (`None` input: an empty vector).
9434    pub fn calib_window_span(
9435        &mut self,
9436        ids: &[u32],
9437        input: Option<&[f32]>,
9438        from: usize,
9439        mid: usize,
9440        upto: usize,
9441    ) -> Result<Vec<f32>, String> {
9442        if !self.can_prefill_batched() {
9443            return Err("calib_window_span: this architecture has no batched prefill".into());
9444        }
9445        self.nll_begin()?;
9446        let result: Result<Vec<f32>, String> = (|| {
9447            let h = if mid > from {
9448                let inp = match input {
9449                    Some(h) => PrefillIn::Hidden(h),
9450                    None => PrefillIn::Ids(ids),
9451                };
9452                let h = self.prefill_batch_span(inp, 0, None, from, mid);
9453                self.nll_check_graph("calibration span", 0)?;
9454                h
9455            } else {
9456                input.map(<[f32]>::to_vec).unwrap_or_default()
9457            };
9458            if upto > mid {
9459                let inp = if mid > from || input.is_some() {
9460                    PrefillIn::Hidden(&h)
9461                } else {
9462                    PrefillIn::Ids(ids)
9463                };
9464                let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9465                self.nll_check_graph("calibration span", 0)?;
9466            }
9467            Ok(h)
9468        })();
9469        self.nll_end();
9470        result
9471    }
9472
9473    /// GPTQ calibration of a Flash-Next-class MoE (`qwen4_exp`): MoE input
9474    /// rows and router logits of layers `lo..hi` for `ids` (a fresh
9475    /// sequence), per layer `[ids.len()][hidden + n_experts]`.
9476    #[cfg(feature = "gpu")]
9477    pub fn qwen4_capture(
9478        &mut self,
9479        ids: &[u32],
9480        lo: usize,
9481        hi: usize,
9482    ) -> Result<Vec<Vec<f32>>, String> {
9483        let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9484        crate::qwen4_exp::capture_window(
9485            &b.0,
9486            &b.1,
9487            &b.2,
9488            &mut b.3,
9489            ids,
9490            lo,
9491            hi,
9492            &self.inv_freq,
9493            self.pool.as_deref(),
9494        )
9495    }
9496
9497    /// GPTQ calibration capture of an ordinary MoE stack (Qwen3.5/3.6
9498    /// MoE, Clover): `ids` from position 0 through the batched device graph
9499    /// in prefill-sized chunks, the MoE input rows and router logits of
9500    /// layers `lo..hi` copied out after routing. Returns per layer
9501    /// `[ids.len()][hidden + n_experts]`; the sequence state is left fresh.
9502    #[cfg(feature = "gpu")]
9503    pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9504        use crate::gpu_wgpu::clover as cap;
9505        let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9506        let ne = (lo..hi)
9507            .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9508                FfnKind::Moe(m) => Some(m.experts.len()),
9509                _ => None,
9510            })
9511            .ok_or("no MoE layer in the capture range")?;
9512        let chunk = self.prefill_chunk().max(1);
9513        self.clear_sequence_state();
9514        if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9515            return Err("capture unavailable (no wgpu device)".into());
9516        }
9517        let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9518        let res = (|| {
9519            for (ci, part) in ids.chunks(chunk).enumerate() {
9520                let mut hiddens = Vec::with_capacity(part.len() * hs);
9521                for &id in part {
9522                    hiddens.extend_from_slice(&self.embed_single(id));
9523                }
9524                let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9525                match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9526                    crate::gpu::BatchGraphOutcome::Completed => {}
9527                    _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9528                }
9529                let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9530                if written < hi - lo {
9531                    return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9532                }
9533                for (o, r) in out.iter_mut().zip(rows) {
9534                    o.extend_from_slice(&r);
9535                }
9536            }
9537            Ok(())
9538        })();
9539        cap::moe_capture_end();
9540        self.clear_sequence_state();
9541        res.map(|_| out)
9542    }
9543
9544    /// Expert directory triples (gate, up, down) of an ordinary MoE layer,
9545    /// routed and shared.
9546    pub fn moe_expert_dirs(
9547        &self,
9548        li: usize,
9549    ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9550        let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9551            return None;
9552        };
9553        let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9554        let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9555        let shared = match &m.shared {
9556            Some((se, _)) => Some(dirs(se)?),
9557            None => None,
9558        };
9559        Some((routed, shared))
9560    }
9561
9562    /// Expert directory triples of a `qwen4_exp` layer (routed, shared).
9563    pub fn qwen4_expert_dirs(
9564        &self,
9565        li: usize,
9566    ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9567        let b = self.qwen4_exp.as_ref()?;
9568        crate::qwen4_exp::expert_dirs(&b.1, li)
9569    }
9570
9571    /// Teacher-force the installed MTP draft stack and collect each draft
9572    /// FFN's own pre-down activation statistics.
9573    ///
9574    /// This is deliberately separate from [`Self::probe_ffn_mass`]: canonical
9575    /// MTP and MiMo sidecars can have independently compacted FFNs, and MiMo
9576    /// draft layers can differ in width from one another and from the trunk.
9577    /// The returned vector follows the actual draft stack (one entry for a
9578    /// canonical MTP head; one per MiMo draft layer). MiMo layer `k` replays
9579    /// the teacher-forced pairs `(hidden[j], ids[j + k + 1])`.
9580    ///
9581    /// Calibration starts from a fresh sequence and leaves the trunk, draft
9582    /// KVs, history, and device mirrors fresh on both success and error. It
9583    /// is therefore unsuitable for the middle of a conversation whose KV
9584    /// cache must be retained.
9585    pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9586        self.reset_mtp_probe_state();
9587        // The trunk runs as one prefill (the device graph where there is
9588        // one); the draft replays on the host under a CPU scope, where its
9589        // FFN input can be measured. This guard also suppresses gate/oracle
9590        // sparsity experiments for the duration so the statistics and
9591        // admission replay always execute the full dense FFN.
9592        let _exact_calibration = ExactMtpCalibrationGuard::enter();
9593        let result = if ids.len() < 2 {
9594            Err("MTP FFN probe needs at least two token ids".into())
9595        } else if ids
9596            .iter()
9597            .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9598        {
9599            Err("MTP FFN probe received a token id outside the embedding table".into())
9600        } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9601            // A synthetic pipeline can carry both mechanisms, but merging
9602            // unlike draft semantics into one compaction plan is unsafe.
9603            Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9604        } else if self.mimo_mtp.is_some() {
9605            let mut st = self
9606                .mimo_mtp
9607                .take()
9608                .expect("checked MiMo MTP before detaching it");
9609            let result = match self.probe_trunk_rows(ids) {
9610                Ok(trunk) => crate::gpu::cpu_scope(|| {
9611                    self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
9612                }),
9613                Err(e) => Err(e),
9614            };
9615            // The owner is detached during the replay, so the common trunk
9616            // reset cannot clear its KVs. Restore it even on an error.
9617            st.reset();
9618            self.mimo_mtp = Some(st);
9619            result
9620        } else if self.mtp.is_some() {
9621            let mut mtp = self
9622                .mtp
9623                .take()
9624                .expect("checked canonical MTP before detaching it");
9625            let result = match self.probe_trunk_rows(ids) {
9626                Ok(trunk) => crate::gpu::cpu_scope(|| {
9627                    self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
9628                }),
9629                Err(e) => Err(e),
9630            };
9631            mtp.kv.clear();
9632            self.mtp = Some(mtp);
9633            result
9634        } else {
9635            Err("MTP FFN probe: this pipeline has no draft stack".into())
9636        };
9637        // Cover every early/error arm above: no failed calibration can leak
9638        // a partially populated trunk or draft cache into the next request.
9639        self.reset_mtp_probe_state();
9640        result
9641    }
9642
9643    /// Reset every state owner touched by [`Self::probe_mtp_ffn`].
9644    fn reset_mtp_probe_state(&mut self) {
9645        self.clear_sequence_state();
9646        if let Some(mtp) = self.mtp.as_mut() {
9647            mtp.kv.clear();
9648        }
9649        if let Some(mimo) = self.mimo_mtp.as_mut() {
9650            mimo.reset();
9651        }
9652        self.mtp_graph_mode = None;
9653    }
9654
9655    /// The trunk's last hidden row (before the final norm) at every
9656    /// position a teacher-forced draft probe reads (`ids[..len-1]`), from
9657    /// the whole-stack prefill — the device graph where there is one. One
9658    /// position at a time through `forward_layers` on the host this was the
9659    /// probe's whole cost: a 14B's 96k calibration tokens ran for hours.
9660    pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9661        if ids.len() < 2 {
9662            return Err("MTP FFN probe needs at least two token ids".into());
9663        }
9664        let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
9665        self.clear_sequence_state();
9666        Ok(rows)
9667    }
9668
9669    /// Canonical one-layer MTP teacher-forced replay over the trunk rows
9670    /// of [`Self::probe_trunk_rows`]. Its cache advances for every row so
9671    /// later attention inputs match a real draft warm-up rather than a
9672    /// collection of independent one-token measurements. The FFN inputs are
9673    /// measured in batches: nothing after the FFN feeds a later row.
9674    fn mtp_ffn_probe_teacher_forced(
9675        &mut self,
9676        mtp: &mut MtpModule,
9677        ids: &[u32],
9678        trunk: &[f32],
9679    ) -> Result<Vec<MtpFfnProbe>, String> {
9680        let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9681            return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9682        };
9683        let inter = dense.gate_proj.rows();
9684        let (hs, n) = (self.hidden_size, ids.len() - 1);
9685        if trunk.len() != n * hs {
9686            return Err(format!(
9687                "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
9688                trunk.len()
9689            ));
9690        }
9691        let mut mass = vec![0.0f64; inter];
9692        let mut squared_mass = vec![0.0f64; inter];
9693        let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
9694        for position in 0..n {
9695            self.mtp_probe_ffn_input(
9696                mtp,
9697                &trunk[position * hs..(position + 1) * hs],
9698                ids[position + 1],
9699                position,
9700            )?;
9701            inputs.extend_from_slice(&self.ws.p1[..hs]);
9702            if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
9703                let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9704                    unreachable!("checked dense above");
9705                };
9706                accumulate_dense_ffn_activation_mass(
9707                    dense,
9708                    &inputs,
9709                    inputs.len() / hs,
9710                    self.pool.as_deref(),
9711                    &mut mass,
9712                    &mut squared_mass,
9713                )?;
9714                inputs.clear();
9715            }
9716        }
9717        Ok(vec![MtpFfnProbe {
9718            mass,
9719            squared_mass,
9720            observed_positions: n,
9721        }])
9722    }
9723
9724    /// CPU-only MTP block replay up to the FFN's input (the post-attention
9725    /// norm, left in `ws.p1`), where the probe measures. The ordinary
9726    /// `mtp_step_hl` device path keeps these values resident and therefore
9727    /// cannot truthfully provide a per-neuron calibration statistic. The
9728    /// FFN arm itself is not run: its output feeds only this row's logits.
9729    fn mtp_probe_ffn_input(
9730        &mut self,
9731        mtp: &mut MtpModule,
9732        hidden: &[f32],
9733        next_token: u32,
9734        position: usize,
9735    ) -> Result<(), String> {
9736        let e = self.embed_single(next_token);
9737        let mut cat = vec![0.0f32; 2 * self.hidden_size];
9738        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9739        inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9740        inference::rms_norm_into(
9741            hidden,
9742            &mtp.hnorm,
9743            self.rms_eps,
9744            self.norm_style,
9745            cat_h,
9746        );
9747        let mut x = vec![0.0f32; self.hidden_size];
9748        mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9749
9750        let lw = &mtp.layer;
9751        inference::rms_norm_into(
9752            &x,
9753            &lw.input_norm,
9754            self.rms_eps,
9755            self.norm_style,
9756            &mut self.ws.n1,
9757        );
9758        let attn = match &lw.attn {
9759            AttnKind::Full {
9760                wq,
9761                wk,
9762                wv,
9763                wo,
9764                q_norm,
9765                k_norm,
9766                output_gate,
9767                softplus_gate,
9768                bias,
9769            } => {
9770                let mut cfg = self.attn_cfg(position);
9771                cfg.q_norm = q_norm.as_deref();
9772                cfg.k_norm = k_norm.as_deref();
9773                cfg.output_gate = *output_gate;
9774                cfg.softplus_gate = softplus_gate
9775                    .as_ref()
9776                    .map(|(gate, per_head)| (gate, *per_head));
9777                cfg.bias = bias
9778                    .as_ref()
9779                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9780                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
9781            }
9782            _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
9783        };
9784        for (dst, src) in x.iter_mut().zip(attn) {
9785            *dst += src;
9786        }
9787        inference::rms_norm_into(
9788            &x,
9789            &lw.post_norm,
9790            self.rms_eps,
9791            self.norm_style,
9792            &mut self.ws.p1,
9793        );
9794        Ok(())
9795    }
9796
9797    /// DTG-MA calibration pass (Patent 2): run `ids` through the model
9798    /// (CPU path, per position) and return each layer's per-neuron
9799    /// activation mass Σ|silu(gate)·up| — the statistic the task-guided
9800    /// FFN mask is derived from.
9801    pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9802        self.clear_sequence_state();
9803        FFN_PROBE.with(|p| {
9804            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9805        });
9806        crate::gpu::cpu_scope(|| {
9807            for (pos, &id) in ids.iter().enumerate() {
9808                let emb = self.embed_single(id);
9809                let _ = self.forward_layers(&emb, pos, None);
9810            }
9811        });
9812        self.clear_sequence_state();
9813        FFN_PROBE
9814            .with(|p| p.borrow_mut().take())
9815            .unwrap_or_default()
9816    }
9817
9818    /// `probe_ffn_mass` over the BATCHED prefill: same accumulator, one
9819    /// sweep instead of one forward per token. What makes the statistic
9820    /// affordable on a 27B.
9821    pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9822        if let Err(err) = self.nll_begin() {
9823            // A recorder can be left by a caller that was interrupted before
9824            // this request entered its scoring block.  Consume it even when
9825            // the preflight failure prevents initialization of a new one.
9826            let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9827            self.nll_end();
9828            return Err(err);
9829        }
9830        FFN_PROBE.with(|p| {
9831            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9832        });
9833        let result: Result<(), String> = (|| {
9834            for chunk in ids.chunks(256) {
9835                if chunk.len() < 2 {
9836                    continue;
9837                }
9838                self.nll_ids_masked(chunk, 0, None)?;
9839            }
9840            Ok(())
9841        })();
9842        self.nll_end();
9843        let probe = FFN_PROBE
9844            .with(|p| p.borrow_mut().take())
9845            .unwrap_or_default();
9846        match result {
9847            Ok(()) => Ok(probe),
9848            Err(err) => {
9849                drop(probe);
9850                Err(err)
9851            }
9852        }
9853    }
9854
9855    /// Teacher-forced PPL with a task mask active (sparse execution) —
9856    /// the quality gate for a DTG-MA-masked skill. Sequential per
9857    /// position: the batched prefill path is dense-only.
9858    pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9859        self.nll_begin()?;
9860        let result: Result<f64, String> = (|| {
9861            let mut nll = 0f64;
9862            let mut cnt = 0usize;
9863            let mut hidden = vec![0f32; self.hidden_size];
9864            for (pos, &id) in ids.iter().enumerate() {
9865                if pos > 0 {
9866                    inference::rms_norm_into(
9867                        &hidden,
9868                        &self.weights.final_norm,
9869                        self.rms_eps,
9870                        self.norm_style,
9871                        &mut self.ws.n1,
9872                    );
9873                    let mut logits = self.lm_head_forward(&self.ws.n1);
9874                    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9875                    let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9876                    let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9877                    nll -= p.max(1e-300).ln();
9878                    cnt += 1;
9879                    attention::recycle_buf(&mut logits);
9880                }
9881                let emb = self.embed_single(id);
9882                hidden = self.forward_layers(&emb, pos, Some(mask));
9883                self.nll_check_graph("masked serial forward", pos)?;
9884                // Consume a possible graph logits side channel before the
9885                // next row.  Masked scoring normally disables that route,
9886                // but stale channel state must never survive a request.
9887                let _ = self.graph_logits.take();
9888            }
9889            Ok((nll / cnt.max(1) as f64).exp())
9890        })();
9891        self.nll_end();
9892        result
9893    }
9894
9895    /// Teacher-forced NLL sum + scored-token count over positions
9896    /// `start..len-1`, attention EXACT. Positions below `start` still
9897    /// run — they are the context — they are just not scored, so this
9898    /// pairs with `nll_ids_o1(ids, start)` over the very same tokens.
9899    ///
9900    /// Returning (nll, cnt) rather than a ppl is what lets a windowed
9901    /// caller combine windows before the exp, so every scored token
9902    /// weighs the same regardless of how the windows are cut.
9903    /// `nll_ids_from` with a task mask held active at every position.
9904    ///
9905    /// The batched prefill path does not thread masks, so this walks the
9906    /// per-position forward — slower, but it scores the file exactly the
9907    /// way `run --task` will serve it, which is the point of the gate
9908    /// that calls it. With `None` it defers to the fast path.
9909    /// Masked scoring rides the SAME batched sweep as unmasked scoring —
9910    /// the masked-inference fast path: `prefill_batch_masked` lands the
9911    /// per-visit FFN rows on the activations inside the fused arms. The
9912    /// per-position loop below remains only as the no-batch fallback.
9913    pub fn nll_ids_masked(
9914        &mut self,
9915        ids: &[u32],
9916        start: usize,
9917        task_mask: Option<&TaskMask>,
9918    ) -> Result<(f64, usize), String> {
9919        let task_mask = self.drop_open_mask(task_mask);
9920        self.nll_ids_inner(ids, start, task_mask)
9921    }
9922
9923    pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9924        self.nll_ids_inner(ids, start, None)
9925    }
9926
9927    /// Diagnostic (`CMF_PREFILL_PARITY=split[,k]` on `cortiq ppl`): the same
9928    /// tokens through three ingests, each from a clean sequence — H the host
9929    /// chunked prefill, G the wgpu batch graph in k-row chunks, M the host
9930    /// prefill up to `split` and the batch graph after it, seeded from the
9931    /// host state — and where they part: NLL and top-1 per 256-position bin
9932    /// from each arm's post-stack rows, every GDN layer's recurrent state
9933    /// (the host `linear_state` against the graph's) at `split` and at the
9934    /// end, and every exact-attention layer's K/V rows.
9935    pub fn prefill_parity_probe(
9936        &mut self,
9937        ids: &[u32],
9938        split: usize,
9939        bk: usize,
9940    ) -> Result<String, String> {
9941        use std::fmt::Write as _;
9942        let (n, hs) = (ids.len(), self.hidden_size);
9943        if split == 0 || split >= n || bk == 0 || !self.can_prefill_batched() {
9944            return Err(format!(
9945                "prefill parity: needs 0 < split < {n} tokens, k > 0 and a batched host prefill"
9946            ));
9947        }
9948        // 1 = GDN, 2 = exact attention, 0 = anything else (not compared).
9949        let kinds: Vec<u8> = (0..self.num_layers)
9950            .map(|li| match &self.weights.layers[self.phys_layer(li)].attn {
9951                AttnKind::LinearGdn(_) => 1,
9952                AttnKind::Full { .. } => 2,
9953                _ => 0,
9954            })
9955            .collect();
9956        let kv_id = self.graph_kv_id;
9957        let host_chunk = self.prefill_chunk().max(1);
9958        let host_states = |p: &Self| -> Vec<Vec<f32>> {
9959            (0..p.num_layers)
9960                .map(|li| {
9961                    if kinds[li] == 1 {
9962                        p.kv_cache.layers[li].linear_state.clone()
9963                    } else {
9964                        Vec::new()
9965                    }
9966                })
9967                .collect()
9968        };
9969        let graph_states = |_p: &Self| -> Vec<Vec<f32>> {
9970            (0..kinds.len())
9971                .map(|li| {
9972                    if kinds[li] == 1 {
9973                        crate::gpu::graph_gdn_state_read(kv_id, li).unwrap_or_default()
9974                    } else {
9975                        Vec::new()
9976                    }
9977                })
9978                .collect()
9979        };
9980        // Position-major K/V rows [0, n) of every exact-attention layer.
9981        let host_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
9982            (0..p.num_layers)
9983                .map(|li| {
9984                    let c = &p.kv_cache.layers[li];
9985                    if kinds[li] != 2
9986                        || c.mode != crate::kv_cache::KvMode::F32
9987                        || c.base() != 0
9988                        || c.seq_len != n
9989                    {
9990                        return (Vec::new(), Vec::new());
9991                    }
9992                    let (nkv, hd) = (c.num_kv_heads, c.head_dim);
9993                    let (mut k, mut v) = (vec![0f32; n * nkv * hd], vec![0f32; n * nkv * hd]);
9994                    for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
9995                        for pos in 0..n {
9996                            let d = (pos * nkv + h) * hd;
9997                            k[d..d + hd].copy_from_slice(&kh[pos * hd..(pos + 1) * hd]);
9998                            v[d..d + hd].copy_from_slice(&vh[pos * hd..(pos + 1) * hd]);
9999                        }
10000                    }
10001                    (k, v)
10002                })
10003                .collect()
10004        };
10005        let graph_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
10006            (0..p.num_layers)
10007                .map(|li| {
10008                    let c = &p.kv_cache.layers[li];
10009                    if kinds[li] != 2 {
10010                        return (Vec::new(), Vec::new());
10011                    }
10012                    crate::gpu::graph_kv_read_rows(kv_id, &[(li, 0, n)], c.num_kv_heads, c.head_dim)
10013                        .and_then(|mut v| v.pop())
10014                        .unwrap_or_default()
10015                })
10016                .collect()
10017        };
10018        let host_walk = |p: &mut Self,
10019                         from: usize,
10020                         to: usize,
10021                         rows: &mut [f32],
10022                         at_split: &mut Option<Vec<Vec<f32>>>| {
10023            let mut pos = from;
10024            while pos < to {
10025                let mut end = (pos + host_chunk).min(to);
10026                if pos < split && end > split {
10027                    end = split;
10028                }
10029                let hb = p.prefill_batch(&ids[pos..end], pos);
10030                rows[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10031                pos = end;
10032                if pos == split {
10033                    *at_split = Some(host_states(p));
10034                }
10035            }
10036        };
10037        let graph_walk = |p: &mut Self,
10038                          from: usize,
10039                          to: usize,
10040                          rows: &mut [f32],
10041                          at_split: &mut Option<Vec<Vec<f32>>>|
10042         -> Result<(), String> {
10043            let mut pos = from;
10044            while pos < to {
10045                let mut end = (pos + bk).min(to);
10046                if pos < split && end > split {
10047                    end = split;
10048                }
10049                let k = end - pos;
10050                let mut hid = vec![0f32; k * hs];
10051                for (j, &id) in ids[pos..end].iter().enumerate() {
10052                    hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10053                }
10054                let positions: Vec<usize> = (pos..end).collect();
10055                let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10056                if outcome != crate::gpu::BatchGraphOutcome::Completed {
10057                    return Err(format!(
10058                        "prefill parity: batch graph {outcome:?} at position {pos}"
10059                    ));
10060                }
10061                rows[pos * hs..end * hs].copy_from_slice(&hid);
10062                pos = end;
10063                if pos == split {
10064                    *at_split = Some(graph_states(p));
10065                }
10066            }
10067            Ok(())
10068        };
10069        let score = |p: &Self, rows: &[f32], from: usize| p.parity_score(ids, rows, from);
10070
10071        let t = std::time::Instant::now();
10072        self.clear_sequence_state();
10073        let (mut hh, mut h_split) = (vec![0f32; n * hs], None);
10074        host_walk(self, 0, n, &mut hh, &mut h_split);
10075        let (h_end, h_kv) = (host_states(self), host_kv(self));
10076        let t_h = t.elapsed().as_secs_f64();
10077
10078        let t = std::time::Instant::now();
10079        self.clear_sequence_state();
10080        let (mut hg, mut g_split) = (vec![0f32; n * hs], None);
10081        let g_run = graph_walk(self, 0, n, &mut hg, &mut g_split);
10082        let (g_end, g_kv) = (graph_states(self), graph_kv(self));
10083        let t_g = t.elapsed().as_secs_f64();
10084
10085        let t = std::time::Instant::now();
10086        self.clear_sequence_state();
10087        let (mut hm, mut m_split) = (vec![0f32; n * hs], None);
10088        host_walk(self, 0, split, &mut hm, &mut m_split);
10089        let m_run = graph_walk(self, split, n, &mut hm, &mut None);
10090        let (m_end, m_kv) = (graph_states(self), graph_kv(self));
10091        let t_m = t.elapsed().as_secs_f64();
10092        self.clear_sequence_state();
10093        g_run?;
10094        m_run?;
10095
10096        let (sh, sg, sm) = (
10097            score(self, &hh, 0),
10098            score(self, &hg, 0),
10099            score(self, &hm, split),
10100        );
10101        let mut r = String::new();
10102        let _ = writeln!(
10103            r,
10104            "prefill parity: {n} tokens, split {split}, host chunk {host_chunk}, graph k {bk} \
10105             (host {t_h:.1} s, graph {t_g:.1} s, host+graph {t_m:.1} s)"
10106        );
10107        let _ = writeln!(
10108            r,
10109            "  bin          | NLL host  graph  h+g   | top1 host=graph h+g=graph | hidden rel host:graph h+g:graph"
10110        );
10111        let bin = 256usize;
10112        let mut b0 = 0;
10113        while b0 + 1 < n {
10114            let b1 = (b0 + bin).min(n - 1);
10115            let mean = |s: &[(f64, usize)], off: usize| -> Option<f64> {
10116                let v: Vec<f64> = (b0.max(off)..b1).map(|p| s[p - off].0).collect();
10117                (!v.is_empty()).then(|| v.iter().sum::<f64>() / v.len() as f64)
10118            };
10119            let agree = |a: &[(f64, usize)], ao: usize, b: &[(f64, usize)], bo: usize| {
10120                let ps: Vec<usize> = (b0.max(ao).max(bo)..b1).collect();
10121                if ps.is_empty() {
10122                    return None;
10123                }
10124                let same = ps.iter().filter(|&&p| a[p - ao].1 == b[p - bo].1).count();
10125                Some(100.0 * same as f64 / ps.len() as f64)
10126            };
10127            let hid_rel = |a: &[f32], from: usize| -> Option<f64> {
10128                (b0.max(from)..b1)
10129                    .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &hg[p * hs..(p + 1) * hs]))
10130                    .reduce(f64::max)
10131            };
10132            let f =
10133                |v: Option<f64>, prec: usize| v.map_or("-".to_string(), |x| format!("{x:.prec$}"));
10134            let e = |v: Option<f64>| v.map_or("-".to_string(), |x| format!("{x:.1e}"));
10135            let _ = writeln!(
10136                r,
10137                "  {b0:5}..{b1:<5} | {:>8} {:>6} {:>6} | {:>14} {:>9} | {:>16} {:>9}",
10138                f(mean(&sh, 0), 3),
10139                f(mean(&sg, 0), 3),
10140                f(mean(&sm, split), 3),
10141                f(agree(&sh, 0, &sg, 0), 1),
10142                f(agree(&sm, split, &sg, 0), 1),
10143                e(hid_rel(&hh, 0)),
10144                e(hid_rel(&hm, split)),
10145            );
10146            b0 = b1;
10147        }
10148        // Recurrent state, layer by layer: [conv ring | S] split at the ring.
10149        let ring_len = self
10150            .gdn_cfg
10151            .map(|c| c.conv_dim() * c.conv_kernel.saturating_sub(1))
10152            .unwrap_or(0);
10153        let state_line = |r: &mut String, label: &str, a: &[Vec<f32>], b: &[Vec<f32>]| {
10154            let _ = write!(r, "  GDN {label} (ring/S rel):");
10155            let mut first = None;
10156            for li in (0..kinds.len()).filter(|&li| kinds[li] == 1) {
10157                let (x, y) = (&a[li], &b[li]);
10158                if x.len() != y.len() || x.len() <= ring_len {
10159                    let _ = write!(r, " L{li} len {}/{}", x.len(), y.len());
10160                    continue;
10161                }
10162                let (rr, rs) = (
10163                    rel_l2(&x[..ring_len], &y[..ring_len]),
10164                    rel_l2(&x[ring_len..], &y[ring_len..]),
10165                );
10166                if first.is_none() && rr.max(rs) > 1e-2 {
10167                    first = Some(li);
10168                }
10169                let _ = write!(r, " L{li} {rr:.0e}/{rs:.0e}");
10170            }
10171            let _ = writeln!(
10172                r,
10173                "\n    first GDN layer past 1e-2: {}",
10174                first.map_or("none".to_string(), |l| l.to_string())
10175            );
10176        };
10177        match (&h_split, &g_split) {
10178            (Some(a), Some(b)) => state_line(&mut r, "host vs graph at split", a, b),
10179            _ => {
10180                let _ = writeln!(r, "  GDN at split: not captured");
10181            }
10182        }
10183        if let (Some(a), Some(b)) = (&m_split, &h_split) {
10184            state_line(&mut r, "h+g host part vs host at split", a, b);
10185        }
10186        state_line(&mut r, "host vs graph at end", &h_end, &g_end);
10187        state_line(&mut r, "h+g vs graph at end", &m_end, &g_end);
10188        // Exact-attention K/V: the worst 256-row bin per layer and the first
10189        // bin past 1e-2.
10190        let kv_line = |r: &mut String, label: &str, a: &[(Vec<f32>, Vec<f32>)], from: usize| {
10191            let _ = writeln!(r, "  K/V {label} (worst bin K/V rel, first bin past 1e-2):");
10192            for li in (0..kinds.len()).filter(|&li| kinds[li] == 2) {
10193                let ((ak, av), (gk, gv)) = (&a[li], &g_kv[li]);
10194                if ak.len() != gk.len() || ak.is_empty() {
10195                    let _ = writeln!(r, "    L{li}: rows {}/{}", ak.len(), gk.len());
10196                    continue;
10197                }
10198                let row = ak.len() / n;
10199                let (mut wk, mut wv, mut first) = (0f64, 0f64, None);
10200                let mut b0 = from;
10201                while b0 < n {
10202                    let b1 = (b0 + bin).min(n);
10203                    let (s0, s1) = (b0 * row, b1 * row);
10204                    let (rk, rv) = (
10205                        rel_l2(&ak[s0..s1], &gk[s0..s1]),
10206                        rel_l2(&av[s0..s1], &gv[s0..s1]),
10207                    );
10208                    (wk, wv) = (wk.max(rk), wv.max(rv));
10209                    if first.is_none() && rk.max(rv) > 1e-2 {
10210                        first = Some(b0);
10211                    }
10212                    b0 = b1;
10213                }
10214                let _ = writeln!(
10215                    r,
10216                    "    L{li}: {wk:.1e}/{wv:.1e} first {}",
10217                    first.map_or("none".to_string(), |p| p.to_string())
10218                );
10219            }
10220        };
10221        kv_line(&mut r, "host vs graph", &h_kv, 0);
10222        kv_line(&mut r, "h+g vs graph", &m_kv, 0);
10223        Ok(r)
10224    }
10225
10226    /// (nll, top-1 id) of every position in [from, len − 1) from post-stack
10227    /// rows (`rows` indexed by absolute position), through the scorer's
10228    /// final norm and head.
10229    fn parity_score(&self, ids: &[u32], rows: &[f32], from: usize) -> Vec<(f64, usize)> {
10230        let (n, hs) = (ids.len(), self.hidden_size);
10231        let vrows = self.weights.lm_head.rows();
10232        let vs = self.vocab_size.min(vrows);
10233        let mut out = Vec::with_capacity(n.saturating_sub(from + 1));
10234        let mut pos = from;
10235        while pos + 1 < n {
10236            let end = (pos + 32).min(n - 1);
10237            let sb = end - pos;
10238            let mut normed = vec![0f32; sb * hs];
10239            for j in 0..sb {
10240                let r = inference::rms_norm(
10241                    &rows[(pos + j) * hs..(pos + j + 1) * hs],
10242                    &self.weights.final_norm,
10243                    self.rms_eps,
10244                    self.norm_style,
10245                );
10246                normed[j * hs..(j + 1) * hs].copy_from_slice(&r);
10247            }
10248            let mut logits = vec![0f32; sb * vrows];
10249            self.weights
10250                .lm_head
10251                .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10252            for j in 0..sb {
10253                let lg = &mut logits[j * vrows..j * vrows + vs];
10254                if let Some(mu) = self.logit_multiplier {
10255                    lg.iter_mut().for_each(|v| *v *= mu);
10256                }
10257                if let Some(c) = self.final_softcap {
10258                    softcap_in_place(lg, c, self.pool.as_deref());
10259                }
10260                let (mut max, mut top) = (f32::NEG_INFINITY, 0usize);
10261                for (i, &v) in lg.iter().enumerate() {
10262                    if v > max {
10263                        (max, top) = (v, i);
10264                    }
10265                }
10266                let lse = lg
10267                    .iter()
10268                    .map(|&v| ((v - max) as f64).exp())
10269                    .sum::<f64>()
10270                    .ln()
10271                    + max as f64;
10272                out.push((lse - lg[ids[pos + j + 1] as usize] as f64, top));
10273            }
10274            pos = end;
10275        }
10276        out
10277    }
10278
10279    /// Diagnostic (`CMF_PREFILL_PARITY_O1=k` on `cortiq ppl --o1 …
10280    /// --o1-prefill P`): how much the O(1) seal depends on where its exact
10281    /// prefix ran. Every arm continues on the batch graph from the boundary
10282    /// in k-row chunks:
10283    /// - OH: prefix on the host chunked prefill; seal from the host's
10284    ///   queries and K/V; recurrent state seeded from the host.
10285    /// - OG: prefix on the batch graph (recurrent state stays on the
10286    ///   device); seal from OH's host queries and host K/V.
10287    /// - OD: prefix on the batch graph; seal from OH's queries and the
10288    ///   graph's own K/V rows (read back).
10289    ///
10290    /// OH vs OG isolates the recurrent-state hand-off, OG vs OD the seal's
10291    /// response to the host/graph K/V difference. X is the exact graph.
10292    pub fn o1_seal_parity_probe(
10293        &mut self,
10294        ids: &[u32],
10295        cfg: crate::nystrom::O1Cfg,
10296        requested: usize,
10297        bk: usize,
10298    ) -> Result<String, String> {
10299        use std::fmt::Write as _;
10300        let (n, hs) = (ids.len(), self.hidden_size);
10301        self.set_o1(Some(cfg));
10302        let split = self
10303            .o1_effective_boundary(requested)
10304            .ok_or("o1 parity: no O(1) configuration")?;
10305        if split + 1 >= n || bk == 0 || !self.can_prefill_batched() {
10306            return Err(format!(
10307                "o1 parity: needs the boundary {split} inside {n} tokens, k > 0 and a batched host prefill"
10308            ));
10309        }
10310        let o1_layers: Vec<usize> = (0..self.num_layers)
10311            .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
10312            .collect();
10313        let graph_rows = |p: &mut Self, from: usize, to: usize, rows: &mut [f32]| {
10314            let mut pos = from;
10315            while pos < to {
10316                let end = (pos + bk).min(to);
10317                let k = end - pos;
10318                let mut hid = vec![0f32; k * hs];
10319                for (j, &id) in ids[pos..end].iter().enumerate() {
10320                    hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10321                }
10322                let positions: Vec<usize> = (pos..end).collect();
10323                let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10324                if outcome != crate::gpu::BatchGraphOutcome::Completed {
10325                    return Err(format!(
10326                        "o1 parity: batch graph {outcome:?} at position {pos}"
10327                    ));
10328                }
10329                rows[pos * hs..end * hs].copy_from_slice(&hid);
10330                pos = end;
10331            }
10332            Ok(())
10333        };
10334        // Seal every O(1) layer from the given rows; `trace` = (q, k, v) per
10335        // O(1) layer, position-major.
10336        let seal_from = |p: &mut Self, trace: &[(Vec<f32>, Vec<f32>, Vec<f32>)]| {
10337            for (&li, (q, k, v)) in o1_layers.iter().zip(trace) {
10338                let c = &mut p.kv_cache.layers[li];
10339                let (row, qrow) = (k.len() / split, q.len() / split);
10340                for pos in 0..split {
10341                    c.append(
10342                        &k[pos * row..(pos + 1) * row],
10343                        &v[pos * row..(pos + 1) * row],
10344                        &[],
10345                    );
10346                    c.o1_push_q(&q[pos * qrow..(pos + 1) * qrow]);
10347                }
10348            }
10349            p.o1_seal_checked()
10350        };
10351        // Greedy continuation (`CMF_PARITY_GREEDY` tokens, default 40) from
10352        // the last prompt row, one batch-graph row per step.
10353        let steps: usize = std::env::var("CMF_PARITY_GREEDY")
10354            .ok()
10355            .and_then(|v| v.parse().ok())
10356            .unwrap_or(40);
10357        // `CMF_PARITY_DECODE=token`: the steps through `decode_step_logits`
10358        // (the token graph generation decodes on) instead of the batch graph.
10359        let token_decode = std::env::var("CMF_PARITY_DECODE").as_deref() == Ok("token");
10360        let greedy = |p: &mut Self, rows: &[f32]| -> Result<Vec<u32>, String> {
10361            let argmax = |lg: &[f32], vs: usize| {
10362                (0..vs.min(lg.len())).fold(0, |b, i| if lg[i] > lg[b] { i } else { b }) as u32
10363            };
10364            let normed = inference::rms_norm(
10365                &rows[(n - 1) * hs..n * hs],
10366                &p.weights.final_norm,
10367                p.rms_eps,
10368                p.norm_style,
10369            );
10370            let mut out = vec![argmax(&p.lm_head_forward(&normed), p.vocab_size)];
10371            for s in 1..steps {
10372                let (tok, at) = (out[s - 1], n + s - 1);
10373                let lg = if token_decode {
10374                    p.decode_step_logits(tok, at)
10375                } else {
10376                    let mut hid = p.embed_single(tok);
10377                    let outcome = p.try_batch_graph_wgpu(&mut hid, &[at], 1, None);
10378                    if outcome != crate::gpu::BatchGraphOutcome::Completed {
10379                        return Err(format!("o1 parity: greedy step {s} {outcome:?}"));
10380                    }
10381                    let normed =
10382                        inference::rms_norm(&hid, &p.weights.final_norm, p.rms_eps, p.norm_style);
10383                    p.lm_head_forward(&normed)
10384                };
10385                out.push(argmax(&lg, p.vocab_size));
10386            }
10387            Ok(out)
10388        };
10389
10390        // OW first, in a fresh process as `run` starts: the host prefix
10391        // exactly as generation runs it — boundary at `split`, the
10392        // pipeline's prefill chunk, the layers sealing after the chunk that
10393        // reaches the boundary (`CMF_O1_WALK_BOUNDARY=1`: that chunk walks
10394        // its rows one at a time, each layer sealing after the boundary row).
10395        self.clear_sequence_state();
10396        self.o1_begin_with_prefix(Some(requested));
10397        let walk_chunk = self.prefill_chunk().max(1);
10398        let walk_from = (split - 1) / walk_chunk * walk_chunk;
10399        let mut ow = vec![0f32; n * hs];
10400        let mut pos = 0;
10401        while pos < split {
10402            let end = (pos + walk_chunk).min(split);
10403            let hb = self.prefill_batch(&ids[pos..end], pos);
10404            ow[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10405            pos = end;
10406        }
10407        self.o1_seal_checked()?;
10408        let ow_run = graph_rows(self, split, n, &mut ow);
10409        let ow_text = ow_run.clone().and_then(|_| greedy(self, &ow));
10410
10411        // OH. Collect one row past the boundary's natural barrier so the
10412        // host walk does not seal inside the last chunk, copy the trace,
10413        // then seal at `split` exactly.
10414        let t = std::time::Instant::now();
10415        self.clear_sequence_state();
10416        self.o1_begin_with_prefix(Some(split + 1));
10417        let mut oh = vec![0f32; n * hs];
10418        let mut pos = 0;
10419        while pos < split {
10420            let end = (pos + 128).min(split);
10421            let hb = self.prefill_batch(&ids[pos..end], pos);
10422            oh[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10423            pos = end;
10424        }
10425        let mut host = Vec::with_capacity(o1_layers.len());
10426        for &li in &o1_layers {
10427            let c = &self.kv_cache.layers[li];
10428            let q = c
10429                .o1_q_trace()
10430                .ok_or("o1 parity: host walk sealed early")?
10431                .to_vec();
10432            let (nkv, hd) = (c.num_kv_heads, c.head_dim);
10433            let (mut k, mut v) = (vec![0f32; split * nkv * hd], vec![0f32; split * nkv * hd]);
10434            for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
10435                for p in 0..split {
10436                    let d = (p * nkv + h) * hd;
10437                    k[d..d + hd].copy_from_slice(&kh[p * hd..(p + 1) * hd]);
10438                    v[d..d + hd].copy_from_slice(&vh[p * hd..(p + 1) * hd]);
10439                }
10440            }
10441            host.push((q, k, v));
10442        }
10443        for &li in &o1_layers {
10444            self.kv_cache.layers[li].o1_set_boundary(split);
10445        }
10446        self.o1_seal_checked()?;
10447        let oh_run = graph_rows(self, split, n, &mut oh);
10448        let t_oh = t.elapsed().as_secs_f64();
10449        let oh_text = oh_run.clone().and_then(|_| greedy(self, &oh));
10450
10451        // OG: graph prefix, host seal inputs. `CMF_PARITY_PERTURB=eps`
10452        // scales every host K/V value by (1 ± eps), signs from a fixed
10453        // xorshift stream: the seal's sensitivity to rounding-size noise.
10454        self.clear_sequence_state();
10455        self.o1_begin_with_prefix(Some(requested));
10456        let mut og = vec![0f32; n * hs];
10457        graph_rows(self, 0, split, &mut og)?;
10458        let perturb: f32 = std::env::var("CMF_PARITY_PERTURB")
10459            .ok()
10460            .and_then(|v| v.parse().ok())
10461            .unwrap_or(0.0);
10462        if perturb > 0.0 {
10463            let mut seed = 0x9e37_79b9_7f4a_7c15u64;
10464            let noisy: Vec<_> = host
10465                .iter()
10466                .map(|(q, k, v)| {
10467                    let mut jitter = |x: &f32| {
10468                        seed ^= seed << 13;
10469                        seed ^= seed >> 7;
10470                        seed ^= seed << 17;
10471                        x * if seed & 1 == 0 {
10472                            1.0 + perturb
10473                        } else {
10474                            1.0 - perturb
10475                        }
10476                    };
10477                    let k: Vec<f32> = k.iter().map(&mut jitter).collect();
10478                    let v: Vec<f32> = v.iter().map(&mut jitter).collect();
10479                    (q.clone(), k, v)
10480                })
10481                .collect();
10482            seal_from(self, &noisy)?;
10483        } else {
10484            seal_from(self, &host)?;
10485        }
10486        let og_run = graph_rows(self, split, n, &mut og);
10487        let og_text = og_run.clone().and_then(|_| greedy(self, &og));
10488
10489        // OD: graph prefix, the graph's own K/V with the host queries.
10490        self.clear_sequence_state();
10491        self.o1_begin_with_prefix(Some(requested));
10492        let mut od = vec![0f32; n * hs];
10493        graph_rows(self, 0, split, &mut od)?;
10494        let mut dev = Vec::with_capacity(o1_layers.len());
10495        let mut kv_rel = Vec::with_capacity(o1_layers.len());
10496        for (&li, (q, hk, hv)) in o1_layers.iter().zip(&host) {
10497            let c = &self.kv_cache.layers[li];
10498            let (k, v) = crate::gpu::graph_kv_read_rows(
10499                self.graph_kv_id,
10500                &[(li, 0, split)],
10501                c.num_kv_heads,
10502                c.head_dim,
10503            )
10504            .and_then(|mut rows| rows.pop())
10505            .ok_or_else(|| format!("o1 parity: no graph K/V rows for layer {li}"))?;
10506            kv_rel.push((li, rel_l2(hk, &k), rel_l2(hv, &v)));
10507            dev.push((q.clone(), k, v));
10508        }
10509        seal_from(self, &dev)?;
10510        let od_run = graph_rows(self, split, n, &mut od);
10511        let od_text = od_run.clone().and_then(|_| greedy(self, &od));
10512
10513        // X: exact batch graph.
10514        self.clear_sequence_state();
10515        self.set_o1(None);
10516        let mut xx = vec![0f32; n * hs];
10517        let x_run = graph_rows(self, 0, n, &mut xx);
10518        let x_text = x_run.clone().and_then(|_| greedy(self, &xx));
10519        self.clear_sequence_state();
10520        oh_run?;
10521        ow_run?;
10522        og_run?;
10523        od_run?;
10524        x_run?;
10525
10526        let sx = self.parity_score(ids, &xx, split);
10527        let arms = [
10528            ("OH", self.parity_score(ids, &oh, split)),
10529            ("OG", self.parity_score(ids, &og, split)),
10530            ("OD", self.parity_score(ids, &od, split)),
10531            ("OW", self.parity_score(ids, &ow, split)),
10532        ];
10533        let mut r = String::new();
10534        let _ = writeln!(
10535            r,
10536            "o1 seal parity: {n} tokens, boundary {split}, graph k {bk} (OH {t_oh:.1} s), \
10537             OW chunk {walk_chunk}, boundary chunk {walk_from}..{split}"
10538        );
10539        let _ = write!(r, "  seal K/V rel host vs graph:");
10540        for (li, rk, rv) in &kv_rel {
10541            let _ = write!(r, " L{li} {rk:.0e}/{rv:.0e}");
10542        }
10543        // Post-stack prefix rows against the exact graph: the batched rows,
10544        // then the rows OW walks one at a time.
10545        let worst = |a: &[f32], p0: usize, p1: usize| {
10546            (p0..p1)
10547                .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &xx[p * hs..(p + 1) * hs]))
10548                .fold(0f64, f64::max)
10549        };
10550        let _ = writeln!(
10551            r,
10552            "\n  prefix rows rel vs exact graph, [0,{walk_from}) / [{walk_from},{split}): \
10553             OH {:.1e} / {:.1e}, OW {:.1e} / {:.1e}",
10554            worst(&oh, 0, walk_from),
10555            worst(&oh, walk_from, split),
10556            worst(&ow, 0, walk_from),
10557            worst(&ow, walk_from, split),
10558        );
10559        let _ = writeln!(
10560            r,
10561            "  bin          | NLL exact    OH     OG     OD     OW | top1=exact  OH    OG    OD    OW"
10562        );
10563        let mean = |s: &[(f64, usize)], b0: usize, b1: usize| {
10564            (b0..b1).map(|p| s[p - split].0).sum::<f64>() / (b1 - b0).max(1) as f64
10565        };
10566        let agree = |s: &[(f64, usize)], b0: usize, b1: usize| {
10567            let same = (b0..b1)
10568                .filter(|&p| s[p - split].1 == sx[p - split].1)
10569                .count();
10570            100.0 * same as f64 / (b1 - b0).max(1) as f64
10571        };
10572        let mut b0 = split;
10573        let mut total = [0f64; 5];
10574        while b0 + 1 < n {
10575            let b1 = ((b0 / 256 + 1) * 256).min(n - 1);
10576            let _ = writeln!(
10577                r,
10578                "  {b0:5}..{b1:<5} | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | {:>12.1} {:>5.1} {:>5.1} {:>5.1}",
10579                mean(&sx, b0, b1),
10580                mean(&arms[0].1, b0, b1),
10581                mean(&arms[1].1, b0, b1),
10582                mean(&arms[2].1, b0, b1),
10583                mean(&arms[3].1, b0, b1),
10584                agree(&arms[0].1, b0, b1),
10585                agree(&arms[1].1, b0, b1),
10586                agree(&arms[2].1, b0, b1),
10587                agree(&arms[3].1, b0, b1),
10588            );
10589            b0 = b1;
10590        }
10591        for (i, s) in std::iter::once(&sx)
10592            .chain(arms.iter().map(|a| &a.1))
10593            .enumerate()
10594        {
10595            total[i] = s.iter().map(|x| x.0).sum::<f64>() / s.len().max(1) as f64;
10596        }
10597        let _ = writeln!(
10598            r,
10599            "  all          | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | ppl ratio vs exact OH ×{:.3} OG ×{:.3} OD ×{:.3} OW ×{:.3}",
10600            total[0],
10601            total[1],
10602            total[2],
10603            total[3],
10604            total[4],
10605            (total[1] - total[0]).exp(),
10606            (total[2] - total[0]).exp(),
10607            (total[3] - total[0]).exp(),
10608            (total[4] - total[0]).exp(),
10609        );
10610        for (name, text) in [
10611            ("X ", x_text),
10612            ("OH", oh_text),
10613            ("OG", og_text),
10614            ("OD", od_text),
10615            ("OW", ow_text),
10616        ] {
10617            let s = text.map_or_else(|e| e, |t| self.tokenizer.decode(&t));
10618            let _ = writeln!(r, "  greedy {name}: {:?}", s);
10619        }
10620        Ok(r)
10621    }
10622
10623    fn nll_ids_inner(
10624        &mut self,
10625        ids: &[u32],
10626        start: usize,
10627        task_mask: Option<&TaskMask>,
10628    ) -> Result<(f64, usize), String> {
10629        self.nll_begin()?;
10630        // Block diffusion on Apple silicon: the Metal executor's causal path.
10631        #[cfg(target_os = "macos")]
10632        if task_mask.is_none() && self.is_block_diffusion() {
10633            if let Some(r) = self.clover_metal_nll(ids, start) {
10634                self.nll_end();
10635                return r;
10636            }
10637        }
10638        let result: Result<(f64, usize), String> = (|| {
10639            let mut nll = 0f64;
10640            let mut cnt = 0usize;
10641            // An unmasked quality run with the resident wgpu graph must score
10642            // the same stateful path used by generation.  The layer-major
10643            // GEMM prefill below is a valid CPU/GEMM oracle, but it seeds
10644            // neither the graph's device GDN state nor its device KV mirrors;
10645            // using it here would silently score a different execution.  Keep
10646            // masked scoring on the exact per-position path as before, and
10647            // let the serial arm below drive the graph-aware scorer.
10648            // Only native Metal has a fused graph lm_head contract.  Vulkan
10649            // and other graph backends may expose hidden state without the
10650            // optional logits side channel; preserve their established CPU
10651            // norm/head fallback instead of turning that valid route into a
10652            // hard missing-logits error.
10653            let (graph_quality, fused_head_quality) = nll_graph_policy(
10654                task_mask.is_none(),
10655                self.graph_prefill_preferred(),
10656                crate::gpu::q1_force(),
10657            );
10658            self.graph_head_required = fused_head_quality;
10659            self.graph_want_logits = fused_head_quality;
10660            #[cfg(target_os = "macos")]
10661            if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
10662                match self.nll_batch_metal(ids, start) {
10663                    MetalBatchNllOutcome::Completed(nll, count) => {
10664                        return Ok((nll, count));
10665                    }
10666                    MetalBatchNllOutcome::Declined => {}
10667                    MetalBatchNllOutcome::Failed(err) => return Err(err),
10668                }
10669            }
10670            // CMF_NLL_SERIAL=1 scores position by position through the
10671            // decode path (the wgpu token graph where it admits the model)
10672            // — the perplexity gate of a decode kernel, which the batched
10673            // prefill arm below never runs.
10674            let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
10675            if self.can_prefill_batched() && !graph_quality && !force_serial {
10676                // prefill-GEMM: layer-major position chunks, lm_head batched
10677                // (254MB lm_head read once per chunk, not per position).
10678                // The layer chunk is large (grouping positions by MoE experts
10679                // wins with size), lm_head in sub-blocks (logit buffer
10680                // 32×vocab ≈ 32MB instead of 128×).
10681                // CMF_NLL_CHUNK: positions per prefill step (default 128).
10682                // A device scorer pays one round trip per projection per
10683                // step, so a 512-position step quarters them.
10684                let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
10685                    .ok()
10686                    .and_then(|v| v.parse().ok())
10687                    .filter(|&c: &usize| c > 0)
10688                    .unwrap_or(128);
10689                const LM_SUB: usize = 32;
10690                let n = ids.len().saturating_sub(1);
10691                let hs = self.hidden_size;
10692                let rows = self.weights.lm_head.rows();
10693                let mut pos = 0usize;
10694                let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
10695                while pos < n {
10696                    let end = (pos + chunk_len).min(n);
10697                    let bsz = end - pos;
10698                    let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
10699                    self.nll_check_graph("batched prefill", pos)?;
10700                    if state_trace && end % 256 == 0 {
10701                        self.trace_recurrent_state(end);
10702                    }
10703                    let mut k0 = 0usize;
10704                    while k0 < bsz {
10705                        let k1 = (k0 + LM_SUB).min(bsz);
10706                        let sb = k1 - k0;
10707                        // Sub-block entirely below the scored range: the KV
10708                        // it just built is all this pass needed from it.
10709                        if pos + k1 <= start {
10710                            k0 = k1;
10711                            continue;
10712                        }
10713                        let mut normed = vec![0.0f32; sb * hs];
10714                        for k in 0..sb {
10715                            let r = inference::rms_norm(
10716                                &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
10717                                &self.weights.final_norm,
10718                                self.rms_eps,
10719                                self.norm_style,
10720                            );
10721                            normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
10722                        }
10723                        let mut logits = vec![0.0f32; sb * rows];
10724                        self.weights
10725                            .lm_head
10726                            .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10727                        for k in 0..sb {
10728                            if pos + k0 + k < start {
10729                                continue;
10730                            }
10731                            self.nll_check_graph("batched score row", pos + k0 + k)?;
10732                            let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
10733                            if let Some(mu) = self.logit_multiplier {
10734                                for v in lg.iter_mut() {
10735                                    *v *= mu;
10736                                }
10737                            }
10738                            // Gemma-class final-logit soft-capping: the
10739                            // decode paths apply it; scoring must too, or
10740                            // the uncapped softmax misprices every token.
10741                            if let Some(c) = self.final_softcap {
10742                                softcap_in_place(lg, c, self.pool.as_deref());
10743                            }
10744                            // Cortiq Embryo hierarchical head: same correction
10745                            // the decode path applies (lm_head_forward).
10746                            if let Some(cm) = self.head_clusters.clone() {
10747                                self.hierarchical_head_logprobs(
10748                                    &normed[k * hs..(k + 1) * hs],
10749                                    &cm,
10750                                    lg,
10751                                );
10752                            }
10753                            let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
10754                            let target = ids[pos + k0 + k + 1] as usize;
10755                            let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10756                            let lse: f64 = lg
10757                                .iter()
10758                                .map(|&v| ((v - max) as f64).exp())
10759                                .sum::<f64>()
10760                                .ln()
10761                                + max as f64;
10762                            nll += lse - lg[target] as f64;
10763                            cnt += 1;
10764                            if std::env::var("CMF_PPL_TRACE").is_ok() {
10765                                let top = lg
10766                                    .iter()
10767                                    .enumerate()
10768                                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10769                                    .map(|(i, _)| i)
10770                                    .unwrap_or(0);
10771                                eprintln!(
10772                                    "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
10773                                    pos + k0 + k,
10774                                    target,
10775                                    lse - lg[target] as f64,
10776                                    top,
10777                                    lg[target],
10778                                    lg[top]
10779                                );
10780                            }
10781                        }
10782                        k0 = k1;
10783                    }
10784                    pos = end;
10785                }
10786                return Ok((nll, cnt));
10787            }
10788            for pos in 0..ids.len().saturating_sub(1) {
10789                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
10790                self.nll_check_graph("serial forward", pos)?;
10791                // Architectures whose head lives inside their own stack return
10792                // the logits out of band and a zero hidden — DeepSeek-V4 folds
10793                // its hyper-connection copies between the last layer and the
10794                // norm, so it cannot hand back a vector this loop could use.
10795                // Scoring the zeros gave a perplexity of exactly the vocabulary
10796                // size, which is a uniform distribution reported as a
10797                // measurement. `generate` already reads this channel.
10798                let out_of_band = self.graph_logits.take();
10799                if self.graph_head_required && out_of_band.is_none() {
10800                    METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
10801                    return Err(format!(
10802                        "fused Metal graph head did not complete at NLL position {pos}"
10803                    ));
10804                }
10805                if pos < start {
10806                    continue;
10807                }
10808                let logits = match out_of_band {
10809                    Some(lg) => lg,
10810                    None => {
10811                        let normed = inference::rms_norm(
10812                            &hidden,
10813                            &self.weights.final_norm,
10814                            self.rms_eps,
10815                            self.norm_style,
10816                        );
10817                        // lm_head_forward applies the final-logit softcap itself
10818                        // — capping again here double-squashed gemma-class
10819                        // logits (tanh∘tanh) and reported a flattered ppl.
10820                        self.lm_head_forward(&normed)
10821                    }
10822                };
10823                let target = ids[pos + 1] as usize;
10824                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10825                let lse: f64 = logits
10826                    .iter()
10827                    .map(|&v| ((v - max) as f64).exp())
10828                    .sum::<f64>()
10829                    .ln()
10830                    + max as f64;
10831                let tok_nll = lse - logits[target] as f64;
10832                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10833                    let top = logits
10834                        .iter()
10835                        .enumerate()
10836                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10837                        .map(|(i, _)| i)
10838                        .unwrap_or(0);
10839                    eprintln!(
10840                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10841                        logits[target], logits[top]
10842                    );
10843                }
10844                nll += tok_nll;
10845                cnt += 1;
10846            }
10847            Ok((nll, cnt))
10848        })();
10849        self.nll_end();
10850        result
10851    }
10852
10853    /// Score one post-layer hidden with the same final norm/head path used by
10854    /// decode. Keeping this in one helper is important for the production
10855    /// batch scorer: its rows stop before the final norm, just like the
10856    /// per-position O(1) path below.
10857    fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
10858        let normed = inference::rms_norm(
10859            hidden,
10860            &self.weights.final_norm,
10861            self.rms_eps,
10862            self.norm_style,
10863        );
10864        // lm_head_forward applies the final-logit softcap itself — capping
10865        // again here double-squashed gemma-class logits in earlier scorers.
10866        let mut logits = self.lm_head_forward(&normed);
10867        let target = target as usize;
10868        let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10869        let lse: f64 = logits
10870            .iter()
10871            .map(|&v| ((v - max) as f64).exp())
10872            .sum::<f64>()
10873            .ln()
10874            + max as f64;
10875        let tok_nll = lse - logits[target] as f64;
10876        if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10877            let top = logits
10878                .iter()
10879                .enumerate()
10880                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10881                .map(|(i, _)| i)
10882                .unwrap_or(0);
10883            eprintln!(
10884                "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10885                logits[target], logits[top]
10886            );
10887        }
10888        attention::recycle_buf(&mut logits);
10889        tok_nll
10890    }
10891
10892    /// `CMF_STATE_TRACE`: per-layer magnitude of the recurrent record at
10893    /// position `pos` — the whole `linear_state` (vmf: S then the conv
10894    /// ring; GDN: conv ring then S), its recurrent S part alone, the
10895    /// bounded ring, and the last per-position KV row count.  The tool
10896    /// that separated the ~4k perplexity cliff of the 500-step exports
10897    /// (a state that keeps climbing past the trained window) from a
10898    /// runtime boundary; one line per layer, `STATE pos=… layer=…`.
10899    fn trace_recurrent_state(&self, pos: usize) {
10900        let stats = |v: &[f32]| -> (f64, f64) {
10901            if v.is_empty() {
10902                return (0.0, 0.0);
10903            }
10904            let (mut ss, mut mx) = (0f64, 0f64);
10905            for &x in v {
10906                ss += (x as f64) * (x as f64);
10907                mx = mx.max((x as f64).abs());
10908            }
10909            ((ss / v.len() as f64).sqrt(), mx)
10910        };
10911        for (li, l) in self.kv_cache.layers.iter().enumerate() {
10912            let lw = &self.weights.layers[self.phys_layer(li)];
10913            let (kind, s_len) = match &lw.attn {
10914                AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
10915                AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
10916                AttnKind::Bounded(_) => ("bounded", 0),
10917                AttnKind::Full { .. } => ("full", 0),
10918                _ => ("other", 0),
10919            };
10920            let (rms, max) = stats(&l.linear_state);
10921            let s_part = if kind == "vmf" {
10922                &l.linear_state[..s_len.min(l.linear_state.len())]
10923            } else if kind == "gdn" {
10924                let ring = l
10925                    .linear_state
10926                    .len()
10927                    .saturating_sub(s_len)
10928                    .min(l.linear_state.len());
10929                &l.linear_state[ring..]
10930            } else {
10931                &l.linear_state[..0]
10932            };
10933            let (s_rms, s_max) = stats(s_part);
10934            let (ring_rms, ring_len) = match &l.bounded {
10935                Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
10936                None => (0.0, 0),
10937            };
10938            eprintln!(
10939                "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
10940                 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
10941                l.linear_state.len(),
10942                l.seq_len
10943            );
10944        }
10945    }
10946
10947    /// Teacher-forced NLL of the CONVERTED model: the O(1) Nyström path
10948    /// is ACTIVE over the scored positions. Returns `Ok((nll sum, scored
10949    /// count))` over `prefill..len-1` and surfaces a post-mutation batch
10950    /// failure instead of returning a partial score.
10951    ///
10952    /// Runtime discipline, deliberately NOT the matrix probe's: the
10953    /// requested prefix plus any required deferred lead-in run the exact
10954    /// prompt pass — that pass is what freezes the landmarks and M — and
10955    /// every post-seal scored position goes through `NystromState::step()`,
10956    /// the same code decode runs.
10957    /// So the landmarks are PREFILL-frozen (what ships), not
10958    /// full-sequence oracles (what the published probe measured). When the
10959    /// requested prefix is shorter than the bounded transition, rows in the
10960    /// exact lead-in are still scored so the shifted target range is stable.
10961    ///
10962    /// Pair with `nll_ids_from(ids, prefill)` for the exact baseline
10963    /// over the identical token set — that ratio is the honest one.
10964    pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
10965        // This scorer consumes host hiddens, so never request the optional
10966        // token-graph lm_head side channel. `nll_begin` also consumes a
10967        // prior graph failure and clears only the cancel bit that failure
10968        // raised, leaving a caller-owned cancellation observable.
10969        self.nll_begin()?;
10970        let requested_prefix = (prefill > 0).then_some(prefill);
10971        self.o1_begin_with_prefix(requested_prefix);
10972        let n = ids.len().saturating_sub(1);
10973        let requested_start = prefill.min(n);
10974        // The exact prefix must reach the deferred boundary before a
10975        // collecting layer can convert. Rows between the requested start and
10976        // that boundary remain part of the public NLL range and are scored
10977        // from the same hidden pass below.
10978        let exact_end = if self.o1_active() {
10979            match requested_prefix {
10980                Some(requested) => self.o1_effective_boundary(requested),
10981                None => self
10982                    .o1_cfg
10983                    .as_ref()
10984                    .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
10985            }
10986            .unwrap_or(requested_start)
10987            .min(n)
10988        } else {
10989            requested_start
10990        };
10991        let mut nll = 0f64;
10992        let mut cnt = 0usize;
10993
10994        // Exact prompt pass over ids[..exact_end]: the seal consumes its
10995        // q/k/v. Rows at or after requested_start are scored here when the
10996        // bounded lead-in is longer than the caller's requested prefix.
10997        let mut pos = 0usize;
10998        if self.can_prefill_batched() {
10999            const CHUNK: usize = 128;
11000            while pos < exact_end {
11001                let end = (pos + CHUNK).min(exact_end);
11002                let hiddens = self.prefill_batch(&ids[pos..end], pos);
11003                if self
11004                    .graph_failed
11005                    .swap(false, std::sync::atomic::Ordering::Relaxed)
11006                {
11007                    self.cancel
11008                        .store(false, std::sync::atomic::Ordering::Relaxed);
11009                    self.nll_end();
11010                    return Err("GPU graph failed during O(1) NLL prefix".into());
11011                }
11012                for row in 0..end - pos {
11013                    let score_pos = pos + row;
11014                    if score_pos >= requested_start && score_pos < n {
11015                        nll += self.nll_from_hidden(
11016                            &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
11017                            ids[score_pos + 1],
11018                            score_pos,
11019                        );
11020                        cnt += 1;
11021                    }
11022                }
11023                pos = end;
11024            }
11025        } else {
11026            while pos < exact_end {
11027                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11028                if self
11029                    .graph_failed
11030                    .swap(false, std::sync::atomic::Ordering::Relaxed)
11031                {
11032                    self.cancel
11033                        .store(false, std::sync::atomic::Ordering::Relaxed);
11034                    self.nll_end();
11035                    return Err("GPU graph failed during O(1) NLL prefix".into());
11036                }
11037                if pos >= requested_start && pos < n {
11038                    nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11039                    cnt += 1;
11040                }
11041                pos += 1;
11042            }
11043        }
11044        self.o1_seal_checked().map_err(|err| {
11045            self.nll_end();
11046            err
11047        })?;
11048
11049        // Reuse the production whole-token batch graph for the post-seal
11050        // suffix when the caller explicitly enabled both routes. This is a
11051        // teacher-forced scorer, so every row is ids[pos] and its target is
11052        // ids[pos + 1]; no speculative tail or rollback state is involved.
11053        // A first Declined is safe to handle with the established serial O(1)
11054        // path. Once a chunk completes, however, the device recurrent state
11055        // owns the sequence and a later decline must be terminal rather than
11056        // falling back to stale CPU state.
11057        let batch_k = std::env::var("CMF_BATCH_K")
11058            .ok()
11059            .and_then(|v| v.parse::<usize>().ok())
11060            .unwrap_or(0);
11061        let batch_admitted = batch_k > 0
11062            && self.can_prefill_batched()
11063            && self.o1_active()
11064            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
11065            && (0..self.num_layers).all(|li| {
11066                let cache = &self.kv_cache.layers[self.phys_layer(li)];
11067                cache.o1.is_none() || cache.o1_views().is_some()
11068            });
11069        if std::env::var("CMF_GRAPH_PROF").is_ok() {
11070            eprintln!(
11071                "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
11072                batch_admitted,
11073                batch_k,
11074                n.saturating_sub(exact_end),
11075            );
11076        }
11077        let mut batch_completed = false;
11078        if batch_admitted && exact_end < n {
11079            let hs = self.hidden_size;
11080            let mut batch_pos = exact_end;
11081            while batch_pos < n {
11082                let end = (batch_pos + batch_k).min(n);
11083                let bk = end - batch_pos;
11084                let mut hiddens = vec![0.0f32; bk * hs];
11085                for (row, &id) in ids[batch_pos..end].iter().enumerate() {
11086                    hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
11087                }
11088                let positions: Vec<usize> = (batch_pos..end).collect();
11089                let t_batch = std::time::Instant::now();
11090                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
11091                if std::env::var("CMF_GRAPH_PROF").is_ok() {
11092                    let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
11093                    eprintln!(
11094                        "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
11095                        batch_pos,
11096                        end.saturating_sub(1),
11097                        bk as f64 / (ms / 1000.0),
11098                    );
11099                }
11100                if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
11101                    self.nll_end();
11102                    return Err(err);
11103                }
11104                match outcome {
11105                    crate::gpu::BatchGraphOutcome::Completed => {
11106                        batch_completed = true;
11107                        for row in 0..bk {
11108                            nll += self.nll_from_hidden(
11109                                &hiddens[row * hs..(row + 1) * hs],
11110                                ids[batch_pos + row + 1],
11111                                batch_pos + row,
11112                            );
11113                            cnt += 1;
11114                        }
11115                        batch_pos = end;
11116                    }
11117                    crate::gpu::BatchGraphOutcome::Declined => {
11118                        if batch_completed {
11119                            self.nll_end();
11120                            return Err(format!(
11121                                "O(1) NLL batch declined after completed chunk at position {batch_pos}"
11122                            ));
11123                        }
11124                        break;
11125                    }
11126                    crate::gpu::BatchGraphOutcome::Failed => {
11127                        self.nll_end();
11128                        return Err(format!(
11129                            "O(1) NLL batch graph failed after admission at position {batch_pos}"
11130                        ));
11131                    }
11132                }
11133            }
11134            if batch_completed && cnt == n.saturating_sub(requested_start) {
11135                self.nll_end();
11136                return Ok((nll, cnt));
11137            }
11138        }
11139
11140        // Serial O(1) fallback/reference. It is intentionally retained when
11141        // batch admission declines before mutation; callers must label this
11142        // CMF_BATCH_K=0/per-position path separately from the production
11143        // whole-token batch route.
11144        for pos in exact_end..n {
11145            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11146            if self
11147                .graph_failed
11148                .swap(false, std::sync::atomic::Ordering::Relaxed)
11149            {
11150                self.cancel
11151                    .store(false, std::sync::atomic::Ordering::Relaxed);
11152                self.nll_end();
11153                return Err(format!(
11154                    "GPU graph failed during O(1) NLL serial scoring at position {pos}"
11155                ));
11156            }
11157            nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11158            cnt += 1;
11159        }
11160        self.nll_end();
11161        Ok((nll, cnt))
11162    }
11163
11164    /// Teacher-forced calibration data (B1): for each position, whether the
11165    /// argmax equals the actual next token, and the top-1 softmax prob
11166    /// (top-1 probability) under EACH temperature in `temps` — all from ONE forward
11167    /// pass (argmax/correctness are temperature-invariant; only p_max
11168    /// reshapes). Feeds `cortiq calibrate` (reliability/ECE + temperature
11169    /// fit): is the model's confidence a true property, or does it need a
11170    /// measured scaling?
11171    pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
11172        self.clear_sequence_state();
11173        let n = ids.len().saturating_sub(1);
11174        let mut correct = Vec::with_capacity(n);
11175        let mut pmax = Vec::with_capacity(n);
11176        for pos in 0..n {
11177            let emb = self.embed_single(ids[pos]);
11178            let hidden = self.forward_layers(&emb, pos, None);
11179            let logits = if let Some(logits) = self.graph_logits.take() {
11180                logits
11181            } else {
11182                let normed = inference::rms_norm(
11183                    &hidden,
11184                    &self.weights.final_norm,
11185                    self.rms_eps,
11186                    self.norm_style,
11187                );
11188                // lm_head_forward applies the final-logit softcap itself —
11189                // capping again here double-squashed gemma-class logits
11190                // (tanh∘tanh) and reported a flattered ppl.
11191                self.lm_head_forward(&normed)
11192            };
11193            let target = ids[pos + 1] as usize;
11194            let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
11195            for (i, &v) in logits.iter().enumerate() {
11196                if v > mval {
11197                    mval = v;
11198                    amax = i;
11199                }
11200            }
11201            correct.push(amax == target);
11202            let row: Vec<f32> = temps
11203                .iter()
11204                .map(|&t| {
11205                    let tt = t.max(1e-3);
11206                    let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
11207                    1.0 / s.max(1e-12) // numerator at the max is exp(0)=1
11208                })
11209                .collect();
11210            pmax.push(row);
11211        }
11212        self.clear_sequence_state();
11213        (correct, pmax)
11214    }
11215
11216    /// Teacher-forced PPL with the dynamic router driving per-window
11217    /// skill switches (VMF experiment №2 measurement). Sequential (φ
11218    /// must update per token), returns (ppl, switch_count). The router
11219    /// must be enabled (`enable_dynamic_routing`); else this equals
11220    /// plain `ppl_ids`. The active skill when scoring token t shapes the
11221    /// logits for t+1 — on-policy over the held-out text itself.
11222    pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
11223        if self.dyn_router.is_none() {
11224            return Ok((self.ppl_ids(ids)?, 0));
11225        }
11226        self.nll_begin()?;
11227        let saved_active = self.dyn_active;
11228        let mut router = self
11229            .dyn_router
11230            .take()
11231            .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
11232        router.reset();
11233        self.dyn_phi_seen = 0;
11234        let _ = self.set_active_skill(None);
11235
11236        let result: Result<(f64, usize), String> = (|| {
11237            let mut nll = 0f64;
11238            let mut cnt = 0usize;
11239            for pos in 0..ids.len().saturating_sub(1) {
11240                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11241                self.nll_check_graph("dynamic serial forward", pos)?;
11242                let out_of_band = self.graph_logits.take();
11243                let mut logits = match out_of_band {
11244                    Some(lg) => lg,
11245                    None => {
11246                        let normed = inference::rms_norm(
11247                            &hidden,
11248                            &self.weights.final_norm,
11249                            self.rms_eps,
11250                            self.norm_style,
11251                        );
11252                        // lm_head_forward applies the final-logit softcap itself —
11253                        // capping again here double-squashed gemma-class logits
11254                        // and reported a flattered ppl.
11255                        self.lm_head_forward(&normed)
11256                    }
11257                };
11258                let target = ids[pos + 1] as usize;
11259                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11260                let lse: f64 = logits
11261                    .iter()
11262                    .map(|&v| ((v - max) as f64).exp())
11263                    .sum::<f64>()
11264                    .ln()
11265                    + max as f64;
11266                let tok_nll = lse - logits[target] as f64;
11267                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11268                    let top = logits
11269                        .iter()
11270                        .enumerate()
11271                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11272                        .map(|(i, _)| i)
11273                        .unwrap_or(0);
11274                    eprintln!(
11275                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11276                        logits[target], logits[top]
11277                    );
11278                }
11279                nll += tok_nll;
11280                cnt += 1;
11281                attention::recycle_buf(&mut logits);
11282                // Route on the evolving phi (drives the NEXT token's skill).
11283                let phi = self.dyn_phi_ema.clone();
11284                if let Some(new_active) = router.step(&phi, pos) {
11285                    let _ = self.set_active_skill(new_active);
11286                }
11287            }
11288            Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
11289        })();
11290
11291        // Restore the detached router and the active overlay on both success
11292        // and failure. The scoring state is cleared independently below.
11293        let _ = self.set_active_skill(saved_active);
11294        self.dyn_router = Some(router);
11295        self.nll_end();
11296        result
11297    }
11298
11299    /// Routing probe φ (spec §9): mean-pooled hidden after `layer`.
11300    pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
11301        self.clear_sequence_state();
11302        let mut acc = vec![0f32; self.hidden_size];
11303        for (pos, &id) in ids.iter().enumerate() {
11304            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11305            for (a, v) in acc.iter_mut().zip(&h) {
11306                *a += v;
11307            }
11308        }
11309        let n = ids.len().max(1) as f32;
11310        for a in acc.iter_mut() {
11311            *a /= n;
11312        }
11313        self.clear_sequence_state();
11314        acc
11315    }
11316
11317    /// Router-v2 φ probe (spec §9.4, `phi.pool = "span_mean"`): the hidden
11318    /// AFTER `layer` — the same per-position walk and the same quantity as
11319    /// [`Self::probe_phi`] — averaged over the positions in `span` only
11320    /// (the user text between the template's prefix and suffix ids), NOT
11321    /// unit-normalized (the decision normalizes). The walk stops at
11322    /// `span.end`: causality makes the later positions irrelevant, so the
11323    /// result is bit-identical to probing `ids[..span.end]`. An empty span
11324    /// gives the zero vector, which the decision treats as degenerate.
11325    ///
11326    /// Every sequence state is reset before and after — the host KV/ring/
11327    /// recurrent state, the reuse keys (`kv_history`, `kv_prefix`) and the
11328    /// device graph's sequence — so run it on a pipeline that does not
11329    /// also serve a conversation (its prefix reuse would be lost).
11330    pub fn probe_phi_span(
11331        &mut self,
11332        ids: &[u32],
11333        layer: usize,
11334        span: std::ops::Range<usize>,
11335    ) -> Vec<f32> {
11336        #[cfg(target_os = "macos")]
11337        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11338        let end = span.end.min(ids.len());
11339        let start = span.start.min(end);
11340        let reset = |p: &mut Self| p.clear_sequence_state();
11341        reset(self);
11342        let mut acc = vec![0f32; self.hidden_size];
11343        for (pos, &id) in ids[..end].iter().enumerate() {
11344            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11345            if pos >= start {
11346                for (a, v) in acc.iter_mut().zip(&h) {
11347                    *a += v;
11348                }
11349            }
11350        }
11351        let n = end - start;
11352        if n > 0 {
11353            let n = n as f32;
11354            for a in acc.iter_mut() {
11355                *a /= n;
11356            }
11357        }
11358        reset(self);
11359        acc
11360    }
11361
11362    /// One decode step of the current sequence: forward `token` at
11363    /// `position` (the cache holds positions `[0, position)`, e.g. after
11364    /// [`Self::forward_ids`]) and return the next-token logits — the same
11365    /// forward and head the generation loop runs (resident-graph logits
11366    /// when the graph ran, final norm + lm_head otherwise). The logit-dump
11367    /// tools drive greedy decoding with it so every position is observable.
11368    pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
11369        #[cfg(target_os = "macos")]
11370        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11371        self.graph_logits = None;
11372        let hidden = self.forward_layers(&self.embed_single(token), position, None);
11373        if let Some(logits) = self.graph_logits.take() {
11374            return logits;
11375        }
11376        inference::rms_norm_into(
11377            &hidden,
11378            &self.weights.final_norm,
11379            self.rms_eps,
11380            self.norm_style,
11381            &mut self.ws.n1,
11382        );
11383        self.lm_head_forward(&self.ws.n1)
11384    }
11385
11386    /// Layer-major batched prefill (prefill-GEMM): full-attention —
11387    /// per-position with the existing operators (KV grows naturally,
11388    /// causality preserved), GDN projections / FFN / MoE — batched
11389    /// (a weight row is read from DRAM once per chunk, not per
11390    /// position). Returns the hidden of all positions [b × hidden].
11391    fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
11392        self.prefill_batch_masked(ids, start_pos, None)
11393    }
11394
11395    /// `prefill_batch` with a task mask honored on the dense-FFN panels
11396    /// (the masked-inference fast path: full fused compute, mask lands on
11397    /// the activations). The whole-chunk GPU graph is skipped for masked
11398    /// layers by the callers' arms; the per-GEMM device paths stay in
11399    /// play because the zeroing happens on the host between them.
11400    fn prefill_batch_masked(
11401        &mut self,
11402        ids: &[u32],
11403        start_pos: usize,
11404        task_mask: Option<&TaskMask>,
11405    ) -> Vec<f32> {
11406        self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
11407    }
11408
11409    /// One prompt chunk through the whole stack, post-stack rows out (no
11410    /// final norm) — the ingest generation uses, shared by scoring and
11411    /// `forward_ids` so they measure the same execution: the batched wgpu
11412    /// graph's device prefix plus the host's batched walk for the rest when
11413    /// `batch_prefix_prefill` holds and the graph admits the chunk, else
11414    /// the host's chunked prefill. Err only when a graph that had mutated
11415    /// device state failed.
11416    fn prefill_rows(
11417        &mut self,
11418        ids: &[u32],
11419        pos: usize,
11420        task_mask: Option<&TaskMask>,
11421    ) -> Result<Vec<f32>, String> {
11422        self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
11423    }
11424
11425    fn prefill_input_rows(
11426        &mut self,
11427        input: PrefillIn<'_>,
11428        pos: usize,
11429        task_mask: Option<&TaskMask>,
11430    ) -> Result<Vec<f32>, String> {
11431        self.mimo_moe_prepare();
11432        let hs = self.hidden_size;
11433        let bk = match input {
11434            PrefillIn::Ids(ids) => ids.len(),
11435            PrefillIn::Hidden(rows) => rows.len() / hs,
11436        };
11437        #[cfg(not(target_os = "macos"))]
11438        if task_mask.is_none()
11439            && !self.o1_active()
11440            && bk > 1
11441            && (self.batch_prefix_prefill()
11442                || (self.verify_exact_moe
11443                    && crate::gpu::enabled_here()
11444                    && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
11445        {
11446            let mut hiddens = match input {
11447                PrefillIn::Hidden(rows) => rows.to_vec(),
11448                PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
11449            };
11450            let positions: Vec<usize> = (pos..pos + bk).collect();
11451            let mut run = 0usize;
11452            match self.try_batch_graph_wgpu_prefix(
11453                &mut hiddens,
11454                &positions,
11455                bk,
11456                None,
11457                Some(&mut run),
11458            ) {
11459                crate::gpu::BatchGraphOutcome::Completed => {
11460                    let out = if run < self.num_layers {
11461                        self.prefill_batch_span(
11462                            PrefillIn::Hidden(&hiddens),
11463                            pos,
11464                            None,
11465                            run,
11466                            self.num_layers,
11467                        )
11468                    } else {
11469                        hiddens
11470                    };
11471                    return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11472                        Err("MiMo attention graph failed after admission".into())
11473                    } else {
11474                        Ok(out)
11475                    };
11476                }
11477                crate::gpu::BatchGraphOutcome::Failed => {
11478                    return Err("batched prefix prefill failed after admission".into());
11479                }
11480                crate::gpu::BatchGraphOutcome::Declined => {
11481                    // Rows an earlier chunk left on the device only.
11482                    #[cfg(feature = "gpu")]
11483                    self.pull_lagging_host_kv(0, self.num_layers, pos);
11484                }
11485            }
11486        }
11487        let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
11488        if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11489            Err("batch tail graph failed after admission".into())
11490        } else {
11491            Ok(out)
11492        }
11493    }
11494
11495    /// The layer-major batched walk over a layer span [from..upto_excl):
11496    /// the whole prefill machinery (chunk graph, batched attends, GEMM
11497    /// panels) for a PARTIAL stack — the network split's prefill rides
11498    /// the same canon as the local one. Input is token ids (embeds
11499    /// itself, coordinator side) or ready boundary hiddens (worker side).
11500    fn prefill_batch_span(
11501        &mut self,
11502        input: PrefillIn<'_>,
11503        start_pos: usize,
11504        task_mask: Option<&TaskMask>,
11505        from: usize,
11506        upto_excl: usize,
11507    ) -> Vec<f32> {
11508        let hs = self.hidden_size;
11509        let b = match input {
11510            PrefillIn::Ids(ids) => ids.len(),
11511            PrefillIn::Hidden(hb) => hb.len() / hs,
11512        };
11513        let upto_excl = upto_excl.min(self.num_layers);
11514        // The CPU embed is deferred: when the chunk graph takes the run
11515        // from layer 0 it gathers the embeddings on the device instead.
11516        // A hidden input is ready by definition.
11517        let mut h: Vec<f32>;
11518        let mut h_ready;
11519        match input {
11520            PrefillIn::Ids(_) => {
11521                h = vec![0.0; b * hs];
11522                h_ready = false;
11523            }
11524            PrefillIn::Hidden(hb) => {
11525                h = hb.to_vec();
11526                h_ready = true;
11527            }
11528        }
11529        let fill_h = |h: &mut Vec<f32>, me: &Self| {
11530            if let PrefillIn::Ids(ids) = input {
11531                for (bi, &id) in ids.iter().enumerate() {
11532                    let e = me.embed_single(id);
11533                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
11534                }
11535                if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11536                    if let Ok(t) = tp.parse::<usize>() {
11537                        if t >= start_pos && t < start_pos + ids.len() {
11538                            let bi = t - start_pos;
11539                            let row = &h[bi * hs..(bi + 1) * hs];
11540                            let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11541                            eprintln!(
11542                                "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
11543                                ids[bi],
11544                                row[0],
11545                                row[1],
11546                                ids.len(),
11547                                &ids[..ids.len().min(8)]
11548                            );
11549                        }
11550                    }
11551                }
11552            }
11553        };
11554        let (_nkv, _hd, _rd, eps) = (
11555            self.num_kv_heads,
11556            self.head_dim,
11557            self.rotary_dim,
11558            self.rms_eps,
11559        );
11560        let pool = self.pool.clone();
11561        let norm_style = self.norm_style;
11562        self.mimo_moe_prepare();
11563        let automatic_gpu_prefix = self.automatic_gpu_prefix();
11564
11565        #[cfg(target_os = "macos")]
11566        let mut chunk_skip_until = 0usize;
11567        for li in from..upto_excl {
11568            let _capacity_tail = automatic_gpu_prefix
11569                .filter(|&prefix| {
11570                    li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
11571                })
11572                .map(|_| crate::gpu::enter_cpu_scope());
11573            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU
11574            // Spark-X2.5: the layer's int8 matrix-unit GEMMs take the
11575            // `zi_mm` tile, bit-identical to the 64x64 kernel and about
11576            // three times its rate (`gpu_wgpu::zi_gemm_route`).
11577            #[cfg(feature = "gpu")]
11578            let _fast_gemm = self
11579                .proj_gate_sigmoid
11580                .then(crate::gpu::enter_prefill_fast_gemm);
11581            // GPU chunk graph (default-on under CMF_GPU=1): a run of
11582            // consecutive eligible layers for the whole chunk in ONE
11583            // Metal submission — norm, QKV, RoPE with fused mirror
11584            // append, causal attend, O, FFN, hidden device-resident
11585            // across the run. Any refusal falls through to the CPU path.
11586            #[cfg(target_os = "macos")]
11587            if task_mask.is_none() {
11588                if li < chunk_skip_until {
11589                    continue;
11590                }
11591                // Device-side embedding needs a q8_row embedding matrix;
11592                // with any other layout the CPU fills `h` first and the
11593                // graph starts from a ready hidden (refusing the whole
11594                // run over the embedding alone kept q4t models — the
11595                // whole Nanbeige/Bonsai class — on the CPU prefill).
11596                if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
11597                    fill_h(&mut h, self);
11598                    h_ready = true;
11599                }
11600                let ids_for_embed = match input {
11601                    PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
11602                    PrefillIn::Hidden(_) => None,
11603                };
11604                let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
11605                if end > li {
11606                    h_ready = true;
11607                    chunk_skip_until = end;
11608                    // Looped Transformer: the graph stopped at a loop
11609                    // boundary — apply final norm before the next iteration.
11610                    if self.is_loop_end(end - 1) && end < self.num_layers {
11611                        for bi in 0..b {
11612                            let normed = inference::rms_norm(
11613                                &h[bi * hs..(bi + 1) * hs],
11614                                &self.weights.final_norm,
11615                                eps,
11616                                norm_style,
11617                            );
11618                            h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11619                        }
11620                    }
11621                    continue;
11622                }
11623            }
11624            if !h_ready {
11625                fill_h(&mut h, self);
11626                h_ready = true;
11627            }
11628            if task_mask.is_none() && self.verify_exact_moe {
11629                let positions: Vec<_> = (start_pos..start_pos + b).collect();
11630                match self.mimo_graph_layer_rows(li, &mut h, &positions) {
11631                    crate::gpu::BatchGraphOutcome::Completed => continue,
11632                    crate::gpu::BatchGraphOutcome::Failed => return h,
11633                    crate::gpu::BatchGraphOutcome::Declined => {}
11634                }
11635            }
11636            #[cfg(feature = "gpu")]
11637            self.pull_lagging_host_kv(li, li + 1, start_pos);
11638            let lw = &self.weights.layers[self.phys_layer(li)];
11639            let t_attn = std::time::Instant::now();
11640            // ── attention ──
11641            match &lw.attn {
11642                AttnKind::Kda(w) => {
11643                    // Projections batched, recurrence sequential.
11644                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
11645                    let mut normed = vec![0.0f32; b * hs];
11646                    for bi in 0..b {
11647                        inference::rms_norm_into(
11648                            &h[bi * hs..(bi + 1) * hs],
11649                            &lw.input_norm,
11650                            eps,
11651                            norm_style,
11652                            &mut normed[bi * hs..(bi + 1) * hs],
11653                        );
11654                    }
11655                    let attn = crate::linear_core::kda_forward_batch(
11656                        &normed,
11657                        b,
11658                        w,
11659                        &cfg,
11660                        &mut self.kv_cache.layers[li].linear_state,
11661                        pool.as_deref(),
11662                    );
11663                    for (dst, &a) in h.iter_mut().zip(&attn) {
11664                        *dst += a;
11665                    }
11666                }
11667                AttnKind::LinearGdn(w) => {
11668                    // Projections batched, recurrence sequential.
11669                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
11670                    let mut normed = vec![0.0f32; b * hs];
11671                    for bi in 0..b {
11672                        let r = inference::rms_norm(
11673                            &h[bi * hs..(bi + 1) * hs],
11674                            &lw.input_norm,
11675                            eps,
11676                            norm_style,
11677                        );
11678                        normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11679                    }
11680                    let attn = crate::linear_core::gdn_forward_batch(
11681                        &normed,
11682                        b,
11683                        w,
11684                        &cfg,
11685                        &mut self.kv_cache.layers[li].linear_state,
11686                        pool.as_deref(),
11687                    );
11688                    for (dst, &a) in h.iter_mut().zip(&attn) {
11689                        *dst += a;
11690                    }
11691                }
11692                AttnKind::ShortConv(w) => {
11693                    // Projections batched over the chunk; the conv walks the
11694                    // contiguous positions in order (same ring as decode).
11695                    let cfg = self
11696                        .short_conv_cfg
11697                        .expect("short-conv layer without short_conv_cfg");
11698                    let mut normed = vec![0.0f32; b * hs];
11699                    for bi in 0..b {
11700                        inference::rms_norm_into(
11701                            &h[bi * hs..(bi + 1) * hs],
11702                            &lw.input_norm,
11703                            eps,
11704                            norm_style,
11705                            &mut normed[bi * hs..(bi + 1) * hs],
11706                        );
11707                    }
11708                    let mut attn = short_conv_forward_batch(
11709                        &normed,
11710                        b,
11711                        w,
11712                        &cfg,
11713                        &mut self.kv_cache.layers[li].linear_state,
11714                        pool.as_deref(),
11715                    );
11716                    // LFM3 sandwich: the mixer output is normalized before
11717                    // the residual, as on the decode path.
11718                    if let Some(wn) = &lw.attn_out_norm {
11719                        for bi in 0..b {
11720                            inference::rms_norm_into(
11721                                &attn[bi * hs..(bi + 1) * hs],
11722                                wn,
11723                                eps,
11724                                norm_style,
11725                                &mut normed[bi * hs..(bi + 1) * hs],
11726                            );
11727                        }
11728                        attn.copy_from_slice(&normed);
11729                    }
11730                    for (dst, &a) in h.iter_mut().zip(&attn) {
11731                        *dst += a;
11732                    }
11733                }
11734                AttnKind::Mla(w) => {
11735                    // Per-position prefill (correctness first; latent
11736                    // batching is a later optimization).
11737                    let inv_freq_l = self.layer_inv_freq(li);
11738                    let rs = self.layer_rope_scale(li);
11739                    let mut normed = vec![0.0f32; hs];
11740                    for bi in 0..b {
11741                        inference::rms_norm_into(
11742                            &h[bi * hs..(bi + 1) * hs],
11743                            &lw.input_norm,
11744                            eps,
11745                            norm_style,
11746                            &mut normed,
11747                        );
11748                        let ao = mla_attention(
11749                            w,
11750                            &normed,
11751                            &mut self.kv_cache.layers[li],
11752                            start_pos + bi,
11753                            &inv_freq_l,
11754                            rs,
11755                            eps,
11756                            pool.as_deref(),
11757                        );
11758                        for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
11759                            *dst += a;
11760                        }
11761                    }
11762                }
11763                AttnKind::Full {
11764                    wq,
11765                    wk,
11766                    wv,
11767                    wo,
11768                    q_norm,
11769                    k_norm,
11770                    output_gate,
11771                    softplus_gate,
11772                    bias,
11773                } => {
11774                    // Chunk-GEMM QKV/O; per-position causal attention
11775                    // inside (roadmap §3 P0 — full-attention prefill no
11776                    // longer re-reads the projection weights b times).
11777                    let mut normed = vec![0.0f32; b * hs];
11778                    for bi in 0..b {
11779                        inference::rms_norm_into(
11780                            &h[bi * hs..(bi + 1) * hs],
11781                            &lw.input_norm,
11782                            eps,
11783                            norm_style,
11784                            &mut normed[bi * hs..(bi + 1) * hs],
11785                        );
11786                    }
11787                    let inv_freq_l = self.layer_inv_freq(li);
11788                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
11789                    let cfg = QwenAttnCfg {
11790                        num_heads: self.layer_num_heads(li),
11791                        num_kv_heads: nkv_l,
11792                        head_dim: hd_l,
11793                        hidden_size: hs,
11794                        position: start_pos,
11795                        inv_freq: &inv_freq_l,
11796                        rotary_dim: rd_l,
11797                        scale: self.attn_scale,
11798                        softcap: self.attn_softcap,
11799                        window: self.layer_window(li),
11800                        v_norm: self.attn_v_norm,
11801                        qk_norm_after_rope: self.qk_norm_after_rope,
11802                        gate_sigmoid: self.proj_gate_sigmoid,
11803                        q_norm: q_norm.as_deref(),
11804                        k_norm: k_norm.as_deref(),
11805                        output_gate: *output_gate,
11806                        softplus_gate: softplus_gate
11807                            .as_ref()
11808                            .map(|(gate, per_head)| (gate, *per_head)),
11809                        rope_scale: self.layer_rope_scale(li),
11810                        bias: bias
11811                            .as_ref()
11812                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
11813                        rms_eps: eps,
11814                        norm_style,
11815                        pool: pool.as_deref(),
11816                        v_head_dim: self.layer_v_dim(li),
11817                    };
11818                    #[cfg(feature = "gpu")]
11819                    let _mirror = self
11820                        .prefill_mirror_target(li)
11821                        .map(crate::gpu::enter_prefill_mirror);
11822                    let mut attn = attention::qwen_attention_batch(
11823                        &normed,
11824                        b,
11825                        wq,
11826                        wk,
11827                        wv,
11828                        wo,
11829                        &mut self.kv_cache.layers[li],
11830                        &cfg,
11831                    );
11832                    if let Some(w) = &lw.attn_out_norm {
11833                        for bi in 0..b {
11834                            inference::rms_norm_into(
11835                                &attn[bi * hs..(bi + 1) * hs],
11836                                w,
11837                                eps,
11838                                norm_style,
11839                                &mut normed[bi * hs..(bi + 1) * hs],
11840                            );
11841                        }
11842                        attn.copy_from_slice(&normed);
11843                    }
11844                    for (dst, &a) in h.iter_mut().zip(&attn) {
11845                        *dst += a;
11846                    }
11847                }
11848                AttnKind::Bounded(w) => {
11849                    // Chunk-GEMM projections, the bounded operator per
11850                    // position over ring + chunk — never a growing KV.
11851                    let mut normed = vec![0.0f32; b * hs];
11852                    for bi in 0..b {
11853                        inference::rms_norm_into(
11854                            &h[bi * hs..(bi + 1) * hs],
11855                            &lw.input_norm,
11856                            eps,
11857                            norm_style,
11858                            &mut normed[bi * hs..(bi + 1) * hs],
11859                        );
11860                    }
11861                    let rope = self
11862                        .bounded_rope
11863                        .clone()
11864                        .expect("bounded layer without an installed rotation table");
11865                    let cfg = crate::bounded::BoundedAttnCfg {
11866                        num_heads: self.num_heads,
11867                        num_kv_heads: self.num_kv_heads,
11868                        head_dim: self.head_dim,
11869                        hidden_size: hs,
11870                        scale: self.attn_scale,
11871                        rope: &rope,
11872                        pool: pool.as_deref(),
11873                    };
11874                    let mut attn = crate::bounded::bounded_attention_batch(
11875                        &normed,
11876                        b,
11877                        w,
11878                        &mut self.kv_cache.layers[li],
11879                        &cfg,
11880                    );
11881                    if let Some(wn) = &lw.attn_out_norm {
11882                        for bi in 0..b {
11883                            inference::rms_norm_into(
11884                                &attn[bi * hs..(bi + 1) * hs],
11885                                wn,
11886                                eps,
11887                                norm_style,
11888                                &mut normed[bi * hs..(bi + 1) * hs],
11889                            );
11890                        }
11891                        attn.copy_from_slice(&normed);
11892                    }
11893                    for (dst, &a) in h.iter_mut().zip(&attn) {
11894                        *dst += a;
11895                    }
11896                    attention::recycle_buf(&mut attn);
11897                }
11898                AttnKind::Linear(w) => {
11899                    for bi in 0..b {
11900                        let normed = inference::rms_norm(
11901                            &h[bi * hs..(bi + 1) * hs],
11902                            &lw.input_norm,
11903                            eps,
11904                            norm_style,
11905                        );
11906                        vmf_phase_forward(
11907                            &normed,
11908                            w,
11909                            &self.vmf_cfg.expect("linear layer without vmf_cfg"),
11910                            &mut self.kv_cache.layers[li].linear_state,
11911                            pool.as_deref(),
11912                        )
11913                        .iter()
11914                        .enumerate()
11915                        .for_each(|(i, &a)| h[bi * hs + i] += a);
11916                    }
11917                }
11918            }
11919
11920            // ── FFN batched ──
11921            let lw = &self.weights.layers[self.phys_layer(li)];
11922            let mut post = vec![0.0f32; b * hs];
11923            for bi in 0..b {
11924                let r =
11925                    inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
11926                post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11927            }
11928            // A restrictive per-visit FFN row lands on the activations
11929            // inside the dense arm; an all-open row costs nothing.
11930            let mask_row = task_mask
11931                .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
11932                .and_then(|m| m.ffn_masks.get(li))
11933                .map(|v| v.as_slice());
11934            let attn_ns = t_attn.elapsed().as_nanos() as u64;
11935            let t_ffn = std::time::Instant::now();
11936            let mut ffn = match &lw.ffn {
11937                FfnKind::Dense(d) if !d.segs.is_empty() => {
11938                    tube_ffn(d, &post, b, pool.as_deref(), mask_row)
11939                }
11940                FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
11941                FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
11942                    moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
11943                }
11944                FfnKind::Moe(m) if self.verify_exact_moe => {
11945                    moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
11946                }
11947                // Keep prompt expert panels off the projection arena and
11948                // use their routes to prime the model-wide bank.
11949                FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
11950                    let before = m.stats.borrow().clone();
11951                    let out = crate::gpu::cpu_scope(|| {
11952                        moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
11953                    });
11954                    self.mimo_moe.prime(li, m, &before);
11955                    out
11956                }
11957                FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
11958                // Dual-branch layers run per position (the expert branch
11959                // reads the raw residual — nothing to batch yet).
11960                FfnKind::DenseMoe(dm) => {
11961                    let mut out = vec![0.0f32; b * hs];
11962                    for bi in 0..b {
11963                        let r = dense_moe_ffn(
11964                            dm,
11965                            &post[bi * hs..(bi + 1) * hs],
11966                            &h[bi * hs..(bi + 1) * hs],
11967                            eps,
11968                            norm_style,
11969                            pool.as_deref(),
11970                        );
11971                        out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11972                    }
11973                    out
11974                }
11975            };
11976            if prefill_prof_on() {
11977                PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
11978                PREFILL_SPLIT[1].fetch_add(
11979                    t_ffn.elapsed().as_nanos() as u64,
11980                    std::sync::atomic::Ordering::Relaxed,
11981                );
11982            }
11983            if let Some(w) = &lw.ffn_out_norm {
11984                for bi in 0..b {
11985                    inference::rms_norm_into(
11986                        &ffn[bi * hs..(bi + 1) * hs],
11987                        w,
11988                        eps,
11989                        norm_style,
11990                        &mut post[bi * hs..(bi + 1) * hs],
11991                    );
11992                }
11993                ffn.copy_from_slice(&post);
11994            }
11995            for (dst, &f) in h.iter_mut().zip(&ffn) {
11996                *dst += f;
11997            }
11998            if let Some(sc) = lw.layer_scale {
11999                for v in h.iter_mut() {
12000                    *v *= sc;
12001                }
12002            }
12003            // CMF_LAYER_DUMP: every position's hidden after layer li.
12004            if self.layer_dump.is_some() {
12005                for bi in 0..b {
12006                    self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
12007                }
12008            }
12009            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
12010                if let Ok(t) = tp.parse::<usize>() {
12011                    if t >= start_pos && t < start_pos + b {
12012                        let bi = t - start_pos;
12013                        let row = &h[bi * hs..(bi + 1) * hs];
12014                        let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
12015                        eprintln!(
12016                            "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
12017                            row[0], row[1]
12018                        );
12019                    }
12020                }
12021            }
12022            // CMF_DEBUG_LAYERS=1: per-layer hidden-state health of the
12023            // LAST prompt position — the knife for "which layer type
12024            // breaks first" on a new architecture.
12025            if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
12026                let row = &h[(b - 1) * hs..b * hs];
12027                let rms =
12028                    (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
12029                let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
12030                eprintln!(
12031                    "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
12032                    match &self.weights.layers[self.phys_layer(li)].attn {
12033                        AttnKind::LinearGdn(_) => "gdn",
12034                        AttnKind::Linear(_) => "vmf",
12035                        AttnKind::ShortConv(_) => "conv",
12036                        _ => "attn",
12037                    },
12038                    match &lw.ffn {
12039                        FfnKind::Moe(_) => "moe",
12040                        FfnKind::Dense(_) => "dense",
12041                        FfnKind::DenseMoe(_) => "dense+moe",
12042                    },
12043                );
12044            }
12045            // Looped Transformer: apply final norm at the end of each loop iteration.
12046            if self.is_loop_end(li) && li + 1 < self.num_layers {
12047                for bi in 0..b {
12048                    let normed = inference::rms_norm(
12049                        &h[bi * hs..(bi + 1) * hs],
12050                        &self.weights.final_norm,
12051                        eps,
12052                        norm_style,
12053                    );
12054                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
12055                }
12056            }
12057            if std::env::var("CMF_TRACE_H").is_ok() {
12058                let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
12059                let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
12060                eprintln!(
12061                    "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
12062                    lw.layer_scale
12063                );
12064            }
12065        }
12066        crate::gpu::set_layer(-1); // lm_head/final ops outside layer-split
12067        if prefill_prof_on() {
12068            let ms = |a: &std::sync::atomic::AtomicU64| {
12069                a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
12070            };
12071            let sp = &attention::ATTN_SPLIT;
12072            let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
12073            eprintln!(
12074                "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
12075                 host gate {:.1}], host loop {:.1}, attend {:.1} \
12076                 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
12077                 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
12078                ms(&PREFILL_SPLIT[0]),
12079                ms(&sp[0]),
12080                qkv_enq,
12081                qkv_wait,
12082                ms(&sp[6]),
12083                ms(&sp[1]),
12084                ms(&sp[2]),
12085                ms(&sp[4]),
12086                ms(&sp[5]),
12087                ca_wait,
12088                ms(&sp[3]),
12089                ms(&PREFILL_SPLIT[1]),
12090            );
12091            let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
12092            eprintln!(
12093                "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
12094                 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
12095                 fallbacks (cumulative)",
12096                attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
12097            );
12098        }
12099        // A batched span owns a complete set of positions. Publish any
12100        // collecting→sealed transition only after every layer has finished;
12101        // callers that cross into serial/device work must see the new epoch
12102        // before this function returns.
12103        self.o1_progress();
12104        // Every layer of the chunk has appended and attended its rows.
12105        self.swa_trim_tails();
12106        h
12107    }
12108
12109    /// Embed a single token.
12110    fn embed_single(&self, id: u32) -> Vec<f32> {
12111        let mut out = vec![0.0f32; self.hidden_size];
12112        if (id as usize) < self.weights.embed_tokens.rows() {
12113            self.weights.embed_tokens.row_f32(id as usize, &mut out);
12114        }
12115        if self.embed_multiplier != 1.0 {
12116            for v in out.iter_mut() {
12117                *v *= self.embed_multiplier;
12118            }
12119        }
12120        // DeepSeek-V4's hash layers route by TOKEN ID, so the id has to
12121        // reach the forward. It rides in slot 0 (the forward re-reads the
12122        // real embedding itself from the table).
12123        if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
12124            let mut v = vec![0.0f32; self.hidden_size.max(1)];
12125            v[0] = id as f32;
12126            return v;
12127        }
12128        // Gemma-3n: the per-layer-embedding half needs the token ID, so
12129        // it rides appended to the embedding; the g3n forward splits it.
12130        // Gemma-4 E-series PLE: same contract, its own table.
12131        if let Some(b) = &self.g4e {
12132            return b.0.extend_embedding(id, &out, self.pool.as_deref());
12133        }
12134        if let Some(b) = &self.g3n {
12135            return b.0.extend_embedding(id, &out, self.pool.as_deref());
12136        }
12137        out
12138    }
12139
12140    /// A run of consecutive prefill layers on the GPU for the whole
12141    /// chunk (default-on under CMF_GPU=1; CMF_GPU_CHUNK=0 disables).
12142    /// Eligibility per layer: q8_row weights, plain full attention
12143    /// (no output gate), F32 KV, no o1/masks/gemma extras. Returns the
12144    /// first layer index NOT processed (== `li0` when the run is empty).
12145    #[cfg(target_os = "macos")]
12146    fn chunk_run_gpu(
12147        &mut self,
12148        li0: usize,
12149        h: &mut [f32],
12150        b: usize,
12151        pos0: usize,
12152        embed_ids: Option<&[u32]>,
12153        cap: usize,
12154    ) -> usize {
12155        // (The old streaming attend needed a depth bound at ~1k; the
12156        // GEMM attention scales like the CPU path and lifted it.)
12157        // CMF_GPU_CHUNK=0 disables the graph.
12158        // Gemma-4 (`metal_gemma4`) brings its own kernels for the global
12159        // layers, V norm, attention scale and sandwich (`ChunkGemma4`).
12160        // `CMF_METAL_GEMMA4_CHUNK=0` keeps its prompt on the host walk.
12161        let gemma4 =
12162            self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
12163        if !crate::gpu::enabled_here()
12164            || std::env::var("CMF_GPU_CHUNK")
12165                .map(|v| v == "0")
12166                .unwrap_or(false)
12167            || b < self.chunk_min_rows
12168            // Sliding windows with per-layer RoPE ride the chunk graph
12169            // (`metal_graph_swa`: causal_softmax_win, per-layer tables).
12170            || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
12171            || (self.global_attn.is_some() && !gemma4)
12172            // Chunk attention has no value-less learned-sink column. q1
12173            // decode can carry it through ordinary `gqa_attend`, but prompt
12174            // chunks must stay on the exact host attention path.
12175            || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
12176            // Per-layer KV heads and narrow V remain outside this chunk
12177            // graph (the q1 sliding-native door checks the same geometry).
12178            || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
12179            // Collection owns the exact Q trace and boundary conversion;
12180            // this chunk graph appends dense KV without feeding that trace.
12181            || self.o1_active()
12182            || (self.attn_v_norm && !gemma4)
12183            || !self.metal_rope_scale_ok()
12184            || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
12185        {
12186            return li0;
12187        }
12188        let Some(model) = self.model.clone() else {
12189            return li0;
12190        };
12191        let (nh, nkv, hd, hs) = (
12192            self.num_heads,
12193            self.num_kv_heads,
12194            self.head_dim,
12195            self.hidden_size,
12196        );
12197        // Collect the longest run of consecutive eligible layers.
12198        // Looped Transformer: stop at the loop boundary so the CPU can
12199        // apply loop_final_norm between iterations.
12200        let loop_end = if self.loop_final_norm {
12201            ((li0 / self.physical_layers) + 1) * self.physical_layers
12202        } else {
12203            self.num_layers
12204        };
12205        let run_end = self.num_layers.min(loop_end).min(cap);
12206        // Each layer's own RoPE table (Spark-X2.5: sliding and full layers
12207        // differ); the global one for every model without sliding layers.
12208        // Declared before `layers`, which borrows it (and drops a routing
12209        // closure on MoE layers).
12210        let tables: Vec<std::sync::Arc<Vec<f32>>> =
12211            (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
12212        let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
12213        let mut stored_at: Vec<usize> = Vec::new();
12214        for li in li0..run_end {
12215            let lw = &self.weights.layers[self.phys_layer(li)];
12216            if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
12217                && !gemma4
12218            {
12219                break;
12220            }
12221            let AttnKind::Full {
12222                wq,
12223                wk,
12224                wv,
12225                wo,
12226                q_norm,
12227                k_norm,
12228                output_gate: false,
12229                softplus_gate,
12230                bias,
12231            } = &lw.attn
12232            else {
12233                break;
12234            };
12235            // Spark-X2.5's per-head sigmoid g_proj gate, from f32 rows.
12236            let head_gate = match softplus_gate {
12237                None => None,
12238                Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
12239                    Some((d, r, c)) if r == nh && c == hs => Some(d),
12240                    _ => break,
12241                },
12242                Some(_) => break,
12243            };
12244            // The FFN: a dense SiLU/GELU trio, or routed q4tp experts
12245            // (Mellum2.1: softmax router, no shared expert) whose first
12246            // trio stands in for the shape checks.
12247            let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
12248                FfnKind::Dense(d)
12249                    if (matches!(d.act, Act::Silu | Act::Gelu)
12250                        || (gemma4 && d.act == Act::GeluTanh))
12251                        && d.segs.is_empty() =>
12252                {
12253                    (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
12254                }
12255                FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
12256                    Some((router, experts, q8)) => {
12257                        let e0 = &m.experts[0];
12258                        let route = Box::new(move |lg: &[f32]| {
12259                            // `moe_ffn_batch`'s routing and weights, row by row.
12260                            let (idx, p, wsum) = moe_route(lg, m, None);
12261                            let mut st = m.stats.borrow_mut();
12262                            if st.len() < m.experts.len() {
12263                                st.resize(m.experts.len(), 0);
12264                            }
12265                            for &e in &idx {
12266                                st[e] += 1;
12267                            }
12268                            let w = idx.iter().map(|&e| p[e] / wsum).collect();
12269                            (idx, w)
12270                        });
12271                        (
12272                            &e0.gate_proj,
12273                            &e0.up_proj,
12274                            &e0.down_proj,
12275                            Act::Silu,
12276                            Some(crate::gpu_metal::ChunkMoe {
12277                                router,
12278                                experts,
12279                                q8,
12280                                route,
12281                            }),
12282                        )
12283                    }
12284                    None => break,
12285                },
12286                _ => break,
12287            };
12288            // q8_row (row_scale populated), q8_2f (plus its input field),
12289            // or q4_tiled / q4tp (row_scale empty — their scales are in the
12290            // payload). Mixing across the seven projections of one layer is
12291            // fine; the encoder branches per weight on the tensor's dtype.
12292            // Anything else refuses.
12293            fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
12294                t.q8_row_parts()
12295                    .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
12296                    .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12297                    .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12298            }
12299            fn cf(t: &QTensor) -> &[f32] {
12300                t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
12301            }
12302            let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
12303            let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
12304            // q8_2f projections ride the chunk graph on the MoE layers it
12305            // was measured on (Mellum2.1) and on the dense exact-GELU FFN
12306            // (Spark-X2.6-4B q8_2f on the M4: the gate/up inputs staged as
12307            // x·col, the down field applied after the activation —
12308            // `ChunkLayer::ffn_col`). A dense SiLU q8_2f stack keeps the host
12309            // walk: its down GEMM fuses the activation into the X-tile load
12310            // and has no input field (0.8.14 refused every q8_2f weight here).
12311            // Gemma-4 reads every q8_2f weight through the blob GEMM with its
12312            // own field (`ChunkGemma4`), so its dense q8_2f stack rides.
12313            if moe.is_none()
12314                && !gemma4
12315                && ffn_act != Act::Gelu
12316                && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
12317            {
12318                break;
12319            }
12320            let parts = (
12321                cw(wq),
12322                cw(wk),
12323                cw(wv),
12324                cw(wo),
12325                cw(gate_t),
12326                cw(up_t),
12327                cw(down_t),
12328            );
12329            let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
12330            else {
12331                break;
12332            };
12333            let layer = &self.kv_cache.layers[li];
12334            if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
12335                break;
12336            }
12337            // A trimmed tail holds every row the chunk's first query reads.
12338            debug_assert!(
12339                layer.base() == 0
12340                    || self
12341                        .layer_window(li)
12342                        .is_some_and(|w| layer.head_len(0) + 1 >= w)
12343            );
12344            stored_at.push(layer.head_len(0));
12345            // Per-layer geometry for Gemma-4 (512-wide global layers over
12346            // one KV head); every other model keeps the pipeline-wide one.
12347            let (nkv, hd) = if gemma4 {
12348                let (a, bb, _) = self.layer_geom(li);
12349                (a, bb)
12350            } else {
12351                (nkv, hd)
12352            };
12353            layers.push(crate::gpu_metal::ChunkLayer {
12354                model: &model,
12355                kv_id: self.graph_kv_id,
12356                layer: li,
12357                wq: pq,
12358                wk: pk,
12359                wv: pv,
12360                wo: po,
12361                gate: pg,
12362                up: pu,
12363                down: pd,
12364                input_norm: &lw.input_norm,
12365                post_norm: &lw.post_norm,
12366                bias: bias
12367                    .as_ref()
12368                    .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
12369                q_norm: q_norm.as_deref(),
12370                k_norm: k_norm.as_deref(),
12371                inv_freq: &tables[li - li0],
12372                rope_scale: self.layer_rope_scale(li),
12373                rd: self.layer_geom(li).2,
12374                nh,
12375                nkv,
12376                hd,
12377                hs,
12378                inter: gate_t.rows(),
12379                gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
12380                late_qk_norm: self.qk_norm_after_rope,
12381                eps: self.rms_eps as f32,
12382                window: self.layer_window(li),
12383                head_gate,
12384                act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
12385                g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
12386                    v_norm: self.attn_v_norm,
12387                    scale: self.attn_scale,
12388                    attn_out_norm: lw.attn_out_norm.as_deref(),
12389                    ffn_out_norm: lw.ffn_out_norm.as_deref(),
12390                    layer_scale: lw.layer_scale,
12391                }),
12392                col,
12393                // The plain dense branch alone reads it: routed experts and
12394                // Gemma-4 carry their own fields.
12395                ffn_col: if moe.is_none() && !gemma4 {
12396                    ffn_col
12397                } else {
12398                    [&[][..]; 3]
12399                },
12400                moe,
12401            });
12402        }
12403        if layers.is_empty() {
12404            return li0;
12405        }
12406        // KV row width per layer (uniform but for Gemma-4).
12407        let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
12408        let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
12409            .iter()
12410            .zip(&rows_kv)
12411            .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
12412            .collect();
12413        let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
12414        for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
12415            let li = layers[i].layer;
12416            let layer = &self.kv_cache.layers[li];
12417            let nkv = rows_kv[i].0;
12418            io.push(crate::gpu_metal::ChunkIo {
12419                cpu_stored: stored_at[i],
12420                cpu_gen: layer.generation(),
12421                cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
12422                cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
12423                out_k: ok,
12424                out_v: ov,
12425                imp: oi,
12426            });
12427        }
12428        let n_run = layers.len();
12429        let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
12430        // Device-side embedding when the run starts the model and the
12431        // embedding matrix is q8_row-mapped.
12432        let ep = embed_ids.and_then(|ids| {
12433            self.weights
12434                .embed_tokens
12435                .q8_row_parts()
12436                .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
12437                    idx,
12438                    rows,
12439                    row_scale: rs,
12440                    ids,
12441                    mult: self.embed_multiplier,
12442                })
12443        });
12444        if embed_ids.is_some() && ep.is_none() {
12445            return li0;
12446        }
12447        if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
12448            return li0;
12449        }
12450        drop(io);
12451        drop(layers);
12452        // CPU caches stay the owners of record: append the chunk rows
12453        // and bank the importance masses per layer.
12454        for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
12455            let li = li0 + i;
12456            let row = rows_kv[i].1;
12457            let layer = &mut self.kv_cache.layers[li];
12458            for bi in 0..b {
12459                layer.append(
12460                    &ok[bi * row..(bi + 1) * row],
12461                    &ov[bi * row..(bi + 1) * row],
12462                    &[],
12463                );
12464            }
12465            layer.accumulate_imp(oi);
12466        }
12467        last
12468    }
12469
12470    /// Is layer `li` a sliding-window (local-RoPE) layer? Gemma-3:
12471    /// every `pattern`-th layer is global, the rest are local.
12472    fn layer_is_local(&self, li: usize) -> bool {
12473        if let Some(layers) = &self.sliding_layers {
12474            return layers.get(li).copied().unwrap_or(false);
12475        }
12476        match self.swa {
12477            Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
12478            None => false,
12479        }
12480    }
12481
12482    /// The RoPE table for layer `li` (local layers may have their own;
12483    /// Gemma-4 global layers use the proportional padded table).
12484    fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
12485        if self.layer_is_local(li) {
12486            if let Some(f) = &self.inv_freq_local {
12487                return f.clone();
12488            }
12489        } else if let Some(f) = &self.inv_freq_global {
12490            return f.clone();
12491        }
12492        self.inv_freq.clone()
12493    }
12494
12495    /// The attend window for layer `li` (None = full context).
12496    fn layer_window(&self, li: usize) -> Option<usize> {
12497        self.swa
12498            .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
12499    }
12500
12501    /// Drop every sliding layer's rows its window can no longer read
12502    /// (`LayerKvCache::trim_window`). Call only at a safe point — after a
12503    /// complete position / pair / chunk walk, never between a layer's
12504    /// appends and its attend. One comparison per layer below the trigger.
12505    ///
12506    /// Off with an MTP head: its verify oracles and rollbacks snapshot and
12507    /// compare per-layer row counts as positions (`CMF_METAL_VERIFY_CHECK`,
12508    /// the MTP caches), which a trimmed tail would break.
12509    fn swa_trim_tails(&mut self) {
12510        let Some((slack, align)) = self.swa_trim else {
12511            return;
12512        };
12513        if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
12514            return;
12515        }
12516        for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
12517            if let Some(w) = self.layer_window(li) {
12518                self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
12519            }
12520        }
12521    }
12522
12523    fn layer_num_heads(&self, li: usize) -> usize {
12524        self.attention_heads_per_layer
12525            .as_ref()
12526            .and_then(|v| v.get(li).copied())
12527            .unwrap_or(self.num_heads)
12528    }
12529
12530    fn layer_rope_scale(&self, li: usize) -> f32 {
12531        if self.layer_is_local(li) {
12532            self.rope_scale_local
12533        } else {
12534            self.rope_scale
12535        }
12536    }
12537
12538    /// Attention geometry of layer `li`: (num_kv_heads, head_dim,
12539    /// rotary_dim). Gemma-4 global layers override all three.
12540    fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
12541        if !self.layer_is_local(li) {
12542            if let Some((ghd, gkv)) = self.global_attn {
12543                return (gkv, ghd, ghd);
12544            }
12545        }
12546        let rd = if self.layer_is_local(li) {
12547            self.rotary_dim_local.unwrap_or(self.rotary_dim)
12548        } else {
12549            self.rotary_dim
12550        };
12551        // A layer without positional encoding rotates nothing.
12552        let rd = if self
12553            .no_rope_layers
12554            .as_ref()
12555            .and_then(|v| v.get(li))
12556            .copied()
12557            == Some(true)
12558        {
12559            0
12560        } else {
12561            rd
12562        };
12563        (self.layer_num_kv_heads(li), self.head_dim, rd)
12564    }
12565
12566    /// KV heads of layer `li` (virtual index): the per-layer count when the
12567    /// model has one (MiMo-V2), else the uniform `num_kv_heads`.
12568    fn layer_num_kv_heads(&self, li: usize) -> usize {
12569        self.kv_heads_per_layer
12570            .as_ref()
12571            .and_then(|v| v.get(self.phys_layer(li)).copied())
12572            .unwrap_or(self.num_kv_heads)
12573    }
12574
12575    /// V head width of layer `li` (≤ its head_dim).
12576    fn layer_v_dim(&self, li: usize) -> usize {
12577        let (_, hd, _) = self.layer_geom(li);
12578        self.v_head_dim.unwrap_or(hd).min(hd)
12579    }
12580
12581    /// Install a per-layer KV geometry: KV heads per PHYSICAL layer and/or
12582    /// a V head width narrower than `head_dim` (MiMo-V2). Validates it and
12583    /// reshapes the caches of every layer whose KV head count differs from
12584    /// `num_kv_heads`. The loader and the tests share this one path, so a
12585    /// hand-built pipeline cannot hold a geometry the loader would refuse.
12586    /// Call before the first forward (it drops cached rows of reshaped
12587    /// layers). Refuses combinations whose paths would read it wrong:
12588    /// Gemma-4 global layers and MLA carry their own geometry.
12589    pub fn set_attn_geometry(
12590        &mut self,
12591        kv_heads_per_layer: Option<Vec<usize>>,
12592        v_head_dim: Option<usize>,
12593    ) -> Result<(), String> {
12594        if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
12595            if self.global_attn.is_some() {
12596                return Err(
12597                    "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
12598                     attention geometry"
12599                        .into(),
12600                );
12601            }
12602            if self
12603                .weights
12604                .layers
12605                .iter()
12606                .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
12607            {
12608                return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
12609            }
12610        }
12611        if let Some(vd) = v_head_dim {
12612            if vd == 0 || vd > self.head_dim {
12613                return Err(format!(
12614                    "v_head_dim {vd} must be in 1..={} (head_dim)",
12615                    self.head_dim
12616                ));
12617            }
12618        }
12619        if let Some(v) = &kv_heads_per_layer {
12620            if v.len() != self.physical_layers {
12621                return Err(format!(
12622                    "kv_heads_per_layer has {} entries, expected {} layers",
12623                    v.len(),
12624                    self.physical_layers
12625                ));
12626            }
12627            for (li, &nkv) in v.iter().enumerate() {
12628                let is_attn = matches!(
12629                    self.weights.layers.get(li).map(|lw| &lw.attn),
12630                    Some(AttnKind::Full { .. }) | None
12631                );
12632                if !is_attn {
12633                    continue;
12634                }
12635                let nh = self
12636                    .attention_heads_per_layer
12637                    .as_ref()
12638                    .and_then(|h| h.get(li).copied())
12639                    .unwrap_or(self.num_heads);
12640                if nkv == 0 || nh % nkv != 0 {
12641                    return Err(format!(
12642                        "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
12643                    ));
12644                }
12645            }
12646        }
12647        self.kv_heads_per_layer = kv_heads_per_layer;
12648        self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
12649        if self.kv_heads_per_layer.is_some() {
12650            for li in 0..self.kv_cache.layers.len() {
12651                let full = matches!(
12652                    self.weights
12653                        .layers
12654                        .get(self.phys_layer(li))
12655                        .map(|lw| &lw.attn),
12656                    Some(AttnKind::Full { .. })
12657                );
12658                let nkv = self.layer_num_kv_heads(li);
12659                let cache = &self.kv_cache.layers[li];
12660                if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
12661                    let sinks = cache.sinks.clone();
12662                    self.kv_cache.layers[li] =
12663                        crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
12664                    self.kv_cache.layers[li].sinks = sinks;
12665                }
12666            }
12667        }
12668        Ok(())
12669    }
12670
12671    /// Attach learned attention-sink logits (one per Q head) to PHYSICAL
12672    /// layer `phys` — every virtual layer that runs it. The loader calls
12673    /// this for each `model.layers.N.self_attn.sinks` tensor.
12674    pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
12675        let Some(lw) = self.weights.layers.get(phys) else {
12676            return Err(format!("sinks for layer {phys}: no such layer"));
12677        };
12678        if !matches!(lw.attn, AttnKind::Full { .. }) {
12679            return Err(format!(
12680                "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
12681            ));
12682        }
12683        let nh = self
12684            .attention_heads_per_layer
12685            .as_ref()
12686            .and_then(|h| h.get(phys).copied())
12687            .unwrap_or(self.num_heads);
12688        if sinks.len() != nh {
12689            return Err(format!(
12690                "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
12691                sinks.len()
12692            ));
12693        }
12694        if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
12695            return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
12696        }
12697        for li in 0..self.kv_cache.layers.len() {
12698            if self.phys_layer(li) == phys {
12699                self.kv_cache.layers[li].sinks = Some(sinks.clone());
12700            }
12701        }
12702        Ok(())
12703    }
12704
12705    /// Why the GPU attention graphs cannot serve this model, if they
12706    /// cannot: the wgpu whole-token and batched graphs, the greedy
12707    /// multi-burst, the q1 attention dropin and the Metal block/chunk/rows
12708    /// graphs all assume ONE (num_kv_heads, head_dim) geometry, V heads as
12709    /// wide as K, a single RoPE table, full-context attention and a plain
12710    /// softmax. A model outside that contract runs on the CPU layer walk
12711    /// (and the per-op GPU matvecs) until a graph learns it — never on a
12712    /// graph that would read it wrong. None = no attention-level reason
12713    /// (the graph builders still check weights and layer kinds).
12714    pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
12715        if self.kv_heads_per_layer.is_some() {
12716            return Some("per-layer KV head counts");
12717        }
12718        if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
12719            return Some("V heads narrower than Q/K heads");
12720        }
12721        if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
12722            return Some("learned attention sinks");
12723        }
12724        if self.swa.is_some() || self.sliding_layers.is_some() {
12725            return Some("sliding-window layers");
12726        }
12727        None
12728    }
12729
12730    /// Can the Metal kernels apply this model's RoPE amplitude? Every
12731    /// rotation kernel (`attn_rope_qkn[_b]`, `chunk_rope_kv`) multiplies the
12732    /// rotated pair by the layer's `rope_scale` (YaRN `attention_factor`,
12733    /// Mellum2.1's global layers) exactly where the CPU's
12734    /// `rope_rotate_scaled` does. With the Q/K norm AFTER RoPE the kernels'
12735    /// shared sum of squares would be the unscaled one, so that pairing
12736    /// (no known model) declines.
12737    #[cfg(target_os = "macos")]
12738    fn metal_rope_scale_ok(&self) -> bool {
12739        !(self.qk_norm_after_rope
12740            && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
12741    }
12742
12743    /// Can the Metal block graph carry this model's sliding-window layers?
12744    /// Both of its attention forms take a layer's own window, RoPE table
12745    /// and rotary width: the device attend (`AttnDeviceParams::window`, the
12746    /// layer's `inv_freq`/`rd`) and the sandwich's host attend (exactly the
12747    /// CPU path's attention). True when the sliding window is the model's
12748    /// only attention-level decline (Spark-X2.5). Learned, value-less
12749    /// sinks may accompany that geometry: q1's ordinary device GQA
12750    /// includes the sink in its softmax denominator and disables split-K.
12751    /// Per-layer KV heads, narrow V and Gemma-4 global geometry still
12752    /// decline; a YaRN amplitude per layer rides along
12753    /// (`metal_rope_scale_ok`).
12754    ///
12755    /// Per-layer Q heads (Laguna) and capped scores (Gemma-2) decline here
12756    /// too, with V norm (Gemma-4): the chunk prefill's own gate checks
12757    /// neither of the first two, and before this door opened its `swa`
12758    /// refusal kept every sliding model with them off the chunk graph.
12759    #[cfg(target_os = "macos")]
12760    fn metal_graph_swa(&self) -> bool {
12761        (self.swa.is_some() || self.sliding_layers.is_some())
12762            && self.attention_heads_per_layer.is_none()
12763            && !self.attn_v_norm
12764            && self.attn_softcap == 0.0
12765            && self.kv_heads_per_layer.is_none()
12766            && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
12767            && self.global_attn.is_none()
12768            && self.inv_freq_global.is_none()
12769            && self.metal_rope_scale_ok()
12770            && !(0..self.num_layers).any(|li| {
12771                self.layer_is_local(li)
12772                    && self.inv_freq_local.is_none()
12773                    && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
12774            })
12775    }
12776
12777    /// Can the Metal graphs carry this Gemma-4 model (`gpu_metal::gemma4`)?
12778    /// Its sliding layers (window, local RoPE table, 256-wide heads)
12779    /// device-attend like Spark-X2.5's; the 512-wide global layers (one KV
12780    /// head, proportional RoPE in the zero-padded table) take the 16-slot
12781    /// kernels; the V norm, sandwich norms, layer scalar and tanh-GELU come
12782    /// from the Gemma-4 library. Dense FFNs only (no Gemma-4 MoE measured).
12783    /// `CMF_METAL_GEMMA4=0` declines it as before (CPU attention).
12784    #[cfg(target_os = "macos")]
12785    fn metal_gemma4(&self) -> bool {
12786        crate::gpu_metal::gemma4::enabled()
12787            && (self.global_attn.is_some() || self.attn_v_norm)
12788            && self.attention_heads_per_layer.is_none()
12789            && self.kv_heads_per_layer.is_none()
12790            && self.v_head_dim.is_none()
12791            && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
12792            && self.attn_softcap == 0.0
12793            && !self.qk_norm_after_rope
12794            && self.metal_rope_scale_ok()
12795            && self
12796                .weights
12797                .layers
12798                .iter()
12799                .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
12800            && !(0..self.num_layers).any(|li| {
12801                self.layer_is_local(li)
12802                    && self.inv_freq_local.is_none()
12803                    && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
12804            })
12805    }
12806
12807    /// Why the WGPU graphs (whole-token, batched prefill, greedy burst)
12808    /// cannot run this model's attention, if they cannot. Per-layer KV
12809    /// heads, V narrower than K, learned sinks and sliding windows ride
12810    /// their per-layer geometry (`GraphAttnGeom`, the ATTEND_X kernels);
12811    /// what that geometry does not express keeps the decline, by name.
12812    /// None for every model with one attention geometry.
12813    pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
12814        // Keep the ordinary (uniform) fast arms off YaRN until they carry a
12815        // scale field too. A non-uniform geometry gets a `GraphAttnGeom` and
12816        // is handled below by the generic graph kernels.
12817        let geometry_reason = self.graph_attn_decline_reason();
12818        let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
12819        if has_scaled_rope && geometry_reason.is_none() {
12820            return Some("scaled RoPE without per-layer geometry");
12821        }
12822        // For Q/K norm after partial RoPE, the scale changes only the rotary
12823        // slice's RMS denominator. The generic graph deliberately declines
12824        // that uncommon combination until it carries a separate rotary sum;
12825        // Mellum has q/k norm before RoPE, so it remains on this path.
12826        if has_scaled_rope && self.qk_norm_after_rope {
12827            return Some("scaled RoPE with post-RoPE Q/K norm");
12828        }
12829        geometry_reason?;
12830        // Gemma-4: global layers 512 wide over 256-wide sliding ones
12831        // (`GraphAttnGeom::hd`), the scale-less V norm (`v_norm`), and the
12832        // sandwich norms + layer scalar (`GraphLayer`) ride the TOKEN graph.
12833        // The batch graph has none of the three and refuses them itself
12834        // (`batch_graph_gemma_decline`).
12835        if self.attention_heads_per_layer.is_some() {
12836            return Some("per-layer Q head counts with per-layer geometry");
12837        }
12838        // Sandwich norms are measured on Gemma-4 (the humanizer port) and
12839        // LFM3, both with plain x̂·w weights, against HF transformers;
12840        // Gemma-2/3's (1+w) norms keep the per-op path until they are
12841        // measured the same way, and a layer scale stays Gemma-4's.
12842        let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
12843        let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
12844        if !gemma4
12845            && self.weights.layers.iter().any(|lw| {
12846                lw.layer_scale.is_some()
12847                    || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some()))
12848            })
12849        {
12850            return Some("sandwich norms / layer scale with per-layer geometry");
12851        }
12852        // `GraphAttnGeom::rope_scale` carries YaRN's post-rotation
12853        // attention-amplitude factor to the WGPU RoPE kernel. It is not an
12854        // angular position scale, so it must not be folded into `invf`.
12855        if self.weights.layers.iter().any(|lw| {
12856            matches!(
12857                &lw.attn,
12858                AttnKind::Full {
12859                    output_gate: true,
12860                    ..
12861                }
12862            )
12863        }) && self.v_head_dim.is_some()
12864        {
12865            return Some("gated attention with V narrower than K");
12866        }
12867        if (0..self.num_layers).any(|li| {
12868            self.layer_is_local(li)
12869                && self.inv_freq_local.is_none()
12870                && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
12871        }) {
12872            return Some("local rotary width without a local RoPE table");
12873        }
12874        None
12875    }
12876
12877    /// What the wgpu BATCH graph (and the attention-only singleton graphs)
12878    /// cannot run of Gemma-4, which the token graph does run: per-layer
12879    /// head widths, the V norm, sandwich norms and the layer scalar.
12880    fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
12881        // Gemma-4 rides `gemma_batch` (per-layer head width, V norm,
12882        // sandwich norms, layer scalar) unless CMF_G4_BATCH=0.
12883        if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
12884            return None;
12885        }
12886        if self.global_attn.is_some() {
12887            return Some("per-layer head width (Gemma-4 global layers)");
12888        }
12889        if self.attn_v_norm {
12890            return Some("V norm");
12891        }
12892        if self.weights.layers.iter().any(|lw| {
12893            lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
12894        }) {
12895            return Some("sandwich norms / layer scale");
12896        }
12897        None
12898    }
12899
12900    /// The wgpu graphs' attention geometry for layer `li` (virtual index):
12901    /// Some only for a model whose layers do not share one (MiMo-V2) — KV
12902    /// heads, V width, rotary width and RoPE table, window and sinks of
12903    /// THIS layer, exactly what the CPU attention reads for it.
12904    fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
12905        self.graph_attn_decline_reason()?;
12906        let (nkv, hd, rd) = self.layer_geom(li);
12907        let invf: &[f32] = if self.layer_is_local(li) {
12908            match &self.inv_freq_local {
12909                Some(f) => f.as_slice(),
12910                None => self.inv_freq.as_slice(),
12911            }
12912        } else {
12913            match &self.inv_freq_global {
12914                Some(f) => f.as_slice(),
12915                None => self.inv_freq.as_slice(),
12916            }
12917        };
12918        Some(crate::gpu::GraphAttnGeom {
12919            nkv,
12920            hd,
12921            dv: self.layer_v_dim(li),
12922            rd,
12923            invf,
12924            rope_scale: self.layer_rope_scale(li),
12925            window: self.layer_window(li),
12926            sink: self.kv_cache.layers[li].sinks.as_deref(),
12927            v_norm: self.attn_v_norm,
12928        })
12929    }
12930
12931    /// The decode graph's device K/V mirror that layer `li`'s batched
12932    /// prefill appends its chunk to and attends against (wgpu), instead
12933    /// of uploading the layer's whole K/V prefix every chunk and the whole
12934    /// cache again at the first decode token. Only where the token graph
12935    /// will read that very mirror: the decode graph on, not refused, and
12936    /// this layer's geometry what it requests (`graph_attn_geom`: KV heads,
12937    /// V as wide as K, the window as a ring, no sink).
12938    ///
12939    /// Spark-X2.5 only (`proj_gate_sigmoid`), the one architecture whose
12940    /// output was measured identical on it. `CMF_PREFILL_MIRROR=0` keeps
12941    /// the host upload (A/B).
12942    #[cfg(feature = "gpu")]
12943    fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
12944        if !self.proj_gate_sigmoid
12945            || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
12946            || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
12947            || self.graph_refused()
12948            || self.o1_active()
12949            || self.attn_softcap > 0.0
12950            || self.wgpu_graph_attn_decline().is_some()
12951        {
12952            return None;
12953        }
12954        let g = self.graph_attn_geom(li)?;
12955        let (nkv, hd, _) = self.layer_geom(li);
12956        // The token graph passes the call-wide head width (layer 0's).
12957        if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
12958            return None;
12959        }
12960        Some(crate::gpu::PrefillMirror {
12961            kv_id: self.graph_kv_id,
12962            layer: li,
12963            limit: self.kv_cache.max_seq_len,
12964        })
12965    }
12966
12967    /// Bring the host KV cache of every Full-attention layer in
12968    /// `[from, upto)` up to `position` rows from the wgpu mirrors, where a
12969    /// device graph advanced a layer that the host is about to run: a
12970    /// device prefix that shrank since the prompt (or a batched prefill
12971    /// prefix longer than the decode one). A layer whose mirror does not
12972    /// hold the missing rows is left alone. Rows a sliding layer's ring
12973    /// no longer holds come back as zeros — outside every window that
12974    /// will read them.
12975    #[cfg(feature = "gpu")]
12976    fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
12977        let kv_id = self.graph_kv_id;
12978        for li in from..upto.min(self.num_layers) {
12979            if !matches!(
12980                self.weights.layers[self.phys_layer(li)].attn,
12981                AttnKind::Full { .. }
12982            ) {
12983                continue;
12984            }
12985            // Absolute: a trimmed sliding tail holds positions base.. only.
12986            let host = self.kv_cache.layers[li].pos_len();
12987            if host >= position {
12988                continue;
12989            }
12990            let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
12991                continue;
12992            };
12993            let to = dev.min(position);
12994            if to <= host {
12995                continue;
12996            }
12997            let (nkv, hd) = {
12998                let c = &self.kv_cache.layers[li];
12999                (c.num_kv_heads, c.head_dim)
13000            };
13001            let Some((k, v, first_valid)) =
13002                crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
13003            else {
13004                continue;
13005            };
13006            // A sliding layer only ever reads its last `window` rows; a
13007            // full-context layer needs every row it did not have.
13008            let need_from = match self.layer_window(li) {
13009                Some(w) => host.max((position + 1).saturating_sub(w)),
13010                None => host,
13011            };
13012            if first_valid > need_from {
13013                tracing::warn!(
13014                    "layer {li}: device KV rows {host}..{to} no longer resident \
13015                     (from {first_valid}); host attention will miss them"
13016                );
13017            }
13018            let row = nkv * hd;
13019            let cache = &mut self.kv_cache.layers[li];
13020            for p in 0..to - host {
13021                cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
13022            }
13023        }
13024    }
13025
13026    /// Log (once per graph site and pipeline) that `site` declined for
13027    /// `reason`. The lines are kept so a caller or a test can read them.
13028    fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
13029        let mut seen = self.graph_declines.borrow_mut();
13030        if !seen.iter().any(|&(s, r)| s == site && r == reason) {
13031            tracing::warn!("{site} declined: {reason} (CPU attention path)");
13032            seen.push((site, reason));
13033        }
13034    }
13035
13036    /// The GPU-graph declines this pipeline has logged so far, as the
13037    /// logged lines.
13038    pub fn graph_declines(&self) -> Vec<String> {
13039        self.graph_declines
13040            .borrow()
13041            .iter()
13042            .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
13043            .collect()
13044    }
13045
13046    /// Does layer `li` have the plain attention geometry the historical
13047    /// head-masked f32 path (`multi_head_attention`) assumes — pipeline-wide
13048    /// KV heads / head_dim / RoPE table, full context, no sink, V as wide
13049    /// as K? Anything else runs the dense `qwen_attention` instead.
13050    /// `CMF_LAYER_DUMP` writer (see `Pipeline::layer_dump`): one position's
13051    /// hidden after layer `li` as raw little-endian f32 into
13052    /// `<dir>/p{pos:06}_l{li:02}.f32`. A failed write is reported once and
13053    /// never stops the forward — the dump is a diagnostic.
13054    fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
13055        let Some(dir) = &self.layer_dump else {
13056            return;
13057        };
13058        let mut bytes = Vec::with_capacity(row.len() * 4);
13059        for v in row {
13060            bytes.extend_from_slice(&v.to_le_bytes());
13061        }
13062        let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
13063        if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
13064            use std::sync::atomic::{AtomicBool, Ordering};
13065            static SAID: AtomicBool = AtomicBool::new(false);
13066            if !SAID.swap(true, Ordering::Relaxed) {
13067                tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
13068            }
13069        }
13070    }
13071
13072    /// Decide the MiMo-V2 expert placement once (`crate::mimo_moe`). Any
13073    /// other model turns the slot off on the first call.
13074    fn mimo_moe_prepare(&mut self) {
13075        if !self.mimo_moe.is_undecided() {
13076            return;
13077        }
13078        let slot = {
13079            let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
13080                .filter_map(
13081                    |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
13082                        FfnKind::Moe(m) => Some((li, m)),
13083                        _ => None,
13084                    },
13085                )
13086                .collect();
13087            // One bank lives on one device: an in-process multi-GPU split
13088            // keeps the whole-layer path.
13089            if layers.is_empty()
13090                || self.physical_layers != self.num_layers
13091                || self.gpu_plan.is_some()
13092            {
13093                crate::mimo_moe::Slot::Off
13094            } else {
13095                // Whether a whole-token graph could run this model's layers
13096                // (then a whole-layer prefix is one submit, not per-layer
13097                // fences).
13098                let graph_prefix =
13099                    self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
13100                crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
13101            }
13102        };
13103        self.mimo_moe = slot;
13104    }
13105
13106    #[cfg(test)]
13107    pub(crate) fn test_graph_kv_id(&self) -> u64 {
13108        self.graph_kv_id
13109    }
13110
13111    /// Dynamic MiMo layer: one device attention graph, followed by a bank
13112    /// frame. Both decode and short verification use this same attention
13113    /// path and absolute layer key; the host KV may intentionally lag.
13114    pub(crate) fn mimo_graph_layer_rows(
13115        &mut self,
13116        li: usize,
13117        h: &mut [f32],
13118        positions: &[usize],
13119    ) -> crate::gpu::BatchGraphOutcome {
13120        use crate::gpu::BatchGraphOutcome as Out;
13121        let b = positions.len();
13122        if !(1..=4).contains(&b)
13123            || h.len() != b * self.hidden_size
13124            || !self.mimo_moe.is_dynamic(li, true)
13125            || !crate::gpu::enabled_here()
13126            || !crate::gpu::wgpu_active()
13127            || self.o1_active()
13128            || self.physical_layers != self.num_layers
13129            // The pair-fusion diagnostic (and an explicit graph-off run)
13130            // rewinds only host KV. A hidden singleton attention graph here
13131            // would leave device mirrors ahead of the next host position.
13132            || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
13133            || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
13134            || self.wgpu_graph_attn_decline().is_some()
13135        {
13136            return Out::Declined;
13137        }
13138        let attn_started = std::time::Instant::now();
13139        let outcome = {
13140            let lw = &self.weights.layers[li];
13141            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13142                return Out::Declined;
13143            }
13144            let FfnKind::Moe(m) = &lw.ffn else {
13145                return Out::Declined;
13146            };
13147            let AttnKind::Full {
13148                wq,
13149                wk,
13150                wv,
13151                wo,
13152                q_norm,
13153                k_norm,
13154                output_gate,
13155                softplus_gate,
13156                bias,
13157            } = &lw.attn
13158            else {
13159                return Out::Declined;
13160            };
13161            if *output_gate || softplus_gate.is_some() {
13162                return Out::Declined;
13163            }
13164            let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
13165                m.experts
13166                    .first()?
13167                    .gate_proj
13168                    .mapped_q4tp()
13169                    .map(|(m, _)| m.clone())
13170            }) else {
13171                return Out::Declined;
13172            };
13173            fn gw<'a>(
13174                t: &'a QTensor,
13175                owner: &std::sync::Arc<cortiq_core::CmfModel>,
13176            ) -> Option<crate::gpu::GraphW<'a>> {
13177                if let Some((m, idx, kind, rs)) = t.graph_weight() {
13178                    if m.uid() != owner.uid() || t.has_prism_contract() {
13179                        return None;
13180                    }
13181                    return Some(crate::gpu::GraphW {
13182                        idx,
13183                        kind,
13184                        row_scale: rs,
13185                        data: &[],
13186                        prism: crate::gpu::GraphPrismOp::None,
13187                        affine: false,
13188                    });
13189                }
13190                t.as_f32().map(|data| crate::gpu::GraphW {
13191                    idx: 0,
13192                    kind: 4,
13193                    row_scale: &[],
13194                    data,
13195                    prism: crate::gpu::GraphPrismOp::None,
13196                    affine: false,
13197                })
13198            }
13199            let (Some(q), Some(k), Some(v), Some(o)) = (
13200                gw(wq, &model),
13201                gw(wk, &model),
13202                gw(wv, &model),
13203                gw(wo, &model),
13204            ) else {
13205                return Out::Declined;
13206            };
13207            let layer = crate::gpu::GraphLayer {
13208                input_norm: &lw.input_norm,
13209                post_norm: &lw.post_norm,
13210                attn_out_norm: None,
13211                ffn_out_norm: None,
13212                layer_scale: None,
13213                ffn: crate::gpu::GraphFfn::AttentionOnly,
13214                attn: crate::gpu::GraphAttn::Full {
13215                    wq: q,
13216                    wk: k,
13217                    wv: v,
13218                    wo: o,
13219                    q_norm: q_norm.as_deref(),
13220                    k_norm: k_norm.as_deref(),
13221                    late_qk_norm: self.qk_norm_after_rope,
13222                    bias: bias
13223                        .as_ref()
13224                        .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
13225                    output_gate: false,
13226                    cpu_k: self.kv_cache.layers[li].k_heads(),
13227                    cpu_v: self.kv_cache.layers[li].v_heads(),
13228                    cpu_base: self.kv_cache.layers[li].base(),
13229                    geom: self.graph_attn_geom(li),
13230                    head_gate: None,
13231                },
13232            };
13233            let (nkv, hd, rd) = self.layer_geom(li);
13234            crate::gpu::forward_batch_graph_at(
13235                &model,
13236                self.graph_kv_id,
13237                li,
13238                &[layer],
13239                &self.inv_freq,
13240                h,
13241                self.layer_num_heads(li),
13242                nkv,
13243                hd,
13244                rd,
13245                self.hidden_size,
13246                1,
13247                positions,
13248                self.kv_cache.max_seq_len,
13249                self.norm_style == cortiq_core::NormStyle::Gemma,
13250                self.rms_eps as f32,
13251                self.attn_scale,
13252                b,
13253                &[],
13254                self.o1_epoch,
13255                None,
13256                None,
13257            )
13258        };
13259        match outcome {
13260            Out::Completed => {}
13261            Out::Declined => return Out::Declined,
13262            Out::Failed => {
13263                self.graph_failed
13264                    .store(true, std::sync::atomic::Ordering::Relaxed);
13265                return Out::Failed;
13266            }
13267        }
13268        let attn_ns = attn_started.elapsed().as_nanos() as u64;
13269        let hs = self.hidden_size;
13270        let lw = &self.weights.layers[li];
13271        let FfnKind::Moe(m) = &lw.ffn else {
13272            unreachable!()
13273        };
13274        let mut post = vec![0.0; h.len()];
13275        for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
13276            inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
13277        }
13278        let mut ffn = if b == 1 {
13279            moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
13280        } else {
13281            moe_ffn_banked_rows(
13282                &mut self.mimo_moe,
13283                li,
13284                m,
13285                &post,
13286                b,
13287                hs,
13288                self.pool.as_deref(),
13289            )
13290        };
13291        for (x, &f) in h.iter_mut().zip(&ffn) {
13292            *x += f;
13293        }
13294        attention::recycle_buf(&mut ffn);
13295        if self.layer_dump.is_some() {
13296            for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
13297                self.dump_layer_row(pos, li, row);
13298            }
13299        }
13300        crate::mimo_moe::note_attention_graph(b, attn_ns);
13301        Out::Completed
13302    }
13303
13304    fn layer_attn_plain(&self, li: usize) -> bool {
13305        self.kv_heads_per_layer.is_none()
13306            && self.v_head_dim.is_none()
13307            && self.global_attn.is_none()
13308            && self.layer_window(li).is_none()
13309            && self.kv_cache.layers[li].sinks.is_none()
13310    }
13311
13312    /// Forward one position through all layers (hybrid dispatch).
13313    fn forward_layers(
13314        &mut self,
13315        hidden: &[f32],
13316        position: usize,
13317        task_mask: Option<&TaskMask>,
13318    ) -> Vec<f32> {
13319        let out = self.forward_layers_upto(hidden, position, task_mask, None);
13320        self.o1_progress();
13321        self.swa_trim_tails();
13322        out
13323    }
13324
13325    // ── Network pipeline-split building blocks (coordinator/worker) ──
13326    // A remote worker owns layers [from ..= upto] and their KV; the
13327    // coordinator owns the rest plus embed / final norm / head. Attention
13328    // causality is per-layer, so a whole prompt's boundary hiddens ship
13329    // as one batch and decode ships one vector per token.
13330
13331    /// Embed one token id (embed multiplier applied).
13332    pub fn embed_id(&self, id: u32) -> Vec<f32> {
13333        self.embed_single(id)
13334    }
13335
13336    /// Refuse the archs/modes whose forward cannot be cut at a layer
13337    /// boundary. Loud by design: a split that silently changed the math
13338    /// would be a chimera.
13339    pub fn split_supported(&self) -> Result<(), String> {
13340        if self.dsv4.is_some() {
13341            return Err(
13342                "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
13343            );
13344        }
13345        if self.dsv41.is_some() {
13346            return Err(
13347                "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
13348                    .into(),
13349            );
13350        }
13351        if self.qwen4_exp.is_some() {
13352            return Err(
13353                "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
13354            );
13355        }
13356        if self.per_token_stack() {
13357            return Err(
13358                "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
13359            );
13360        }
13361        Ok(())
13362    }
13363
13364    /// Forward `hidden` through layers [from ..= upto] at `position`,
13365    /// appending those layers' KV/state. Both split sides call this
13366    /// over their own range; a task mask applies to the span's own
13367    /// layers (each side masks what it runs).
13368    pub fn forward_span(
13369        &mut self,
13370        hidden: &[f32],
13371        position: usize,
13372        from: usize,
13373        upto: usize,
13374        task_mask: Option<&TaskMask>,
13375    ) -> Result<Vec<f32>, String> {
13376        self.split_supported()?;
13377        if from > upto || upto >= self.num_layers {
13378            return Err(format!(
13379                "forward_span: layer range {from}..={upto} outside 0..{}",
13380                self.num_layers
13381            ));
13382        }
13383        if hidden.len() != self.hidden_size {
13384            return Err(format!(
13385                "forward_span: hidden len {} ≠ hidden_size {}",
13386                hidden.len(),
13387                self.hidden_size
13388            ));
13389        }
13390        let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
13391        self.o1_progress();
13392        self.swa_trim_tails();
13393        if self
13394            .graph_failed
13395            .swap(false, std::sync::atomic::Ordering::Relaxed)
13396        {
13397            self.cancel
13398                .store(false, std::sync::atomic::Ordering::Relaxed);
13399            self.clear_sequence_state();
13400            return Err("forward_span: deferred O(1) transition failed".into());
13401        }
13402        Ok(out)
13403    }
13404
13405    /// Final norm + lm_head over a boundary hidden (the final-logit
13406    /// softcap is applied by lm_head_forward itself).
13407    pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
13408        let normed = inference::rms_norm(
13409            hidden,
13410            &self.weights.final_norm,
13411            self.rms_eps,
13412            self.norm_style,
13413        );
13414        self.lm_head_forward(&normed)
13415    }
13416
13417    /// Sample the next token with this pipeline's sampler state.
13418    pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
13419        sampler::sample_with_scratch(
13420            logits,
13421            &self.sampler_config,
13422            past_tokens,
13423            &mut self.rng,
13424            &mut self.sampler_scratch,
13425        )
13426    }
13427
13428    /// Fresh sequence: clear KV, reuse history and device mirrors.
13429    pub fn reset_session(&mut self) {
13430        self.clear_sequence_state();
13431    }
13432
13433    /// Batched span prefill from token ids (coordinator side): embed +
13434    /// layers [0 ..= upto]; returns the boundary hiddens of ALL positions
13435    /// (ids.len() × hidden). Rides the same layer-major machinery as the
13436    /// local prefill; falls back to the per-position walk under
13437    /// CMF_PREFILL=seq.
13438    pub fn prefill_span_ids(
13439        &mut self,
13440        ids: &[u32],
13441        start_pos: usize,
13442        upto: usize,
13443        task_mask: Option<&TaskMask>,
13444    ) -> Result<Vec<f32>, String> {
13445        self.split_supported()?;
13446        if upto >= self.num_layers {
13447            return Err(format!(
13448                "prefill_span_ids: upto {upto} outside 0..{}",
13449                self.num_layers
13450            ));
13451        }
13452        // Same predicate as the whole-stack prefill: a span whose GDN
13453        // state lives on the device must walk positions through the
13454        // graph, not through the batched CPU span.
13455        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13456            let out =
13457                self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
13458            self.check_o1_progress_failure("prefill_span_ids")?;
13459            Ok(out)
13460        } else {
13461            let hs = self.hidden_size;
13462            let mut out = Vec::with_capacity(ids.len() * hs);
13463            for (i, &id) in ids.iter().enumerate() {
13464                let emb = self.embed_id(id);
13465                out.extend_from_slice(&self.forward_span(
13466                    &emb,
13467                    start_pos + i,
13468                    0,
13469                    upto,
13470                    task_mask,
13471                )?);
13472            }
13473            Ok(out)
13474        }
13475    }
13476
13477    /// Batched span prefill from boundary hiddens (worker side): layers
13478    /// [from ..= upto] for every position in the batch; returns the batch.
13479    pub fn prefill_span_hidden(
13480        &mut self,
13481        hidden: &[f32],
13482        start_pos: usize,
13483        from: usize,
13484        upto: usize,
13485        task_mask: Option<&TaskMask>,
13486    ) -> Result<Vec<f32>, String> {
13487        self.split_supported()?;
13488        let hs = self.hidden_size;
13489        if hidden.is_empty() || hidden.len() % hs != 0 {
13490            return Err(format!(
13491                "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
13492                hidden.len()
13493            ));
13494        }
13495        if from > upto || upto >= self.num_layers {
13496            return Err(format!(
13497                "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
13498                self.num_layers
13499            ));
13500        }
13501        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13502            let out = self.prefill_batch_span(
13503                PrefillIn::Hidden(hidden),
13504                start_pos,
13505                task_mask,
13506                from,
13507                upto + 1,
13508            );
13509            self.check_o1_progress_failure("prefill_span_hidden")?;
13510            Ok(out)
13511        } else {
13512            let b = hidden.len() / hs;
13513            let mut out = Vec::with_capacity(hidden.len());
13514            for i in 0..b {
13515                let h = self.forward_span(
13516                    &hidden[i * hs..(i + 1) * hs],
13517                    start_pos + i,
13518                    from,
13519                    upto,
13520                    task_mask,
13521                )?;
13522                out.extend_from_slice(&h);
13523            }
13524            Ok(out)
13525        }
13526    }
13527
13528    /// Build the whole-token wgpu graph for a pure-attention q1 model (every
13529    /// layer Full q1 + dense q1 FFN, no gate/bias). Returns the post-stack
13530    /// hidden (caller does final norm + lm_head), or None to fall back.
13531    fn try_token_graph_wgpu(
13532        &self,
13533        hidden: &[f32],
13534        position: usize,
13535        logits_out: &mut Vec<f32>,
13536        layers_run: &mut usize,
13537    ) -> Option<Result<Vec<f32>, ()>> {
13538        self.try_token_graph_wgpu_steps(
13539            hidden,
13540            position,
13541            logits_out,
13542            1,
13543            None,
13544            Some(layers_run),
13545            0,
13546            self.num_layers,
13547        )
13548    }
13549
13550    /// The span twin (network split): the graph covers [from..upto_excl)
13551    /// — one submit per SEGMENT per token. lm_head folds in only when
13552    /// the span reaches the last layer.
13553    fn try_token_graph_wgpu_span(
13554        &self,
13555        hidden: &[f32],
13556        position: usize,
13557        logits_out: &mut Vec<f32>,
13558        from: usize,
13559        upto_excl: usize,
13560        layers_run: &mut usize,
13561    ) -> Option<Result<Vec<f32>, ()>> {
13562        self.try_token_graph_wgpu_steps(
13563            hidden,
13564            position,
13565            logits_out,
13566            1,
13567            None,
13568            Some(layers_run),
13569            from,
13570            upto_excl,
13571        )
13572    }
13573
13574    /// The pooled tail's device lm_head: final-norm + head as one
13575    /// layer-less graph pass, so decode stops paying the double-digit
13576    /// millisecond host matvec every token. `None` keeps the host head.
13577    #[cfg(feature = "gpu")]
13578    fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
13579        if !crate::minicpm_tail::minimax_pool_on()
13580            || !crate::gpu::enabled_here()
13581            || self.head_clusters.is_some()
13582            || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
13583            || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
13584        {
13585            return None;
13586        }
13587        let (model, i, kind, rs) = self
13588            .weights
13589            .lm_head
13590            .graph_weight()
13591            .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
13592        let name = &model.tensors[i].name;
13593        let prism = if crate::prism::is_inverse_embedding(&model, name) {
13594            crate::gpu::GraphPrismOp::InverseEmbedding
13595        } else if crate::prism::is_forward_weight(&model, name) {
13596            crate::gpu::GraphPrismOp::Forward
13597        } else {
13598            crate::gpu::GraphPrismOp::None
13599        };
13600        let gw = crate::gpu::GraphW {
13601            idx: i,
13602            kind,
13603            row_scale: rs,
13604            data: &[],
13605            prism,
13606            affine: crate::prism::is_affine_target(&model, name),
13607        };
13608        let rows = self.weights.lm_head.rows();
13609        let mut h = hidden.to_vec();
13610        let mut lg = Vec::new();
13611        let mut layers_run = 0usize;
13612        let outcome = crate::gpu::forward_token_graph(
13613            &model,
13614            self.graph_kv_id,
13615            &[],
13616            &[],
13617            self.o1_epoch,
13618            &self.inv_freq,
13619            &mut h,
13620            self.num_heads,
13621            self.num_kv_heads,
13622            self.head_dim,
13623            self.attn_scale,
13624            self.head_dim,
13625            self.hidden_size,
13626            self.intermediate_size,
13627            0,
13628            self.kv_cache.max_seq_len,
13629            self.norm_style == cortiq_core::NormStyle::Gemma,
13630            self.rms_eps as f32,
13631            Some((&gw, rows)),
13632            &self.weights.final_norm,
13633            &mut lg,
13634            &[],
13635            1,
13636            None,
13637            None,
13638            Some(&mut layers_run),
13639            0,
13640            false,
13641        );
13642        if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
13643            return None;
13644        }
13645        // The same post-steps the host head applies — keep sampling
13646        // identical whichever arm produced the logits.
13647        lg.resize(self.vocab_size, 0.0);
13648        if let Some(m) = self.logit_multiplier {
13649            for l in lg.iter_mut() {
13650                *l *= m;
13651            }
13652        }
13653        if let Some(c) = self.final_softcap {
13654            softcap_in_place(&mut lg, c, self.pool.as_deref());
13655        }
13656        Some(lg)
13657    }
13658
13659    /// Record one token-layer's winners into the route-frequency profile and
13660    /// write it out every 16 tokens when `CMF_M3_PROFILE_SAVE=<path>` is set.
13661    fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
13662        if winners.is_empty() {
13663            return;
13664        }
13665        if self.m3_n_exp == 0 {
13666            self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
13667            self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
13668        }
13669        for &e in winners {
13670            let e = e as usize;
13671            if e < self.m3_n_exp && li < self.num_layers {
13672                self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
13673            }
13674        }
13675        if li + 1 == self.num_layers {
13676            self.m3_tokens += 1;
13677            if self.m3_tokens % 16 == 0 {
13678                if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
13679                    let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
13680                    let _ = std::fs::write(&path, bytes);
13681                }
13682            }
13683        }
13684    }
13685
13686    /// Admit the most frequent (layer, expert) pairs of a saved profile into
13687    /// the bank before the first token: `CMF_M3_PROFILE=<path>` names the
13688    /// file, `CMF_M3_PREWARM_N` (default 300) bounds how many experts go in.
13689    /// The bank's own eviction stays in charge; prewarm only chooses order.
13690    fn m3_prewarm(&mut self) {
13691        let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
13692            return;
13693        };
13694        let Ok(bytes) = std::fs::read(&path) else {
13695            tracing::warn!("m3 prewarm: cannot read profile {path}");
13696            return;
13697        };
13698        let counts: Vec<u32> = bytes
13699            .chunks_exact(4)
13700            .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
13701            .collect();
13702        let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
13703        if counts.len() != self.num_layers * n_exp {
13704            tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
13705            return;
13706        }
13707        let limit: usize = std::env::var("CMF_M3_PREWARM_N")
13708            .ok()
13709            .and_then(|v| v.parse().ok())
13710            .unwrap_or(300);
13711        let mut ranked: Vec<(u32, usize, usize)> = counts
13712            .iter()
13713            .enumerate()
13714            .filter(|(_, c)| **c > 0)
13715            .map(|(i, &c)| (c, i / n_exp, i % n_exp))
13716            .collect();
13717        ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
13718        ranked.truncate(limit);
13719        let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
13720        for (_, li, e) in ranked {
13721            per_layer[li].push(e);
13722        }
13723        let mut admitted = 0usize;
13724        for (li, ids) in per_layer.iter().enumerate() {
13725            if ids.is_empty() {
13726                continue;
13727            }
13728            let lw = &self.weights.layers[self.phys_layer(li)];
13729            let FfnKind::Moe(m) = &lw.ffn else {
13730                continue;
13731            };
13732            // Arming installs the layer's layout; nothing predicted yet.
13733            let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
13734            if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
13735                admitted += ids.len();
13736            }
13737        }
13738        tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
13739    }
13740
13741    /// One MiniMax tail layer through the pooled token-graph arm: attention,
13742    /// the pre-FFN norm, device routing and the descriptor-indexed expert
13743    /// bank in ONE submit (one sync instead of host attention + a MoE
13744    /// frame), with cold experts finished on the host against the read-back
13745    /// FFN input and admitted for the next token.
13746    ///
13747    /// `Ok(true)` — the layer is done (hidden replaced). `Ok(false)` —
13748    /// decline before any device mutation; the ordinary host path owns this
13749    /// position. `Err(())` — the device failed after admission; the caller
13750    /// must stop the sequence exactly like a failed span run.
13751    #[cfg(feature = "gpu")]
13752    fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
13753        if self.o1_active() || self.attn_softcap > 0.0 {
13754            return Ok(false);
13755        }
13756        if !self.m3_prewarmed {
13757            self.m3_prewarmed = true;
13758            self.m3_prewarm();
13759        }
13760        let frame = {
13761            let lw = &self.weights.layers[self.phys_layer(li)];
13762            let FfnKind::Moe(m) = &lw.ffn else {
13763                return Ok(false);
13764            };
13765            if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
13766                return Ok(false);
13767            }
13768            let t_arm = std::time::Instant::now();
13769            if self.m3_winners.len() != self.num_layers {
13770                self.m3_winners = vec![Vec::new(); self.num_layers];
13771            }
13772            let pred = std::mem::take(&mut self.m3_winners[li]);
13773            let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
13774            if armed.is_none() {
13775                self.m3_winners[li] = pred; // declined before anything ran
13776            }
13777            M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
13778            match armed {
13779                Some(frame) => frame,
13780                // First-call refusal (geometry/bank): the host layer still
13781                // runs; nothing was mutated.
13782                None => return Ok(false),
13783            }
13784        };
13785        crate::gpu_wgpu::pooled_frame_enter(frame);
13786        // The pooled call must not fold lm_head: its cold experts have to
13787        // land before any final norm. The decode loop re-arms this flag for
13788        // the next position anyway.
13789        let want_logits = self.graph_want_logits;
13790        self.graph_want_logits = false;
13791        let mut lg = Vec::new();
13792        let mut gl = 0usize;
13793        let t_span = std::time::Instant::now();
13794        let span = crate::gpu::gpu_scope(|| {
13795            self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
13796        });
13797        self.graph_want_logits = want_logits;
13798        M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
13799        let hh = match span {
13800            Some(Ok(hh)) => hh,
13801            Some(Err(())) => return Err(()),
13802            None => {
13803                // Declined before submission (seed/resolve/refusal): the
13804                // device mirror has not moved, so the host path is exact.
13805                return Ok(false);
13806            }
13807        };
13808        if gl != 1 {
13809            // A single-layer run that reports no layer mutated nothing we
13810            // may build on; anything else is a contract break.
13811            return if gl == 0 { Ok(false) } else { Err(()) };
13812        }
13813        *h = hh;
13814        let winners = crate::gpu_wgpu::pooled_frame_take_winners();
13815        self.m3_note_route(li, &winners);
13816        if self.m3_winners.len() != self.num_layers {
13817            self.m3_winners = vec![Vec::new(); self.num_layers];
13818        }
13819        self.m3_winners[li] = winners;
13820        let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
13821        // A non-finite parsed weight would poison the residual for the rest
13822        // of the sequence — drop such entries loudly instead.
13823        cold.retain(|&(_, w)| {
13824            if w.is_finite() {
13825                true
13826            } else {
13827                tracing::error!("pooled tail: non-finite cold weight from expert route");
13828                false
13829            }
13830        });
13831        if cold.is_empty() {
13832            return Ok(true);
13833        }
13834        let x = crate::gpu_wgpu::pooled_frame_take_input();
13835        if x.len() != self.hidden_size {
13836            return Err(());
13837        }
13838        // Cold experts: on the device first (admit, then one prepared frame
13839        // over exactly those slots — one submit for the layer). The host
13840        // computes them only when the device declines, and then they are
13841        // admitted for the next token. `CMF_M3_COLD_CPU=1` forces the host
13842        // arm for an A/B measurement.
13843        let lw = &self.weights.layers[self.phys_layer(li)];
13844        let FfnKind::Moe(m) = &lw.ffn else {
13845            return Err(());
13846        };
13847        let t_cold = std::time::Instant::now();
13848        let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
13849        let mut out = crate::attention::take_buf(self.hidden_size);
13850        let on_device = !force_host
13851            && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
13852        if !on_device {
13853            crate::attention::recycle_buf(&mut out);
13854            let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
13855                .iter()
13856                .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
13857                .collect::<Result<_, ()>>()?;
13858            out = crate::gpu::cpu_scope(|| {
13859                crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
13860            });
13861            let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
13862            let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
13863        }
13864        M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
13865        M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
13866        if out.len() != h.len() {
13867            crate::attention::recycle_buf(&mut out);
13868            return Err(());
13869        }
13870        for (dst, src) in h.iter_mut().zip(out.iter()) {
13871            *dst += src;
13872        }
13873        crate::attention::recycle_buf(&mut out);
13874        if on_device {
13875            M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
13876        }
13877        Ok(true)
13878    }
13879
13880    #[cfg(not(feature = "gpu"))]
13881    fn m3_pooled_layer(
13882        &mut self,
13883        _li: usize,
13884        _position: usize,
13885        _h: &mut Vec<f32>,
13886    ) -> Result<bool, ()> {
13887        Ok(false)
13888    }
13889
13890    /// Greedy burst: forward `t_next` and let the device pick + re-embed
13891    /// the next k−1 tokens — k frames, ONE submit, k ids back. The ZML
13892    /// trade, on wgpu. None ⇒ caller keeps the per-token path.
13893    fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
13894        if self.o1_active() || self.attn_softcap > 0.0 {
13895            return None;
13896        }
13897        // The burst builds the whole-token graph; attention the graph's
13898        // per-layer geometry cannot express keeps the per-token path.
13899        if let Some(reason) = self.wgpu_graph_attn_decline() {
13900            self.note_graph_decline("wgpu multi-burst", reason);
13901            return None;
13902        }
13903        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
13904        if !graph_on || self.graph_refused() {
13905            // Same memo as the decode site: this path builds the very
13906            // same graph, so a model it cannot build for must not be
13907            // walked again here either. Missing this guard was worth
13908            // 2.5x on an Adreno — 0.361 tok/s against 0.905 — because
13909            // the burst retried per token what decode had already given
13910            // up on.
13911            return None;
13912        }
13913        let emb = self.embed_single(t_next);
13914        let mut lg = Vec::new();
13915        let mut ids = Vec::new();
13916        match self.try_token_graph_wgpu_steps(
13917            &emb,
13918            position,
13919            &mut lg,
13920            k,
13921            Some(&mut ids),
13922            None,
13923            0,
13924            self.num_layers,
13925        ) {
13926            Some(Ok(_)) => {}
13927            Some(Err(())) => {
13928                // Preserve the backend's post-admission failure through the
13929                // Option-based burst API.  The decode caller consumes this
13930                // flag and clears the sequence instead of falling through
13931                // to a stale CPU recurrent state.
13932                self.graph_failed
13933                    .store(true, std::sync::atomic::Ordering::Relaxed);
13934                return None;
13935            }
13936            None => return None,
13937        }
13938        (ids.len() == k).then_some(ids)
13939    }
13940
13941    /// Multi-step greedy: k whole frames in ONE submit, argmax and re-embed
13942    /// on the device. `ids_out` receives the k winner ids; the hidden/logits
13943    /// outputs are NOT produced in that mode.
13944    fn try_token_graph_wgpu_steps(
13945        &self,
13946        hidden: &[f32],
13947        position: usize,
13948        logits_out: &mut Vec<f32>,
13949        steps: usize,
13950        ids_out: Option<&mut Vec<u32>>,
13951        layers_run: Option<&mut usize>,
13952        from: usize,
13953        upto_excl: usize,
13954    ) -> Option<Result<Vec<f32>, ()>> {
13955        // The bank has already reserved its VRAM. Never build a second
13956        // all-expert arena across bank-owned layers (including bursts).
13957        let upto_excl = match self.mimo_moe.graph_prefix_end() {
13958            Some(end) if end < upto_excl => {
13959                if steps != 1 || layers_run.is_none() || from >= end {
13960                    return None;
13961                }
13962                end
13963            }
13964            _ => upto_excl,
13965        };
13966        // O(1) Nyström decode runs off the sealed state, not the KV cache the
13967        // graph mirrors — never take the graph while o1 is active.
13968        let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
13969        if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
13970            // Softcapped scores have no graph kernel yet — CPU owns them.
13971            // o1 rides the graph only behind CMF_O1_GPU=1 while the port
13972            // proves itself; without it the CPU path owns o1 as before.
13973            return None;
13974        }
13975        // Per-layer KV heads, narrow V, sinks and sliding windows ride
13976        // `GraphAttn::Full::geom` (the ATTEND_X kernels). Anything that
13977        // geometry cannot express declines here, by name — before the
13978        // per-layer gate existed a sliding/sink model ran the graph as
13979        // full-context attention, fluent and wrong. The caller memoizes
13980        // the refusal.
13981        if let Some(reason) = self.wgpu_graph_attn_decline() {
13982            self.note_graph_decline("wgpu token graph", reason);
13983            return None;
13984        }
13985        // Per-layer sealed o1 state for the graph. During prefill the
13986        // state is still Collecting -> views are None -> the graph
13987        // refuses below and the CPU prefill records the q trace and
13988        // seals, exactly as the o1 design requires.
13989        let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
13990            .map(|li| {
13991                if !o1_gpu {
13992                    return None;
13993                }
13994                self.kv_cache.layers[self.phys_layer(li)].o1_views()
13995            })
13996            .collect();
13997        if self.o1_active() && o1_gpu {
13998            // Any o1 layer not sealed (or degenerate exact-only) keeps the
13999            // whole token on the CPU: half-graph forwards would desync.
14000            let want: usize = (from..upto_excl)
14001                .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
14002                .count();
14003            let have = o1_views.iter().filter(|v| v.is_some()).count();
14004            if want == 0 || have != want {
14005                // The silent twin of the gpu-side o1 gates, found the
14006                // same way: a 15x decode drop with an empty log. Views
14007                // stay None until the layer's state SEALS, so `have`
14008                // lagging `want` early in a run is the o1 design working
14009                // — but it must say so, or the next reader spends a
14010                // night proving the kernels innocent.
14011                // On CHANGE, not once: the first decline is the legal
14012                // unsealed prefill, and a once-print buries the state
14013                // that matters — what the count reads AFTER the seal.
14014                use std::sync::atomic::{AtomicUsize, Ordering};
14015                static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
14016                let code = have * 1000 + want;
14017                if LAST.swap(code, Ordering::Relaxed) != code {
14018                    tracing::warn!(
14019                        "o1 graph: {have} of {want} layers sealed — per-op until all seal"
14020                    );
14021                }
14022                return None;
14023            }
14024        }
14025        let nh = self.num_heads;
14026        let (nkv, hd, rd) = self.layer_geom(0);
14027        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14028        let mut layers = Vec::with_capacity(upto_excl - from);
14029        let mut model = None;
14030        let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
14031        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14032            // `blr` rides as GRAPH_KIND_BLR (two stage dispatches).
14033            if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
14034                let name = &m.tensors[i].name;
14035                let prism = if crate::prism::is_inverse_embedding(m, name) {
14036                    crate::gpu::GraphPrismOp::InverseEmbedding
14037                } else if crate::prism::is_forward_weight(m, name) {
14038                    crate::gpu::GraphPrismOp::Forward
14039                } else {
14040                    crate::gpu::GraphPrismOp::None
14041                };
14042                return Some(crate::gpu::GraphW {
14043                    idx: i,
14044                    kind,
14045                    row_scale: rs,
14046                    data: &[],
14047                    prism,
14048                    affine: crate::prism::is_affine_target(m, name),
14049                });
14050            }
14051            // Small unquantized projections (GDN in_proj_a/b) stay f32.
14052            match t.as_f32() {
14053                Some(d) => Some(crate::gpu::GraphW {
14054                    idx: 0,
14055                    kind: 4,
14056                    row_scale: &[],
14057                    data: d,
14058                    prism: crate::gpu::GraphPrismOp::None,
14059                    affine: false,
14060                }),
14061                None => {
14062                    if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14063                        eprintln!("batch graph: weight has no graph/f32 representation");
14064                    }
14065                    None
14066                }
14067            }
14068        }
14069        for li in from..upto_excl {
14070            let lw = &self.weights.layers[self.phys_layer(li)];
14071            if dbg {
14072                let ak = match &lw.attn {
14073                    AttnKind::Mla(_) => "Mla".into(),
14074                    AttnKind::Full {
14075                        output_gate, bias, ..
14076                    } => format!("Full gate={output_gate} bias={}", bias.is_some()),
14077                    AttnKind::LinearGdn(_) => "LinearGdn".into(),
14078                    AttnKind::Kda(_) => "Kda".into(),
14079                    AttnKind::Linear(_) => "Linear".into(),
14080                    AttnKind::ShortConv(_) => "ShortConv".into(),
14081                    AttnKind::Bounded(_) => "Bounded".into(),
14082                };
14083                let fk = match &lw.ffn {
14084                    FfnKind::Dense(_) => "Dense",
14085                    FfnKind::Moe(_) => "Moe",
14086                    FfnKind::DenseMoe(_) => "DenseMoe",
14087                };
14088                eprintln!("graph L{li}: attn={ak} ffn={fk}");
14089            }
14090            let gffn = match &lw.ffn {
14091                FfnKind::DenseMoe(_) => return None, // dual branch: CPU path
14092                // A tube layer is several matrices, not one — the
14093                // whole-layer graph has no shape for it yet.
14094                FfnKind::Dense(d) if !d.segs.is_empty() => return None,
14095                FfnKind::Dense(d) => {
14096                    // An activation the graph has no kernel arm for keeps
14097                    // the CPU/per-op owner, by name — the dense graph FFN
14098                    // used to compute SiLU for whatever the model asked.
14099                    let Some(act) = d.act.wgpu_graph_act() else {
14100                        self.note_graph_decline(
14101                            "wgpu token graph",
14102                            "dense FFN activation without a graph kernel",
14103                        );
14104                        return None;
14105                    };
14106                    crate::gpu::GraphFfn::Dense {
14107                        gate: gw(&d.gate_proj)?,
14108                        up: gw(&d.up_proj)?,
14109                        down: gw(&d.down_proj)?,
14110                        act,
14111                    }
14112                }
14113                FfnKind::Moe(m) => {
14114                    // Adaptive τ and expert masks keep the CPU path, where
14115                    // they are implemented. Sigmoid routing with a selection
14116                    // bias (LFM2-MoE / DeepSeek noaux_tc), a routed scale ≠ 1
14117                    // and an UNGATED shared expert (HunYuan hy_v3: ×2.826 on
14118                    // the routed mix, the shared expert at weight 1) are all
14119                    // graphed — before, every such token fell to the per-op
14120                    // path whole (145 submits/token on Hy-MT2-30B-A3B).
14121                    if m.route_tau.is_some() || m.mask.is_some() {
14122                        return None;
14123                    }
14124                    let shared = m.shared.as_ref();
14125                    // The resident MoE kernels do not yet carry the
14126                    // descriptor-aware transform through router/shared-gate
14127                    // selection.  Refuse the complete layer instead of
14128                    // scoring with an untransformed Prism plane (the dense
14129                    // path has an explicit FWHT boundary below).
14130                    let inter = m.experts.first()?.gate_proj.rows();
14131                    // Flint's shared expert is a 1024-wide SwiGLU while its
14132                    // routed bank is 512-wide.  The resident MoE shader
14133                    // indexes a uniform-width packed bank, so keep that
14134                    // expert out of the routed slots and add it through the
14135                    // dense q4tp path after routed accumulation.  This is
14136                    // also the safe route for a mixed q2-routed/q4-shared
14137                    // artifact: neither layout nor row stride is faked.
14138                    let detached_shared = match shared {
14139                        Some((se, None))
14140                            if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14141                        {
14142                            if !matches!(se.act, Act::Silu)
14143                                || se.gate_proj.mapped_q4tp().is_none()
14144                                || se.up_proj.mapped_q4tp().is_none()
14145                                || se.down_proj.mapped_q4tp().is_none()
14146                            {
14147                                return None;
14148                            }
14149                            Some(crate::gpu::GraphMoeDetachedShared {
14150                                gate: gw(&se.gate_proj)?,
14151                                up: gw(&se.up_proj)?,
14152                                down: gw(&se.down_proj)?,
14153                                width: se.gate_proj.rows(),
14154                            })
14155                        }
14156                        // A separately shaped gated shared expert needs a
14157                        // second sigmoid gate path.  Do not turn it into an
14158                        // ungated one merely to make the graph run.
14159                        Some((se, Some(_)))
14160                            if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14161                        {
14162                            return None;
14163                        }
14164                        _ => None,
14165                    };
14166                    let has_shared = shared.is_some() && detached_shared.is_none();
14167                    let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
14168                    let sgate = match shared {
14169                        Some((_, Some(sg))) if has_shared => gw(sg)?,
14170                        // No gate, no shared expert, or a detached shared
14171                        // expert: the router only keeps the binding total;
14172                        // the selection shader does not read it.
14173                        _ => gw(&m.router)?,
14174                    };
14175                    let router = gw(&m.router)?;
14176                    // The resident MoE kernels do not yet carry the
14177                    // descriptor-aware transform through router/shared-gate
14178                    // selection.  Refuse the complete layer instead of
14179                    // scoring with an untransformed Prism plane (the dense
14180                    // path has an explicit FWHT boundary below).
14181                    if router.prism != crate::gpu::GraphPrismOp::None
14182                        || sgate.prism != crate::gpu::GraphPrismOp::None
14183                        || router.affine
14184                        || sgate.affine
14185                    {
14186                        tracing::warn!(
14187                            "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
14188                        );
14189                        return None;
14190                    }
14191                    let in_bank_shared = shared.filter(|_| has_shared).map(|(se, _)| se);
14192                    let edge = |t: &QTensor| {
14193                        t.graph_weight()
14194                            .or_else(|| t.graph_weight_descriptor())
14195                            .map(|(mm, i, _, _)| (std::sync::Arc::as_ptr(mm) as usize, i))
14196                    };
14197                    let bank_key: Option<GraphMoeKey> = match (
14198                        m.experts.first().and_then(|e| edge(&e.gate_proj)),
14199                        m.experts.last().and_then(|e| edge(&e.down_proj)),
14200                    ) {
14201                        (Some((fm, fi)), Some((_, di))) if graph_moe_bank_on() => Some((
14202                            self.phys_layer(li),
14203                            m.experts.as_ptr() as usize,
14204                            m.experts.len(),
14205                            in_bank_shared.map_or(0, |se| std::ptr::from_ref(se) as usize),
14206                            fm,
14207                            fi,
14208                            di,
14209                        )),
14210                        _ => None,
14211                    };
14212                    let cached =
14213                        bank_key.and_then(|k| self.graph_moe_banks.lock().unwrap().get(&k).cloned());
14214                    let bank = if let Some(b) = cached {
14215                        b
14216                    } else {
14217                    let mut experts = Vec::with_capacity(m.experts.len() + 1);
14218                    let mut bank_model: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14219                    // q4t or q4tp, but not both in one layer — the kernels
14220                    // are picked per layer, not per expert.
14221                    let mut q4tp: Option<bool> = None;
14222                    // The mixed 2-bit profile: q2tp gate/up over a q4tp
14223                    // down. Uniform across the layer, like `q4tp` itself.
14224                    let mut gu_q2: Option<bool> = None;
14225                    // q8_2f experts (gate, up and down): their own kernels.
14226                    let mut q82: Option<bool> = None;
14227                    // One MoE gate/up kernel serves the complete packed bank.
14228                    // Bind an activation only when every routed (and in-bank
14229                    // shared) expert owns the same exact WGPU contract.
14230                    let mut moe_act = None;
14231                    for e in m.experts.iter().chain(in_bank_shared.into_iter()) {
14232                        let Some(e_act) = e.act.wgpu_graph_act() else {
14233                            return None;
14234                        };
14235                        match moe_act {
14236                            Some(existing) if existing != e_act => return None,
14237                            None => moe_act = Some(e_act),
14238                            _ => {}
14239                        }
14240                        if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
14241                            return None;
14242                        }
14243                        // Expert tensors are packed into one resident buffer
14244                        // and the MoE kernels have no transform slot per
14245                        // expert.  Keep the CPU/per-op owner for Prism or
14246                        // affine experts rather than silently using raw bytes.
14247                        for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
14248                            let Some((em, ei, _, _)) = expert_weight
14249                                .graph_weight()
14250                                .or_else(|| expert_weight.graph_weight_descriptor())
14251                            else {
14252                                return None;
14253                            };
14254                            let name = &em.tensors[ei].name;
14255                            if crate::prism::is_forward_weight(em, name)
14256                                || crate::prism::is_inverse_embedding(em, name)
14257                                || crate::prism::is_affine_target(em, name)
14258                            {
14259                                tracing::warn!(
14260                                    "resident MoE declined: expert Prism/affine transform is not implemented"
14261                                );
14262                                return None;
14263                            }
14264                        }
14265                        let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
14266                            Some((mm, gi)) => (
14267                                mm,
14268                                gi,
14269                                e.up_proj.mapped_q4t()?.1,
14270                                e.down_proj.mapped_q4t()?.1,
14271                                false,
14272                                false,
14273                                false,
14274                            ),
14275                            None => match e.gate_proj.mapped_q2tp() {
14276                                Some((mm, gi)) => (
14277                                    mm,
14278                                    gi,
14279                                    e.up_proj.mapped_q2tp()?.1,
14280                                    e.down_proj.mapped_q4tp()?.1,
14281                                    true,
14282                                    true,
14283                                    false,
14284                                ),
14285                                None => match e.gate_proj.mapped_q4tp() {
14286                                    Some((mm, gi)) => (
14287                                        mm,
14288                                        gi,
14289                                        e.up_proj.mapped_q4tp()?.1,
14290                                        e.down_proj.mapped_q4tp()?.1,
14291                                        true,
14292                                        false,
14293                                        false,
14294                                    ),
14295                                    // q8_2f experts: all three projections,
14296                                    // each with its own column field.
14297                                    None => {
14298                                        if !crate::gpu::moe_q82_graph_on() {
14299                                            return None;
14300                                        }
14301                                        let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14302                                        (
14303                                            mm,
14304                                            gi,
14305                                            e.up_proj.mapped_q8_2f()?.1,
14306                                            e.down_proj.mapped_q8_2f()?.1,
14307                                            false,
14308                                            false,
14309                                            true,
14310                                        )
14311                                    }
14312                                },
14313                            },
14314                        };
14315                        if *q4tp.get_or_insert(is_p) != is_p
14316                            || *gu_q2.get_or_insert(is_q2) != is_q2
14317                            || *q82.get_or_insert(is_8) != is_8
14318                        {
14319                            // The shared expert rides in the same packed
14320                            // buffer as the routed ones, so a layer that
14321                            // mixes layouts cannot be indexed by one stride.
14322                            // Say so: the symptom is a whole model quietly
14323                            // running its MoE on the CPU.
14324                            tracing::warn!(
14325                                "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."
14326                            );
14327                            return None;
14328                        }
14329                        bank_model.get_or_insert_with(|| mm.clone());
14330                        experts.push((gi, ui, di));
14331                    }
14332                    let b = GraphMoeBank {
14333                        experts,
14334                        q4tp,
14335                        gu_q2,
14336                        q82,
14337                        act: moe_act,
14338                        model: bank_model,
14339                    };
14340                    if let Some(k) = bank_key {
14341                        self.graph_moe_banks.lock().unwrap().insert(k, b.clone());
14342                    }
14343                    b
14344                    };
14345                    if let Some(mm) = &bank.model {
14346                        model.get_or_insert_with(|| mm.clone());
14347                    }
14348                    crate::gpu::GraphFfn::Moe {
14349                        router,
14350                        shared_gate: sgate,
14351                        experts: bank.experts,
14352                        n_exp: m.experts.len(),
14353                        // CMF_TOPK_PROBE: timing probe only — output is WRONG.
14354                        // Fewer experts shrink the MoE arithmetic while the
14355                        // dispatch count stays identical, which is the only
14356                        // clean way to tell a launch-bound decode from a
14357                        // compute-bound one.
14358                        top_k: std::env::var("CMF_TOPK_PROBE")
14359                            .ok()
14360                            .and_then(|v| v.parse::<usize>().ok())
14361                            .filter(|k| *k > 0 && *k <= m.top_k)
14362                            .unwrap_or(m.top_k),
14363                        inter,
14364                        act: bank.act?,
14365                        norm_topk: m.norm_topk_prob,
14366                        q4tp: bank.q4tp?,
14367                        gu_q2: bank.gu_q2.unwrap_or(false),
14368                        q82: bank.q82.unwrap_or(false),
14369                        sigmoid: m.router_sigmoid,
14370                        sigmoid_denom_eps: m.sigmoid_denom_eps,
14371                        bias: m.expert_bias.as_deref(),
14372                        has_shared,
14373                        detached_shared,
14374                        shared_gated,
14375                        route_scale: m.routed_scaling,
14376                    }
14377                }
14378            };
14379            let attn = match &lw.attn {
14380                AttnKind::Full {
14381                    wq,
14382                    wk,
14383                    wv,
14384                    wo,
14385                    q_norm,
14386                    k_norm,
14387                    output_gate,
14388                    softplus_gate,
14389                    bias,
14390                } => {
14391                    if self.attention_heads_per_layer.is_some() {
14392                        return None;
14393                    }
14394                    // A projected output gate rides the graph in one form:
14395                    // Spark-X2.5's head-wise sigmoid (one g_proj row per Q
14396                    // head). Laguna's softplus gate keeps the CPU path.
14397                    let head_gate = match softplus_gate {
14398                        None => None,
14399                        Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
14400                        Some(_) => {
14401                            self.note_graph_decline(
14402                                "wgpu token graph",
14403                                "projected softplus / per-element output gate",
14404                            );
14405                            return None;
14406                        }
14407                    };
14408                    let (m, _, _, _) = wq.graph_weight_or_blr()?;
14409                    model = Some(m.clone());
14410                    crate::gpu::GraphAttn::Full {
14411                        wq: gw(wq)?,
14412                        wk: gw(wk)?,
14413                        wv: gw(wv)?,
14414                        wo: gw(wo)?,
14415                        q_norm: q_norm.as_deref(),
14416                        k_norm: k_norm.as_deref(),
14417                        late_qk_norm: self.qk_norm_after_rope,
14418                        bias: bias
14419                            .as_ref()
14420                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14421                        output_gate: *output_gate,
14422                        cpu_k: self.kv_cache.layers[li].k_heads(),
14423                        cpu_v: self.kv_cache.layers[li].v_heads(),
14424                        cpu_base: self.kv_cache.layers[li].base(),
14425                        geom: self.graph_attn_geom(li),
14426                        head_gate,
14427                    }
14428                }
14429                AttnKind::LinearGdn(w) => {
14430                    let cfg = self.gdn_cfg?;
14431                    let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
14432                    model = Some(m.clone());
14433                    crate::gpu::GraphAttn::Gdn {
14434                        qkv: gw(&w.in_proj_qkv)?,
14435                        z: gw(&w.in_proj_z)?,
14436                        a: gw(&w.in_proj_a)?,
14437                        b: gw(&w.in_proj_b)?,
14438                        out: gw(&w.out_proj)?,
14439                        conv1d: &w.conv1d,
14440                        a_log: &w.a_log,
14441                        dt_bias: &w.dt_bias,
14442                        norm: &w.norm,
14443                        nv: cfg.num_v_heads,
14444                        nk: cfg.num_k_heads,
14445                        dk: cfg.key_head_dim,
14446                        dv: cfg.value_head_dim,
14447                        kk: cfg.conv_kernel,
14448                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14449                        preview: None,
14450                    }
14451                }
14452                AttnKind::ShortConv(w) => {
14453                    let cfg = self.short_conv_cfg?;
14454                    let (m, _, _, _) = w
14455                        .in_proj
14456                        .graph_weight()
14457                        .or_else(|| w.in_proj.graph_weight_descriptor())?;
14458                    model = Some(m.clone());
14459                    crate::gpu::GraphAttn::ShortConv {
14460                        inp: gw(&w.in_proj)?,
14461                        out: gw(&w.out_proj)?,
14462                        taps: &w.conv,
14463                        kernel: cfg.kernel,
14464                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14465                    }
14466                }
14467                _ => return None,
14468            };
14469            layers.push(crate::gpu::GraphLayer {
14470                input_norm: &lw.input_norm,
14471                attn,
14472                post_norm: &lw.post_norm,
14473                attn_out_norm: lw.attn_out_norm.as_deref(),
14474                ffn_out_norm: lw.ffn_out_norm.as_deref(),
14475                layer_scale: lw.layer_scale,
14476                ffn: gffn,
14477            });
14478        }
14479        let model = model?;
14480        // Fold final-norm + lm_head into the graph when this call wants logits
14481        // and the lm_head is a graphable (quantized) weight — the graph then
14482        // reads back logits (into logits_out) instead of the hidden, dropping
14483        // the separate CPU/GPU lm_head op + its sync. Never the f32 fallback:
14484        // an unquantized lm_head is vocab·hidden and must not be uploaded.
14485        let lm_gw = if upto_excl == self.num_layers
14486            && self.graph_want_logits
14487            && std::env::var("CMF_GPU_LMHEAD")
14488                .map(|v| v != "0")
14489                .unwrap_or(true)
14490        {
14491            self.weights
14492                .lm_head
14493                .graph_weight()
14494                .or_else(|| self.weights.lm_head.graph_weight_descriptor())
14495                .map(|(m, i, kind, rs)| {
14496                    let name = &m.tensors[i].name;
14497                    let prism = if crate::prism::is_inverse_embedding(m, name) {
14498                        crate::gpu::GraphPrismOp::InverseEmbedding
14499                    } else if crate::prism::is_forward_weight(m, name) {
14500                        crate::gpu::GraphPrismOp::Forward
14501                    } else {
14502                        crate::gpu::GraphPrismOp::None
14503                    };
14504                    (
14505                        crate::gpu::GraphW {
14506                            idx: i,
14507                            kind,
14508                            row_scale: rs,
14509                            data: &[],
14510                            prism,
14511                            affine: crate::prism::is_affine_target(m, name),
14512                        },
14513                        self.weights.lm_head.rows(),
14514                    )
14515                })
14516        } else {
14517            None
14518        };
14519        let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
14520        // Multi-step re-embeds the winner on the device.
14521        let emb_gw = if steps > 1 {
14522            self.weights
14523                .embed_tokens
14524                .graph_weight()
14525                .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
14526                .map(|(m, i, kind, rs)| {
14527                    let name = &m.tensors[i].name;
14528                    let prism = if crate::prism::is_inverse_embedding(m, name) {
14529                        crate::gpu::GraphPrismOp::InverseEmbedding
14530                    } else if crate::prism::is_forward_weight(m, name) {
14531                        crate::gpu::GraphPrismOp::Forward
14532                    } else {
14533                        crate::gpu::GraphPrismOp::None
14534                    };
14535                    (
14536                        crate::gpu::GraphW {
14537                            idx: i,
14538                            kind,
14539                            row_scale: rs,
14540                            data: &[],
14541                            prism,
14542                            affine: crate::prism::is_affine_target(m, name),
14543                        },
14544                        self.weights.embed_tokens.rows(),
14545                        self.embed_multiplier,
14546                    )
14547                })
14548        } else {
14549            None
14550        };
14551
14552        // Loop boundaries: virtual layer indices after which final_norm is
14553        // applied (mid-stack only; the GLOBAL last layer's norm folds into
14554        // lm_head). Span-relative — the executor compares its enumerate
14555        // index. A span ending mid-stack keeps its boundary norm even when
14556        // it is the span's own last layer.
14557        let loop_norm_at: Vec<usize> = if self.loop_final_norm {
14558            (from..upto_excl.min(self.num_layers - 1))
14559                .filter(|&li| (li + 1) % self.physical_layers == 0)
14560                .map(|li| li - from)
14561                .collect()
14562        } else {
14563            Vec::new()
14564        };
14565        let mut h = hidden.to_vec();
14566        // The normal decode path only needs the fused lm-head logits.  A
14567        // CMF_LOGIT_DUMP diagnostic, however, promises a prompt-boundary
14568        // post-stack hidden alongside those logits; request the existing
14569        // second readback only for that explicit probe instead of dumping
14570        // the input copy left in `h` by a folded-head graph.
14571        let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
14572        let outcome = crate::gpu::forward_token_graph(
14573            &model,
14574            self.graph_kv_id,
14575            &layers,
14576            &o1_views,
14577            self.o1_epoch,
14578            &self.inv_freq,
14579            &mut h,
14580            nh,
14581            nkv,
14582            hd,
14583            self.attn_scale,
14584            rd,
14585            self.hidden_size,
14586            self.intermediate_size,
14587            position,
14588            self.kv_cache.max_seq_len,
14589            gemma,
14590            self.rms_eps as f32,
14591            lm,
14592            &self.weights.final_norm,
14593            logits_out,
14594            &loop_norm_at,
14595            steps,
14596            emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
14597            ids_out,
14598            layers_run,
14599            from,
14600            dump_hidden,
14601        );
14602        match outcome {
14603            crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
14604            crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
14605            crate::gpu::TokenGraphOutcome::Declined => None,
14606        }
14607    }
14608
14609    /// Batched prefill: k contiguous prompt positions through the whole wgpu
14610    /// graph in ONE submit (projections/FFN as GEMMs). `hiddens` is [k·hidden]
14611    /// in/out (embeddings in, layer output out); KV mirror / GDN state advance.
14612    /// false ⇒ unsupported → caller keeps the per-position graph.
14613    /// The b-row Metal graph plan for the whole model: every layer as a
14614    /// GDN run or a full-attention item, all-or-nothing (a layer outside the
14615    /// graph's contract → None, the caller runs plain). Shared by the
14616    /// speculative verify and the batched prefill.
14617    #[cfg(target_os = "macos")]
14618    #[allow(clippy::type_complexity)]
14619    fn metal_rows_plan(
14620        &self,
14621    ) -> Option<(
14622        Vec<MetalRowsItem<'_>>,
14623        std::sync::Arc<cortiq_core::CmfModel>,
14624        Option<crate::gpu_metal::GdnGpuCfg>,
14625    )> {
14626        use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
14627        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
14628        if !graph_force
14629            || !crate::gpu::enabled_here()
14630            || std::env::var("CMF_GPU_BLOCK")
14631                .map(|v| v == "0")
14632                .unwrap_or(false)
14633            || self.attn_softcap > 0.0
14634            || self.o1_active()
14635            || self.swa.is_some()
14636            || self.global_attn.is_some()
14637            || self.attention_heads_per_layer.is_some()
14638            // per-layer KV heads, narrow V, learned sinks (MiMo-V2)
14639            || self.graph_attn_decline_reason().is_some()
14640            || self.attn_v_norm
14641            || self.loop_final_norm
14642            || !self.metal_rope_scale_ok()
14643        {
14644            return None;
14645        }
14646        let attend_contract = self.head_dim % 4 == 0
14647            && self.head_dim <= 256
14648            && self.rotary_dim >= 2
14649            && self.rotary_dim <= self.head_dim
14650            && (self.rotary_dim / 2) % 32 == 0
14651            && self.num_kv_heads > 0
14652            && self.num_heads % self.num_kv_heads == 0;
14653        if !attend_contract {
14654            return None;
14655        }
14656        let mut plan: Vec<MetalRowsItem> = Vec::new();
14657        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14658        for li in 0..self.num_layers {
14659            let lw = &self.weights.layers[self.phys_layer(li)];
14660            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14661                return None;
14662            }
14663            let ffn = match &lw.ffn {
14664                FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
14665                    let (Some(g), Some(u), Some(dn)) = (
14666                        d.gate_proj.metal_graph_parts(),
14667                        d.up_proj.metal_graph_parts(),
14668                        d.down_proj.metal_graph_parts(),
14669                    ) else {
14670                        return None;
14671                    };
14672                    MetalFfn::Dense {
14673                        gate: g,
14674                        up: u,
14675                        down: dn,
14676                        act: 0, // SiLU only (the arm above)
14677                    }
14678                }
14679                _ => return None,
14680            };
14681            match &lw.attn {
14682                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
14683                    let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
14684                        w.in_proj_qkv.metal_graph_parts(),
14685                        w.in_proj_z.metal_graph_parts(),
14686                        w.in_proj_a.f32_parts(),
14687                        w.in_proj_b.f32_parts(),
14688                        w.out_proj.metal_graph_parts(),
14689                    ) else {
14690                        return None;
14691                    };
14692                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
14693                        model_ref.get_or_insert_with(|| model.clone());
14694                    }
14695                    let gl = GdnGpuLayer {
14696                        attn_norm: &lw.input_norm,
14697                        post_norm: &lw.post_norm,
14698                        qkv,
14699                        z,
14700                        a,
14701                        b: bb,
14702                        out,
14703                        ffn,
14704                        conv1d: &w.conv1d,
14705                        a_log: &w.a_log,
14706                        dt_bias: &w.dt_bias,
14707                        gnorm: &w.norm,
14708                    };
14709                    match plan.last_mut() {
14710                        Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
14711                        _ => plan.push(MetalRowsItem::Gdn {
14712                            run: vec![gl],
14713                            first: li,
14714                        }),
14715                    }
14716                }
14717                AttnKind::Full {
14718                    wq,
14719                    wk,
14720                    wv,
14721                    wo,
14722                    q_norm,
14723                    k_norm,
14724                    output_gate,
14725                    softplus_gate: None,
14726                    bias: None,
14727                } => {
14728                    let (Some(pq), Some(pk), Some(pv), Some(po)) = (
14729                        wq.metal_graph_parts(),
14730                        wk.metal_graph_parts(),
14731                        wv.metal_graph_parts(),
14732                        wo.metal_graph_parts(),
14733                    ) else {
14734                        return None;
14735                    };
14736                    if let QTensor::Mapped { model, .. } = wq {
14737                        model_ref.get_or_insert_with(|| model.clone());
14738                    }
14739                    let cache = &self.kv_cache.layers[li];
14740                    if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
14741                        return None;
14742                    }
14743                    plan.push(MetalRowsItem::Attn {
14744                        l: AttnGpuLayer {
14745                            attn_norm: &lw.input_norm,
14746                            post_norm: &lw.post_norm,
14747                            wq: pq,
14748                            wk: pk,
14749                            wv: pv,
14750                            wo: po,
14751                            ffn,
14752                            attn_out_norm: None,
14753                            ffn_out_norm: None,
14754                            layer_scale: None,
14755                        },
14756                        li,
14757                        q_norm: q_norm.as_deref(),
14758                        k_norm: k_norm.as_deref(),
14759                        output_gate: *output_gate,
14760                    });
14761                }
14762                _ => return None,
14763            }
14764        }
14765        let model = model_ref?;
14766        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
14767            nv: cfg.num_v_heads,
14768            nk: cfg.num_k_heads,
14769            dk: cfg.key_head_dim,
14770            dv: cfg.value_head_dim,
14771            kk: cfg.conv_kernel,
14772            hidden: self.hidden_size,
14773            inter: self.intermediate_size,
14774            c_dim: cfg.conv_dim(),
14775            eps: cfg.rms_eps as f32,
14776            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14777        });
14778        Some((plan, model, gcfg))
14779    }
14780
14781    /// `AttnDeviceParams` for a plan item over the CPU cache as it stands.
14782    #[cfg(target_os = "macos")]
14783    #[allow(clippy::too_many_arguments)]
14784    fn metal_attn_params<'a>(
14785        li: usize,
14786        cache: &'a crate::kv_cache::LayerKvCache,
14787        q_norm: Option<&'a [f32]>,
14788        k_norm: Option<&'a [f32]>,
14789        output_gate: bool,
14790        inv_freq: &'a [f32],
14791        rope_scale: f32,
14792        geom: (usize, usize, usize, usize),
14793        pos0: usize,
14794        kv_id: u64,
14795        scale: f32,
14796        eps: f32,
14797        gemma: bool,
14798        late_qk_norm: bool,
14799    ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
14800        let (nh, nkv, hd, rd) = geom;
14801        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14802        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14803        let cpu_stored = cpu_k[0].len() / hd;
14804        (
14805            crate::gpu_metal::AttnDeviceParams {
14806                kv_id,
14807                layer: li,
14808                nh,
14809                nkv,
14810                hd,
14811                rd,
14812                position: pos0,
14813                scale,
14814                eps,
14815                gemma,
14816                late_qk_norm,
14817                output_gate,
14818                q_norm,
14819                k_norm,
14820                inv_freq,
14821                rope_scale,
14822                cpu_k,
14823                cpu_v,
14824                cpu_stored,
14825                cpu_gen: cache.generation(),
14826                o1: None,
14827                window: None,
14828                softmax_sink: cache.sinks.as_deref(),
14829                head_gate: None,
14830                v_norm: false,
14831            },
14832            cpu_stored,
14833        )
14834    }
14835
14836    /// Run the rows plan over `hiddens` (b rows at `pos0..`): validate,
14837    /// encode every item, optionally the head, sync. Returns the graph
14838    /// (for the commit / state finish) plus the GDN layer indices and the
14839    /// attention layers with the row count they were encoded against.
14840    #[cfg(target_os = "macos")]
14841    #[allow(clippy::type_complexity)]
14842    fn metal_rows_run(
14843        &mut self,
14844        hiddens: &mut [f32],
14845        pos0: usize,
14846        b: usize,
14847        prefill: bool,
14848        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14849        // Greedy verify: (row length scored, the b argmax ids out) — the
14850        // head's argmax runs on the device and the logits plane is NOT
14851        // read back (`spec.2` stays empty).
14852        mut argmax_out: Option<(usize, &mut Vec<u32>)>,
14853    ) -> MetalRowsRun {
14854        use crate::gpu_metal::{GraphDims, VerifyGraph};
14855        // The previous round's commit may still be replaying into the
14856        // trunk GDN owners on the second queue: this graph reads them
14857        // (zero-copy wraps) and may reallocate them below — collect the
14858        // replay first. Normally already complete (the draft chain ran
14859        // in between); a failed replay is terminal like a failed commit.
14860        if !crate::gpu_metal::wait_replay() {
14861            tracing::error!("Metal rows graph: the pending async replay failed");
14862            return MetalRowsRun::Failed;
14863        }
14864        spec_stamp("v.wait");
14865        // Seed the GDN recurrent records the rows graph reads — GDN layers
14866        // ONLY (the record the CPU path would allocate anyway).  Sizing
14867        // every layer here planted a zero GDN-sized record on a bounded
14868        // anchor of a natively bounded file this path then refused, and
14869        // that record counted as recurrent state on macOS.
14870        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
14871        if want > 0 {
14872            let phys = self.physical_layers.max(1);
14873            for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
14874                let is_gdn = self
14875                    .weights
14876                    .layers
14877                    .get(li % phys)
14878                    .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
14879                if is_gdn && l.linear_state.len() != want {
14880                    l.linear_state = vec![0f32; want];
14881                }
14882            }
14883        }
14884        let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
14885            return MetalRowsRun::Declined;
14886        };
14887        spec_stamp("v.plan");
14888        let dims = GraphDims {
14889            hidden: self.hidden_size,
14890            eps: self.rms_eps as f32,
14891            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14892        };
14893        let Some(mut graph) = (if prefill {
14894            VerifyGraph::new_prefill(&model, dims, hiddens, b)
14895        } else {
14896            VerifyGraph::new(&model, dims, hiddens, b)
14897        }) else {
14898            return MetalRowsRun::Declined;
14899        };
14900        let geom = (
14901            self.num_heads,
14902            self.num_kv_heads,
14903            self.head_dim,
14904            self.rotary_dim,
14905        );
14906        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14907        let eps = self.rms_eps as f32;
14908        let kv_id = self.graph_kv_id;
14909        let inv_freq = self.inv_freq.clone();
14910        for item in &plan {
14911            let ok = match item {
14912                MetalRowsItem::Gdn { run, .. } => gcfg
14913                    .as_ref()
14914                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
14915                    .unwrap_or(false),
14916                MetalRowsItem::Attn {
14917                    l,
14918                    li,
14919                    q_norm,
14920                    k_norm,
14921                    output_gate,
14922                } => {
14923                    let (p, _) = Self::metal_attn_params(
14924                        *li,
14925                        &self.kv_cache.layers[*li],
14926                        *q_norm,
14927                        *k_norm,
14928                        *output_gate,
14929                        &inv_freq,
14930                        self.layer_rope_scale(*li),
14931                        geom,
14932                        pos0,
14933                        kv_id,
14934                        self.attn_scale,
14935                        eps,
14936                        gemma,
14937                        self.qk_norm_after_rope,
14938                    );
14939                    graph.attn_ok(l, &p)
14940                }
14941            };
14942            if !ok {
14943                use std::sync::atomic::{AtomicBool, Ordering};
14944                static SAID: AtomicBool = AtomicBool::new(false);
14945                if !SAID.swap(true, Ordering::Relaxed) {
14946                    tracing::warn!("metal rows graph: a layer failed preflight — declining");
14947                }
14948                return MetalRowsRun::Declined;
14949            }
14950        }
14951        let lm = match &spec {
14952            Some((lm, _, _)) => {
14953                if !graph.lm_head_ok(*lm) {
14954                    return MetalRowsRun::Declined;
14955                }
14956                Some(*lm)
14957            }
14958            None => None,
14959        };
14960        let mut gdn_layers = Vec::new();
14961        let mut attn_layers = Vec::new();
14962        for item in &plan {
14963            match item {
14964                MetalRowsItem::Gdn { run, first } => {
14965                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
14966                        .iter()
14967                        .map(|l| l.linear_state.as_slice())
14968                        .collect();
14969                    if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
14970                        return MetalRowsRun::Declined;
14971                    }
14972                    gdn_layers.extend(*first..*first + run.len());
14973                }
14974                MetalRowsItem::Attn {
14975                    l,
14976                    li,
14977                    q_norm,
14978                    k_norm,
14979                    output_gate,
14980                } => {
14981                    let (p, cpu_stored) = Self::metal_attn_params(
14982                        *li,
14983                        &self.kv_cache.layers[*li],
14984                        *q_norm,
14985                        *k_norm,
14986                        *output_gate,
14987                        &inv_freq,
14988                        self.layer_rope_scale(*li),
14989                        geom,
14990                        pos0,
14991                        kv_id,
14992                        self.attn_scale,
14993                        eps,
14994                        gemma,
14995                        self.qk_norm_after_rope,
14996                    );
14997                    if !graph.encode_attn_b(l, &p) {
14998                        return MetalRowsRun::Declined;
14999                    }
15000                    attn_layers.push((*li, cpu_stored));
15001                }
15002            }
15003        }
15004        if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
15005            if !graph.encode_lm_head_b(final_norm, lm) {
15006                return MetalRowsRun::Declined;
15007            }
15008            // The device argmax is an OPTIMISATION, never a reason to
15009            // decline the round: if it will not encode, drop it and read
15010            // the logits plane back the old way (the head is encoded
15011            // either way, so the rows are there).
15012            if let Some((n, _)) = argmax_out.as_ref() {
15013                if !graph.encode_argmax_b(*n) {
15014                    argmax_out = None;
15015                }
15016            }
15017        }
15018        spec_stamp("v.enc");
15019        if !graph.sync() {
15020            return MetalRowsRun::Failed;
15021        }
15022        spec_stamp("v.gpu");
15023        match (spec, argmax_out) {
15024            (Some(_), Some((_, ids))) => {
15025                ids.resize(b, 0);
15026                if !graph.read_argmax(ids) {
15027                    return MetalRowsRun::Failed;
15028                }
15029                spec_stamp("v.am");
15030            }
15031            (Some((lm, _, logits)), None) => {
15032                logits.resize(b * lm.1, 0.0);
15033                if !graph.read_logits(logits) {
15034                    return MetalRowsRun::Failed;
15035                }
15036                spec_stamp("v.lg");
15037            }
15038            (None, _) => {}
15039        }
15040        if !graph.read_hidden(hiddens) {
15041            return MetalRowsRun::Failed;
15042        }
15043        spec_stamp("v.hid");
15044        MetalRowsRun::Completed(MetalVerifyPending {
15045            graph,
15046            gdn_layers,
15047            attn_layers,
15048        })
15049    }
15050
15051    /// Native-Metal twin of `try_batch_graph_wgpu`: the b rows through the
15052    /// whole model on the `VerifyGraph` (one submit), the head folded in
15053    /// when `spec` asks; `hiddens` come back as the last layer's output
15054    /// rows, `spec.2` as `[b][lm_rows]` logits. The graph is parked in
15055    /// `metal_verify` for `metal_verify_commit`.
15056    #[cfg(target_os = "macos")]
15057    fn try_batch_graph_metal(
15058        &mut self,
15059        hiddens: &mut [f32],
15060        positions: &[usize],
15061        b: usize,
15062        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15063        argmax_out: Option<(usize, &mut Vec<u32>)>,
15064    ) -> crate::gpu::BatchGraphOutcome {
15065        let _t0 = std::time::Instant::now();
15066        if positions.len() != b
15067            || positions.windows(2).any(|w| w[1] != w[0] + 1)
15068            || hiddens.len() != b * self.hidden_size
15069        {
15070            return crate::gpu::BatchGraphOutcome::Declined;
15071        }
15072        let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
15073            MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
15074            MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
15075            MetalRowsRun::Completed(pending) => pending,
15076        };
15077        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15078            eprintln!(
15079                "metal-verify: {:.1} ms | b={b}",
15080                _t0.elapsed().as_secs_f64() * 1e3
15081            );
15082        }
15083        self.metal_verify = Some(pending);
15084        crate::gpu::BatchGraphOutcome::Completed
15085    }
15086
15087    /// Batched prefill on the Metal rows graph: `ids` (≤ 512) at
15088    /// `start_pos..`, states written in place, K/V rows appended to the
15089    /// CPU caches; optional final norm/head logits are returned in `spec`.
15090    /// Declined means no command buffer was admitted; Failed is terminal.
15091    #[cfg(target_os = "macos")]
15092    fn prefill_rows_metal(
15093        &mut self,
15094        ids: &[u32],
15095        start_pos: usize,
15096        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15097    ) -> MetalPrefillOutcome {
15098        let b = ids.len();
15099        if b == 0 || b > 512 {
15100            return MetalPrefillOutcome::Declined;
15101        }
15102        METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15103        let with_head = spec.is_some();
15104        let hs = self.hidden_size;
15105        let mut hiddens = vec![0f32; b * hs];
15106        for (j, &id) in ids.iter().enumerate() {
15107            let e = self.embed_single(id);
15108            hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
15109        }
15110        let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
15111            MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
15112            MetalRowsRun::Failed => {
15113                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15114                return MetalPrefillOutcome::Failed;
15115            }
15116            MetalRowsRun::Completed(pending) => pending,
15117        };
15118        // states are final: copy them to the owners
15119        let idxs = pending.gdn_layers.clone();
15120        let mut outs: Vec<&mut [f32]> = self
15121            .kv_cache
15122            .layers
15123            .iter_mut()
15124            .enumerate()
15125            .filter(|(i, _)| idxs.binary_search(i).is_ok())
15126            .map(|(_, l)| l.linear_state.as_mut_slice())
15127            .collect();
15128        if !pending.graph.finish_states(&mut outs) {
15129            METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15130            return MetalPrefillOutcome::Failed;
15131        }
15132        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15133        // Read every layer before mutating any CPU cache.  A missing mirror
15134        // row is a terminal graph failure, not a reason to append a partial
15135        // prefix and replay the remainder serially.
15136        let mut rows = Vec::with_capacity(pending.attn_layers.len());
15137        for (li, cpu_stored) in &pending.attn_layers {
15138            let mut kbuf = vec![0f32; b * nkv * hd];
15139            let mut vbuf = vec![0f32; b * nkv * hd];
15140            if !crate::gpu_metal::kv_mirror_read_rows(
15141                self.graph_kv_id,
15142                *li,
15143                nkv,
15144                hd,
15145                *cpu_stored,
15146                b,
15147                &mut kbuf,
15148                &mut vbuf,
15149            ) {
15150                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15151                return MetalPrefillOutcome::Failed;
15152            }
15153            rows.push((*li, *cpu_stored, kbuf, vbuf));
15154        }
15155        for (li, cpu_stored, kbuf, vbuf) in rows {
15156            let cache = &mut self.kv_cache.layers[li];
15157            for r in 0..b {
15158                cache.append(
15159                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15160                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15161                    &[],
15162                );
15163            }
15164            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
15165        }
15166        METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15167        if with_head {
15168            METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15169        }
15170        MetalPrefillOutcome::Completed(hiddens)
15171    }
15172
15173    #[cfg(target_os = "macos")]
15174    fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
15175        self.prefill_rows_metal(ids, start_pos, None)
15176    }
15177
15178    /// Exact teacher-forced NLL through the ordinary Metal rows graph.  This
15179    /// is intentionally separate from the serial TokenGraph scorer: every
15180    /// chunk owns a real b-row graph/head completion and the recurrent/KV
15181    /// handoff is committed before the next chunk begins.
15182    #[cfg(target_os = "macos")]
15183    fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
15184        if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
15185            return MetalBatchNllOutcome::Declined;
15186        }
15187        let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
15188            return MetalBatchNllOutcome::Declined;
15189        };
15190        let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
15191            .ok()
15192            .and_then(|v| v.parse::<usize>().ok())
15193            .filter(|&v| (1..=512).contains(&v))
15194            .unwrap_or(32);
15195        let final_norm = self.weights.final_norm.clone();
15196        let mut nll = 0.0f64;
15197        let mut count = 0usize;
15198        let mut pos = 0usize;
15199        let mut completed = 0usize;
15200        while pos < ids.len() {
15201            let end = (pos + chunk).min(ids.len());
15202            let mut logits = Vec::new();
15203            let outcome =
15204                self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
15205            match outcome {
15206                MetalPrefillOutcome::Declined => {
15207                    return if completed == 0 {
15208                        MetalBatchNllOutcome::Declined
15209                    } else {
15210                        MetalBatchNllOutcome::Failed(format!(
15211                            "ordinary Metal NLL batch declined after {completed} chunks"
15212                        ))
15213                    };
15214                }
15215                MetalPrefillOutcome::Failed => {
15216                    return MetalBatchNllOutcome::Failed(
15217                        "ordinary Metal NLL batch failed after admission".to_string(),
15218                    );
15219                }
15220                MetalPrefillOutcome::Completed(_) => {}
15221            }
15222            completed += 1;
15223            let vocab = self.vocab_size.min(lm.1);
15224            if logits.len() != (end - pos) * lm.1 || vocab == 0 {
15225                return MetalBatchNllOutcome::Failed(
15226                    "ordinary Metal NLL head returned an invalid shape".to_string(),
15227                );
15228            }
15229            for row in 0..(end - pos) {
15230                let absolute = pos + row;
15231                if absolute < start || absolute + 1 >= ids.len() {
15232                    continue;
15233                }
15234                let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
15235                if let Some(mu) = self.logit_multiplier {
15236                    for v in lg.iter_mut() {
15237                        *v *= mu;
15238                    }
15239                }
15240                if let Some(c) = self.final_softcap {
15241                    softcap_in_place(lg, c, self.pool.as_deref());
15242                }
15243                let target = ids[absolute + 1] as usize;
15244                if target >= vocab {
15245                    return MetalBatchNllOutcome::Failed(format!(
15246                        "target token {target} exceeds Metal head rows {vocab}"
15247                    ));
15248                }
15249                let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
15250                let lse: f64 = lg
15251                    .iter()
15252                    .map(|&v| ((v - max) as f64).exp())
15253                    .sum::<f64>()
15254                    .ln()
15255                    + max as f64;
15256                nll += lse - lg[target] as f64;
15257                count += 1;
15258            }
15259            pos = end;
15260        }
15261        MetalBatchNllOutcome::Completed(nll, count)
15262    }
15263
15264    /// Commit a Metal verify round: replay the GDN recurrences over the
15265    /// `a + 1` accepted positions into the CPU states, append the accepted
15266    /// K/V rows from the mirrors to the CPU caches, re-point the mirrors.
15267    #[cfg(target_os = "macos")]
15268    fn metal_verify_commit(&mut self, a: usize) -> bool {
15269        let Some(mut pending) = self.metal_verify.take() else {
15270            return false;
15271        };
15272        let n = a + 1;
15273        // encode order == ascending layer order (the plan walks 0..layers)
15274        let idxs = pending.gdn_layers.clone();
15275        let mut outs: Vec<&mut [f32]> = self
15276            .kv_cache
15277            .layers
15278            .iter_mut()
15279            .enumerate()
15280            .filter(|(i, _)| idxs.binary_search(i).is_ok())
15281            .map(|(_, l)| l.linear_state.as_mut_slice())
15282            .collect();
15283        if !pending.graph.commit(n, &mut outs) {
15284            return false;
15285        }
15286        spec_stamp("c.replay");
15287        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15288        // Read every layer before mutating any CPU cache.  Missing rows are
15289        // terminal after the replay has executed; never append a partial KV
15290        // prefix and continue on a serial path.
15291        let mut rows = Vec::with_capacity(pending.attn_layers.len());
15292        for (li, cpu_stored) in &pending.attn_layers {
15293            let mut kbuf = vec![0f32; n * nkv * hd];
15294            let mut vbuf = vec![0f32; n * nkv * hd];
15295            if !crate::gpu_metal::kv_mirror_read_rows(
15296                self.graph_kv_id,
15297                *li,
15298                nkv,
15299                hd,
15300                *cpu_stored,
15301                n,
15302                &mut kbuf,
15303                &mut vbuf,
15304            ) {
15305                return false;
15306            }
15307            rows.push((*li, *cpu_stored, kbuf, vbuf));
15308        }
15309        for (li, cpu_stored, kbuf, vbuf) in rows {
15310            let cache = &mut self.kv_cache.layers[li];
15311            for r in 0..n {
15312                cache.append(
15313                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15314                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15315                    &[],
15316                );
15317            }
15318            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
15319        }
15320        spec_stamp("c.kv");
15321        true
15322    }
15323
15324    /// The round's warm-ups as ONE b-row graph run over the MTP block on
15325    /// Metal: `pairs` = (trunk hidden, next token) at consecutive positions
15326    /// from `first_pos`; the block's input projection is folded in. This
15327    /// half encodes and SUBMITS (no wait); `mtp_warm_batch_finish` waits
15328    /// and pulls the appended K/V rows into the CPU MTP cache. None = the
15329    /// graph declined (nothing submitted, nothing appended).
15330    #[cfg(target_os = "macos")]
15331    fn mtp_warm_batch_submit(
15332        &mut self,
15333        m: &mut MtpModule,
15334        pairs: &[(&[f32], u32)],
15335        first_pos: usize,
15336    ) -> Option<MetalWarmPending> {
15337        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
15338        let b = pairs.len();
15339        if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
15340            return None;
15341        }
15342        let AttnKind::Full {
15343            wq,
15344            wk,
15345            wv,
15346            wo,
15347            q_norm,
15348            k_norm,
15349            output_gate,
15350            softplus_gate: None,
15351            bias: None,
15352        } = &m.layer.attn
15353        else {
15354            return None;
15355        };
15356        let FfnKind::Dense(d) = &m.layer.ffn else {
15357            return None;
15358        };
15359        if !d.segs.is_empty() {
15360            return None;
15361        }
15362        let (Some(pq), Some(pk), Some(pv), Some(po)) =
15363            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
15364        else {
15365            return None;
15366        };
15367        let (Some(g), Some(u), Some(dn)) = (
15368            d.gate_proj.q1_parts(),
15369            d.up_proj.q1_parts(),
15370            d.down_proj.q1_parts(),
15371        ) else {
15372            return None;
15373        };
15374        let Some(eh) = m.eh_proj.q1_parts() else {
15375            return None;
15376        };
15377        let QTensor::Mapped { model, .. } = wq else {
15378            return None;
15379        };
15380        let model = model.clone();
15381        let hs = self.hidden_size;
15382        // [enorm(embed(tok)); hnorm(hidden)] rows
15383        let mut cat = vec![0f32; b * 2 * hs];
15384        for (j, (h, tok)) in pairs.iter().enumerate() {
15385            let e = self.embed_single(*tok);
15386            let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
15387            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
15388            inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
15389        }
15390        let dims = GraphDims {
15391            hidden: hs,
15392            eps: self.rms_eps as f32,
15393            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15394        };
15395        spec_stamp("w.cat");
15396        let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
15397            return None;
15398        };
15399        spec_stamp("w.new");
15400        let l = AttnGpuLayer {
15401            attn_norm: &m.layer.input_norm,
15402            post_norm: &m.layer.post_norm,
15403            wq: pq,
15404            wk: pk,
15405            wv: pv,
15406            wo: po,
15407            ffn: MetalFfn::Dense {
15408                gate: g,
15409                up: u,
15410                down: dn,
15411                act: (d.act == Act::Gelu) as u8,
15412            },
15413            attn_out_norm: None,
15414            ffn_out_norm: None,
15415            layer_scale: None,
15416        };
15417        let (nh, nkv, hd, rd) = (
15418            self.num_heads,
15419            self.num_kv_heads,
15420            self.head_dim,
15421            self.rotary_dim,
15422        );
15423        let inv_freq = self.inv_freq.clone();
15424        let cpu_stored;
15425        {
15426            let cache = &m.kv;
15427            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15428            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15429            cpu_stored = cpu_k[0].len() / hd;
15430            // The cache may LAG the position (rows nobody warmed): the
15431            // pairs land at cpu_stored.. with their true RoPE positions
15432            // first_pos.., exactly what the one-by-one warm does. A cache
15433            // AHEAD of the position is a real inconsistency.
15434            if cpu_stored > first_pos {
15435                spec_stamp("w.decl");
15436                return None;
15437            }
15438            let p = AttnDeviceParams {
15439                kv_id: self.mtp_kv_id(),
15440                layer: Self::MTP_LAYER_BASE,
15441                nh,
15442                nkv,
15443                hd,
15444                rd,
15445                position: first_pos,
15446                scale: self.attn_scale,
15447                eps: self.rms_eps as f32,
15448                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15449                late_qk_norm: self.qk_norm_after_rope,
15450                output_gate: *output_gate,
15451                q_norm: q_norm.as_deref(),
15452                k_norm: k_norm.as_deref(),
15453                inv_freq: &inv_freq,
15454                rope_scale: self.rope_scale,
15455                cpu_k,
15456                cpu_v,
15457                cpu_stored,
15458                cpu_gen: cache.generation(),
15459                o1: None,
15460                window: None,
15461                softmax_sink: None,
15462                head_gate: None,
15463                v_norm: false,
15464            };
15465            if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
15466                return None;
15467            }
15468        }
15469        spec_stamp("w.enc");
15470        if !graph.submit() {
15471            return None;
15472        }
15473        spec_stamp("w.sub");
15474        Some(MetalWarmPending {
15475            graph,
15476            cpu_stored,
15477            b,
15478        })
15479    }
15480
15481    /// Submit and finish in one call (the prefill's MTP warm-up, where
15482    /// nothing runs in between).
15483    #[cfg(target_os = "macos")]
15484    fn mtp_warm_batch_metal(
15485        &mut self,
15486        m: &mut MtpModule,
15487        pairs: &[(&[f32], u32)],
15488        first_pos: usize,
15489    ) -> bool {
15490        match self.mtp_warm_batch_submit(m, pairs, first_pos) {
15491            Some(p) => self.mtp_warm_batch_finish(m, p),
15492            None => false,
15493        }
15494    }
15495
15496    /// Second half of the batched warm-up: wait for the submitted graph,
15497    /// pull its b appended K/V rows into the CPU MTP cache, re-point the
15498    /// mirror. False = the command buffer failed or the rows are missing
15499    /// (nothing appended; the caller falls back to the one-by-one warm).
15500    #[cfg(target_os = "macos")]
15501    fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
15502        let MetalWarmPending {
15503            mut graph,
15504            cpu_stored,
15505            b,
15506        } = pending;
15507        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15508        if !graph.sync() {
15509            return false;
15510        }
15511        spec_stamp("w.gpu");
15512        let mut kbuf = vec![0f32; b * nkv * hd];
15513        let mut vbuf = vec![0f32; b * nkv * hd];
15514        if !crate::gpu_metal::kv_mirror_read_rows(
15515            self.mtp_kv_id(),
15516            Self::MTP_LAYER_BASE,
15517            nkv,
15518            hd,
15519            cpu_stored,
15520            b,
15521            &mut kbuf,
15522            &mut vbuf,
15523        ) {
15524            return false;
15525        }
15526        for r in 0..b {
15527            m.kv.append(
15528                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15529                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15530                &[],
15531            );
15532        }
15533        crate::gpu_metal::kv_mirror_set_stored(
15534            self.mtp_kv_id(),
15535            Self::MTP_LAYER_BASE,
15536            cpu_stored + b,
15537        );
15538        spec_stamp("w.kv");
15539        true
15540    }
15541
15542    /// A committed token id from the high table (Cyrillic, CJK and the
15543    /// like sit above 131072 in Qwen's vocabulary; Latin subwords past
15544    /// the 65536 cut are rare enough to lose as rejected drafts) switches
15545    /// the draft to the full head for the next 16 tokens; other ids count
15546    /// down. On an M4 the full 660 MB head costs 5.5 ms a draft step
15547    /// against 1.4 for the shortlist, so the streak is kept short.
15548    pub(crate) fn note_draft_id(&mut self, id: u32) {
15549        let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
15550        if (id as usize) >= cut {
15551            self.draft_full_streak = 16;
15552        } else {
15553            self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
15554        }
15555    }
15556
15557    /// The draft head's rows for the next step: the shortlist, or the full
15558    /// head while `draft_full_streak` runs.
15559    fn draft_head_rows(&self, head_rows: usize) -> usize {
15560        if self.draft_full_streak > 0 {
15561            head_rows
15562        } else {
15563            Self::draft_vocab_rows(head_rows)
15564        }
15565    }
15566
15567    /// Draft-head shortlist size: `CMF_DRAFT_VOCAB` rows (default 65536,
15568    /// capped at the head; 0 = full head).
15569    fn draft_vocab_rows(head_rows: usize) -> usize {
15570        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
15571        let n = *N.get_or_init(|| {
15572            std::env::var("CMF_DRAFT_VOCAB")
15573                .ok()
15574                .and_then(|v| v.parse().ok())
15575                .unwrap_or(65536)
15576        });
15577        if n == 0 { head_rows } else { n.min(head_rows) }
15578    }
15579
15580    /// One MTP block step on the native Metal token graph: block input on
15581    /// the host, the attention layer + FFN device-resident over the MTP
15582    /// mirror, the head folded in when `want_logits`. The appended K/V row
15583    /// is pulled into the CPU MTP cache (owner of record) after the sync.
15584    #[cfg(target_os = "macos")]
15585    fn mtp_step_metal(
15586        &mut self,
15587        m: &mut MtpModule,
15588        hidden: &[f32],
15589        next_token: u32,
15590        position: usize,
15591        want_logits: bool,
15592    ) -> Option<(Vec<f32>, Vec<f32>)> {
15593        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
15594        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
15595            || !crate::gpu::q1_force()
15596            || !crate::gpu::enabled_here()
15597            || self.attn_softcap > 0.0
15598            || self.attention_heads_per_layer.is_some()
15599            || m.kv.mode != crate::kv_cache::KvMode::F32
15600            || m.kv.o1.is_some()
15601        {
15602            return None;
15603        }
15604        let AttnKind::Full {
15605            wq,
15606            wk,
15607            wv,
15608            wo,
15609            q_norm,
15610            k_norm,
15611            output_gate,
15612            softplus_gate: None,
15613            bias: None,
15614        } = &m.layer.attn
15615        else {
15616            return None;
15617        };
15618        let FfnKind::Dense(d) = &m.layer.ffn else {
15619            return None;
15620        };
15621        if d.act != Act::Silu || !d.segs.is_empty() {
15622            return None;
15623        }
15624        let (pq, pk, pv, po) = (
15625            wq.q1_parts()?,
15626            wk.q1_parts()?,
15627            wv.q1_parts()?,
15628            wo.q1_parts()?,
15629        );
15630        let (g, u, dn) = (
15631            d.gate_proj.q1_parts()?,
15632            d.up_proj.q1_parts()?,
15633            d.down_proj.q1_parts()?,
15634        );
15635        let QTensor::Mapped { model, .. } = wq else {
15636            return None;
15637        };
15638        let model = model.clone();
15639        let lm = if want_logits {
15640            Some(self.weights.lm_head.q1_parts()?)
15641        } else {
15642            None
15643        };
15644        let dims = GraphDims {
15645            hidden: self.hidden_size,
15646            eps: self.rms_eps as f32,
15647            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15648        };
15649        // The block input `eh_proj · [enorm(e); hnorm(h)]` rides in the
15650        // graph (one submit a step); the host per-op matvec if it cannot.
15651        let hs = self.hidden_size;
15652        let mut x = vec![0f32; hs];
15653        // Let an eligible q4tp MTP draft use the measured masked-nibble
15654        // matvec.  Unlike fused epilogues, this reduces the draft's
15655        // projection work without changing its residual-update schedule;
15656        // the target verifier remains the authority over emitted tokens.
15657        // The guard keeps `CMF_METAL_MVFAST=0` as an immediate rollback
15658        // switch and restores the caller's setting on every early return.
15659        let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
15660            crate::gpu_metal::DENSE_MV,
15661        );
15662        let mut graph = TokenGraph::new(&model, dims, &x)?;
15663        let mut folded = false;
15664        if let Some(eh) = m.eh_proj.q1_parts() {
15665            let e = self.embed_single(next_token);
15666            let mut cat = vec![0.0f32; 2 * hs];
15667            let (cat_e, cat_h) = cat.split_at_mut(hs);
15668            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
15669            inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
15670            folded = graph.encode_input_proj(eh, &cat);
15671        }
15672        if !folded {
15673            x = self.mtp_block_input(m, hidden, next_token);
15674            graph = TokenGraph::new(&model, dims, &x)?;
15675        }
15676        spec_stamp("d.in");
15677        let l = AttnGpuLayer {
15678            attn_norm: &m.layer.input_norm,
15679            post_norm: &m.layer.post_norm,
15680            wq: pq,
15681            wk: pk,
15682            wv: pv,
15683            wo: po,
15684            ffn: MetalFfn::Dense {
15685                gate: g,
15686                up: u,
15687                down: dn,
15688                act: (d.act == Act::Gelu) as u8,
15689            },
15690            attn_out_norm: None,
15691            ffn_out_norm: None,
15692            layer_scale: None,
15693        };
15694        let (nh, nkv, hd, rd) = (
15695            self.num_heads,
15696            self.num_kv_heads,
15697            self.head_dim,
15698            self.rotary_dim,
15699        );
15700        let inv_freq = self.inv_freq.clone();
15701        {
15702            let cache = &m.kv;
15703            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15704            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15705            let cpu_stored = cpu_k[0].len() / hd;
15706            let p = AttnDeviceParams {
15707                kv_id: self.mtp_kv_id(),
15708                layer: Self::MTP_LAYER_BASE,
15709                nh,
15710                nkv,
15711                hd,
15712                rd,
15713                position,
15714                scale: self.attn_scale,
15715                eps: self.rms_eps as f32,
15716                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15717                late_qk_norm: self.qk_norm_after_rope,
15718                output_gate: *output_gate,
15719                q_norm: q_norm.as_deref(),
15720                k_norm: k_norm.as_deref(),
15721                inv_freq: &inv_freq,
15722                rope_scale: self.rope_scale,
15723                cpu_k,
15724                cpu_v,
15725                cpu_stored,
15726                cpu_gen: cache.generation(),
15727                o1: None,
15728                window: None,
15729                softmax_sink: None,
15730                head_gate: None,
15731                v_norm: false,
15732            };
15733            if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
15734                return None;
15735            }
15736        }
15737        // The draft's head over a vocabulary SHORTLIST (the first
15738        // CMF_DRAFT_VOCAB rows — BPE ids run roughly by merge rank, so the
15739        // low ids carry the mass): the verify keeps the full head, so a true
15740        // token past the cut is only a rejected draft, never a wrong token.
15741        // 662 MB a step on Qwen3.8 becomes 170 MB at 65536.
15742        let draft_rows = if let Some(lm) = lm {
15743            self.draft_head_rows(lm.1)
15744        } else {
15745            0
15746        };
15747        if let Some(lm) = lm {
15748            if !graph.lm_head_ok(lm) {
15749                return None;
15750            }
15751            if draft_rows < lm.1 {
15752                if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
15753                    return None;
15754                }
15755            } else {
15756                graph.encode_lm_head(&m.final_norm, lm);
15757            }
15758        }
15759        spec_stamp("d.enc");
15760        if graph.sync_checked().is_err() {
15761            return None;
15762        }
15763        spec_stamp("d.gpu");
15764        let mut logits = Vec::new();
15765        if let Some(lm) = lm {
15766            let n_read = draft_rows.min(lm.1).min(self.vocab_size);
15767            logits = attention::take_buf(n_read);
15768            graph.read_logits(&mut logits);
15769            // ids past the shortlist: never drafted (−∞ in every chain)
15770            pad_mtp_draft_logits(&mut logits, self.vocab_size);
15771        }
15772        graph.finish(&mut x);
15773        let mut krow = attention::take_buf(nkv * hd);
15774        let mut vrow = attention::take_buf(nkv * hd);
15775        if crate::gpu_metal::kv_mirror_read_last(
15776            self.mtp_kv_id(),
15777            Self::MTP_LAYER_BASE,
15778            nkv,
15779            hd,
15780            &mut krow,
15781            &mut vrow,
15782        ) {
15783            m.kv.append(&krow, &vrow, &[]);
15784        }
15785        attention::recycle_buf(&mut krow);
15786        attention::recycle_buf(&mut vrow);
15787        spec_stamp("d.rd");
15788        Some((logits, x))
15789    }
15790
15791    /// `CMF_MTP_CHAIN=0` keeps the per-step draft (one submit and one
15792    /// host round trip per MTP step); the default drafts the whole chain
15793    /// in one command buffer when the round is plain greedy.
15794    ///
15795    /// Measured on an M4 (24 GB), Qwen3.8-27B q4tp, P3 at 160 tokens,
15796    /// k=7, six runs per arm alternating inside one lock window — the
15797    /// round's draft phase (median over the 34 rounds of a run) is
15798    /// 34.5 ms per round old against 30.1 new, i.e. 4.93 → 4.31 ms per
15799    /// draft step. That is the whole prize: the 7 submits cost ~0.6 ms
15800    /// each in host and submit latency and nothing else changes —
15801    /// acceptance (3.41 of 7) and tokens per round (4.41) are identical,
15802    /// and the round is 289 → 285 ms, decode 13.8 → 14.0 tok/s.
15803    fn mtp_chain_on() -> bool {
15804        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15805        *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
15806    }
15807
15808    /// The round's k greedy drafts as ONE command buffer on Metal: the MTP
15809    /// block k times back to back, each step's token embedding gathered
15810    /// on the device from the argmax the step before it wrote, the head
15811    /// over the round's shortlist (or the full head during a full-head
15812    /// streak — decided once, before the chain, exactly as the per-step
15813    /// path decides it per step, since `draft_full_streak` only moves on
15814    /// a commit). One wait, then the k ids and the k appended K/V rows
15815    /// come back; the CPU MTP cache ends where k `mtp_step_metal` calls
15816    /// would have left it. `Err(false)` = declined before anything was
15817    /// committed (the per-step path takes the round); `Err(true)` = the
15818    /// command buffer failed after commit.
15819    #[cfg(target_os = "macos")]
15820    fn mtp_draft_chain_metal(
15821        &mut self,
15822        m: &mut MtpModule,
15823        hidden: &[f32],
15824        t_next: u32,
15825        position: usize,
15826        k: usize,
15827    ) -> Result<Vec<u32>, bool> {
15828        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
15829        if k == 0
15830            || k > 64
15831            || !Self::mtp_chain_on()
15832            || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
15833            || !crate::gpu::q1_force()
15834            || !crate::gpu::enabled_here()
15835            || self.attn_softcap > 0.0
15836            || self.attention_heads_per_layer.is_some()
15837            || m.kv.mode != crate::kv_cache::KvMode::F32
15838            || m.kv.o1.is_some()
15839            // the chain gathers embeddings itself: only the plain table
15840            || self.dsv4.is_some()
15841            || self.dsv41.is_some()
15842            || self.qwen4_exp.is_some()
15843            || self.per_token_stack()
15844        {
15845            return Err(false);
15846        }
15847        let AttnKind::Full {
15848            wq,
15849            wk,
15850            wv,
15851            wo,
15852            q_norm,
15853            k_norm,
15854            output_gate,
15855            softplus_gate: None,
15856            bias: None,
15857        } = &m.layer.attn
15858        else {
15859            return Err(false);
15860        };
15861        let FfnKind::Dense(d) = &m.layer.ffn else {
15862            return Err(false);
15863        };
15864        if d.act != Act::Silu || !d.segs.is_empty() {
15865            return Err(false);
15866        }
15867        let (Some(pq), Some(pk), Some(pv), Some(po)) =
15868            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
15869        else {
15870            return Err(false);
15871        };
15872        let (Some(g), Some(u), Some(dn)) = (
15873            d.gate_proj.q1_parts(),
15874            d.up_proj.q1_parts(),
15875            d.down_proj.q1_parts(),
15876        ) else {
15877            return Err(false);
15878        };
15879        let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
15880            return Err(false);
15881        };
15882        let QTensor::Mapped { model, .. } = wq else {
15883            return Err(false);
15884        };
15885        let model = model.clone();
15886        // the embedding table: a q4tp tensor of the SAME blob, no Prism
15887        // inverse-embedding post-pass
15888        let QTensor::Mapped {
15889            model: em,
15890            idx: eidx,
15891            dtype: cortiq_core::TensorDtype::Q4TiledP,
15892            ..
15893        } = &self.weights.embed_tokens
15894        else {
15895            return Err(false);
15896        };
15897        if !std::sync::Arc::ptr_eq(em, &model)
15898            || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
15899        {
15900            return Err(false);
15901        }
15902        let embed = (
15903            *eidx,
15904            self.weights.embed_tokens.rows(),
15905            self.weights.embed_tokens.cols(),
15906        );
15907        if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
15908            return Err(false);
15909        }
15910        let dims = GraphDims {
15911            hidden: self.hidden_size,
15912            eps: self.rms_eps as f32,
15913            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15914        };
15915        // Keep the chained MTP draft on the masked-nibble Metal matvec
15916        // path.  This scope covers every encode in the chain; the guard
15917        // restores its predecessor when this round returns or declines.
15918        let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
15919            crate::gpu_metal::DENSE_MV,
15920        );
15921        let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
15922            return Err(false);
15923        };
15924        if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
15925            return Err(false);
15926        }
15927        let l = AttnGpuLayer {
15928            attn_norm: &m.layer.input_norm,
15929            post_norm: &m.layer.post_norm,
15930            wq: pq,
15931            wk: pk,
15932            wv: pv,
15933            wo: po,
15934            ffn: MetalFfn::Dense {
15935                gate: g,
15936                up: u,
15937                down: dn,
15938                act: (d.act == Act::Gelu) as u8,
15939            },
15940            attn_out_norm: None,
15941            ffn_out_norm: None,
15942            layer_scale: None,
15943        };
15944        let (nh, nkv, hd, rd) = (
15945            self.num_heads,
15946            self.num_kv_heads,
15947            self.head_dim,
15948            self.rotary_dim,
15949        );
15950        let inv_freq = self.inv_freq.clone();
15951        let draft_rows = self.draft_head_rows(lm.1);
15952        let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
15953        if n_arg == 0 {
15954            return Err(false);
15955        }
15956        // `CMF_MTP_CHAIN_SPLIT=1` commits each step as it is encoded, so
15957        // the GPU starts on step 0 while the host is still encoding step
15958        // 1 — a probe for whether the host encode is on the critical
15959        // path. It is not: three runs each, draft 30.0 ms per round split
15960        // against 30.1 whole, and the whole chain's host encode measures
15961        // 0.3 ms against a 29.7 ms wait. Kept as a probe, off by default.
15962        let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
15963        let t_chain = std::time::Instant::now();
15964        graph.chain_ids_init(t_next, k);
15965        let cpu_stored;
15966        {
15967            let cache = &m.kv;
15968            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15969            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15970            cpu_stored = cpu_k[0].len() / hd;
15971            for j in 0..k {
15972                if !graph.encode_chain_input(
15973                    embed,
15974                    j as u32,
15975                    &m.enorm,
15976                    &m.hnorm,
15977                    self.embed_multiplier,
15978                    eh,
15979                ) {
15980                    return Err(false);
15981                }
15982                // step j's mirror row: the mirror is re-pointed at the CPU
15983                // rows before step 0 and advances by one per step; its
15984                // resync (never taken past step 0) reads the CPU rows
15985                let p = AttnDeviceParams {
15986                    kv_id: self.mtp_kv_id(),
15987                    layer: Self::MTP_LAYER_BASE,
15988                    nh,
15989                    nkv,
15990                    hd,
15991                    rd,
15992                    position: position + j,
15993                    scale: self.attn_scale,
15994                    eps: self.rms_eps as f32,
15995                    gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15996                    late_qk_norm: self.qk_norm_after_rope,
15997                    output_gate: *output_gate,
15998                    q_norm: q_norm.as_deref(),
15999                    k_norm: k_norm.as_deref(),
16000                    inv_freq: &inv_freq,
16001                    rope_scale: self.rope_scale,
16002                    cpu_k: cpu_k.clone(),
16003                    cpu_v: cpu_v.clone(),
16004                    cpu_stored: cpu_stored + j,
16005                    cpu_gen: cache.generation(),
16006                    o1: None,
16007                    window: None,
16008                    softmax_sink: None,
16009                    head_gate: None,
16010                    v_norm: false,
16011                };
16012                if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
16013                    return Err(false);
16014                }
16015                if draft_rows < lm.1 {
16016                    if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
16017                        return Err(false);
16018                    }
16019                } else {
16020                    graph.encode_lm_head(&m.final_norm, lm);
16021                }
16022                if !graph.encode_argmax(n_arg, j as u32 + 1) {
16023                    return Err(false);
16024                }
16025                if split {
16026                    // CMF_MTP_CHAIN_SPLIT=1: commit every step so the GPU
16027                    // starts on step 0 while the host encodes the rest
16028                    graph.commit();
16029                }
16030            }
16031        }
16032        let t_enc = t_chain.elapsed();
16033        if graph.sync_checked().is_err() {
16034            return Err(true);
16035        }
16036        if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
16037            eprintln!(
16038                "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
16039                t_enc.as_secs_f64() * 1e3,
16040                (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
16041                if split { ", split" } else { "" }
16042            );
16043        }
16044        let mut ids = vec![0u32; k];
16045        if !graph.chain_ids_read(&mut ids) {
16046            return Err(true);
16047        }
16048        let mut kbuf = vec![0f32; k * nkv * hd];
16049        let mut vbuf = vec![0f32; k * nkv * hd];
16050        if !crate::gpu_metal::kv_mirror_read_rows(
16051            self.mtp_kv_id(),
16052            Self::MTP_LAYER_BASE,
16053            nkv,
16054            hd,
16055            cpu_stored,
16056            k,
16057            &mut kbuf,
16058            &mut vbuf,
16059        ) {
16060            return Err(true);
16061        }
16062        for r in 0..k {
16063            m.kv.append(
16064                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
16065                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
16066                &[],
16067            );
16068        }
16069        Ok(ids)
16070    }
16071
16072    fn try_batch_graph_wgpu(
16073        &self,
16074        hiddens: &mut [f32],
16075        positions: &[usize],
16076        k: usize,
16077        spec: Option<crate::gpu::SpecTail<'_>>,
16078    ) -> crate::gpu::BatchGraphOutcome {
16079        self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
16080    }
16081
16082    /// `try_batch_graph_wgpu` with the device-prefix mode: `layers_run`
16083    /// Some lets a stack that does not fit run its leading layers (the
16084    /// token graph's prefix rule) and reports how many; `hiddens` then
16085    /// holds the boundary rows and the caller runs the rest on the host.
16086    fn try_batch_graph_wgpu_prefix(
16087        &self,
16088        hiddens: &mut [f32],
16089        positions: &[usize],
16090        k: usize,
16091        spec: Option<crate::gpu::SpecTail<'_>>,
16092        layers_run: Option<&mut usize>,
16093    ) -> crate::gpu::BatchGraphOutcome {
16094        let graph_end = match self.mimo_moe.graph_prefix_end() {
16095            Some(end) if end < self.num_layers => {
16096                if layers_run.is_none() || spec.is_some() || end == 0 {
16097                    return crate::gpu::BatchGraphOutcome::Declined;
16098                }
16099                end
16100            }
16101            _ => self.num_layers,
16102        };
16103        let _tb = std::time::Instant::now();
16104        let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
16105        if self.attn_softcap > 0.0 {
16106            return crate::gpu::BatchGraphOutcome::Declined; // capped scores: no graph kernel — CPU path
16107        }
16108        // Same attention contract as the token graph: per-layer geometry
16109        // rides `geom`, anything it cannot express declines by name.
16110        if let Some(reason) = self
16111            .wgpu_graph_attn_decline()
16112            .or_else(|| self.batch_graph_gemma_decline())
16113        {
16114            self.note_graph_decline("wgpu batch graph", reason);
16115            return crate::gpu::BatchGraphOutcome::Declined;
16116        }
16117        let nh = self.num_heads;
16118        let (nkv, hd, rd) = self.layer_geom(0);
16119        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16120        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
16121            if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
16122            {
16123                let name = &m.tensors[i].name;
16124                let prism = if crate::prism::is_inverse_embedding(m, name) {
16125                    crate::gpu::GraphPrismOp::InverseEmbedding
16126                } else if crate::prism::is_forward_weight(m, name) {
16127                    crate::gpu::GraphPrismOp::Forward
16128                } else {
16129                    crate::gpu::GraphPrismOp::None
16130                };
16131                return Some(crate::gpu::GraphW {
16132                    idx: i,
16133                    kind,
16134                    row_scale: rs,
16135                    data: &[],
16136                    prism,
16137                    affine: crate::prism::is_affine_target(m, name),
16138                });
16139            }
16140            if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
16141                eprintln!(
16142                    "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
16143                    t.rows(),
16144                    t.cols()
16145                );
16146            }
16147            t.as_f32().map(|d| crate::gpu::GraphW {
16148                idx: 0,
16149                kind: 4,
16150                row_scale: &[],
16151                data: d,
16152                prism: crate::gpu::GraphPrismOp::None,
16153                affine: false,
16154            })
16155        }
16156        let built: Option<(
16157            Vec<crate::gpu::GraphLayer<'_>>,
16158            std::sync::Arc<cortiq_core::CmfModel>,
16159        )> = (|| {
16160            let mut layers = Vec::with_capacity(graph_end);
16161            let mut model = None;
16162            for li in 0..graph_end {
16163                let lw = &self.weights.layers[self.phys_layer(li)];
16164                // MoE routes per token, so its experts are encoded token by
16165                // token inside the batched submit while attention and the
16166                // projections stay GEMMs. Refusing MoE here is what left
16167                // prefill running one position at a time: 33 tok/s against
16168                // 54 on decode, i.e. reading the prompt was slower than
16169                // writing the answer.
16170                let gffn = match &lw.ffn {
16171                    FfnKind::Dense(d) if !d.segs.is_empty() => {
16172                        if batch_debug {
16173                            eprintln!("batch graph: dense segmented FFN at layer {li}");
16174                        }
16175                        return None;
16176                    }
16177                    FfnKind::Dense(d) => {
16178                        let Some(act) = d.act.wgpu_graph_act() else {
16179                            if batch_debug {
16180                                eprintln!(
16181                                    "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
16182                                    d.act
16183                                );
16184                            }
16185                            return None;
16186                        };
16187                        crate::gpu::GraphFfn::Dense {
16188                            gate: gw(&d.gate_proj)?,
16189                            up: gw(&d.up_proj)?,
16190                            down: gw(&d.down_proj)?,
16191                            act,
16192                        }
16193                    }
16194                    FfnKind::Moe(m) => {
16195                        // Adaptive τ and expert masks stay on the CPU path.
16196                        // Sigmoid scores, the selection bias, a routed scale
16197                        // ≠ 1 and an ungated shared expert (hy_v3) ride the
16198                        // same flags word as the token graph — before, this
16199                        // refusal sent every Hy-MT2-30B prompt to the chunked
16200                        // fallback (8 tok/s of ingest against 53 of decode).
16201                        if m.route_tau.is_some() || m.mask.is_some() {
16202                            return None;
16203                        }
16204                        // A shared expert rides as slot top_k (gated or
16205                        // not is a flag on the select kernel); without one
16206                        // (MiMo-V2, LFM2-MoE) the kernels run top_k slots.
16207                        let shared = m.shared.as_ref();
16208                        let has_shared = shared.is_some();
16209                        let shared_gated = matches!(shared, Some((_, Some(_))));
16210                        let sgate = match shared {
16211                            Some((_, Some(sg))) => gw(sg)?,
16212                            // Ungated or absent: the router plane stands in
16213                            // so the plumbing stays total; the kernel pins
16214                            // weight 1 or never reads it.
16215                            _ => gw(&m.router)?,
16216                        };
16217                        let router = gw(&m.router)?;
16218                        // The batch MoE kernels still consume raw per-token
16219                        // rows and do not carry the descriptor-aware Prism
16220                        // transform/affine bit for router or shared-gate
16221                        // planes.  Refuse rather than route an untransformed
16222                        // source activation.
16223                        if router.prism != crate::gpu::GraphPrismOp::None
16224                            || router.affine
16225                            || sgate.prism != crate::gpu::GraphPrismOp::None
16226                            || sgate.affine
16227                        {
16228                            return None;
16229                        }
16230                        let inter = m.experts.first()?.gate_proj.rows();
16231                        let mut experts = Vec::with_capacity(m.experts.len() + 1);
16232                        let mut q4tp: Option<bool> = None;
16233                        let mut gu_q2: Option<bool> = None;
16234                        let mut q82: Option<bool> = None;
16235                        // The packed MoE graph has one gate/up program per
16236                        // layer, so accept only one exact activation contract
16237                        // across routed and in-bank shared experts.
16238                        let mut moe_act = None;
16239                        for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
16240                            let Some(e_act) = e.act.wgpu_graph_act() else {
16241                                return None;
16242                            };
16243                            match moe_act {
16244                                Some(existing) if existing != e_act => return None,
16245                                None => moe_act = Some(e_act),
16246                                _ => {}
16247                            }
16248                            if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
16249                                return None;
16250                            }
16251                            // Same ladder as the token graph: q4t → q2tp
16252                            // (mixed profile: 2-bit gate/up over a q4tp
16253                            // down) → q4tp → q8_2f. Uniform across the layer.
16254                            let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
16255                            {
16256                                Some((mm, gi)) => (
16257                                    mm,
16258                                    gi,
16259                                    e.up_proj.mapped_q4t()?.1,
16260                                    e.down_proj.mapped_q4t()?.1,
16261                                    false,
16262                                    false,
16263                                    false,
16264                                ),
16265                                None => match e.gate_proj.mapped_q2tp() {
16266                                    Some((mm, gi)) => (
16267                                        mm,
16268                                        gi,
16269                                        e.up_proj.mapped_q2tp()?.1,
16270                                        e.down_proj.mapped_q4tp()?.1,
16271                                        true,
16272                                        true,
16273                                        false,
16274                                    ),
16275                                    None => match e.gate_proj.mapped_q4tp() {
16276                                        Some((mm, gi)) => (
16277                                            mm,
16278                                            gi,
16279                                            e.up_proj.mapped_q4tp()?.1,
16280                                            e.down_proj.mapped_q4tp()?.1,
16281                                            true,
16282                                            false,
16283                                            false,
16284                                        ),
16285                                        None => {
16286                                            if !crate::gpu::moe_q82_graph_on() {
16287                                                return None;
16288                                            }
16289                                            let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
16290                                            (
16291                                                mm,
16292                                                gi,
16293                                                e.up_proj.mapped_q8_2f()?.1,
16294                                                e.down_proj.mapped_q8_2f()?.1,
16295                                                false,
16296                                                false,
16297                                                true,
16298                                            )
16299                                        }
16300                                    },
16301                                },
16302                            };
16303                            if *q4tp.get_or_insert(is_p) != is_p
16304                                || *gu_q2.get_or_insert(is_q2) != is_q2
16305                                || *q82.get_or_insert(is_8) != is_8
16306                            {
16307                                return None;
16308                            }
16309                            if [gi, ui, di].into_iter().any(|idx| {
16310                                mm.tensors.get(idx).is_some_and(|t| {
16311                                    crate::prism::is_forward_weight(mm, &t.name)
16312                                        || crate::prism::is_affine_target(mm, &t.name)
16313                                })
16314                            }) {
16315                                return None;
16316                            }
16317                            model.get_or_insert_with(|| mm.clone());
16318                            experts.push((gi, ui, di));
16319                        }
16320                        crate::gpu::GraphFfn::Moe {
16321                            router,
16322                            shared_gate: sgate,
16323                            experts,
16324                            n_exp: m.experts.len(),
16325                            top_k: m.top_k,
16326                            inter,
16327                            act: moe_act?,
16328                            norm_topk: m.norm_topk_prob,
16329                            q4tp: q4tp?,
16330                            gu_q2: gu_q2.unwrap_or(false),
16331                            q82: q82.unwrap_or(false),
16332                            sigmoid: m.router_sigmoid,
16333                            sigmoid_denom_eps: m.sigmoid_denom_eps,
16334                            bias: m.expert_bias.as_deref(),
16335                            has_shared,
16336                            detached_shared: None,
16337                            shared_gated,
16338                            route_scale: m.routed_scaling,
16339                        }
16340                    }
16341                    _ => return None,
16342                };
16343                let attn = match &lw.attn {
16344                    AttnKind::Full {
16345                        wq,
16346                        wk,
16347                        wv,
16348                        wo,
16349                        q_norm,
16350                        k_norm,
16351                        output_gate,
16352                        softplus_gate,
16353                        bias,
16354                    } => {
16355                        if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
16356                            if batch_debug {
16357                                eprintln!(
16358                                    "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
16359                                    softplus_gate.is_some(),
16360                                    self.attention_heads_per_layer.is_some()
16361                                );
16362                            }
16363                            return None;
16364                        }
16365                        let (m, _, _, _) =
16366                            wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
16367                        model = Some(m.clone());
16368                        crate::gpu::GraphAttn::Full {
16369                            wq: gw(wq)?,
16370                            wk: gw(wk)?,
16371                            wv: gw(wv)?,
16372                            wo: gw(wo)?,
16373                            q_norm: q_norm.as_deref(),
16374                            k_norm: k_norm.as_deref(),
16375                            late_qk_norm: self.qk_norm_after_rope,
16376                            bias: bias
16377                                .as_ref()
16378                                .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16379                            output_gate: *output_gate,
16380                            cpu_k: self.kv_cache.layers[li].k_heads(),
16381                            cpu_v: self.kv_cache.layers[li].v_heads(),
16382                            cpu_base: self.kv_cache.layers[li].base(),
16383                            geom: self.graph_attn_geom(li),
16384                            // The batched graph has no head-gate arm; a
16385                            // gated layer is refused above.
16386                            head_gate: None,
16387                        }
16388                    }
16389                    AttnKind::LinearGdn(w) => {
16390                        let Some(cfg) = self.gdn_cfg else {
16391                            if batch_debug {
16392                                eprintln!("batch graph: no GDN config at layer {li}");
16393                            }
16394                            return None;
16395                        };
16396                        let (m, _, _, _) = w
16397                            .in_proj_qkv
16398                            .graph_weight()
16399                            .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
16400                        model = Some(m.clone());
16401                        crate::gpu::GraphAttn::Gdn {
16402                            qkv: gw(&w.in_proj_qkv)?,
16403                            z: gw(&w.in_proj_z)?,
16404                            a: gw(&w.in_proj_a)?,
16405                            b: gw(&w.in_proj_b)?,
16406                            out: gw(&w.out_proj)?,
16407                            conv1d: &w.conv1d,
16408                            a_log: &w.a_log,
16409                            dt_bias: &w.dt_bias,
16410                            norm: &w.norm,
16411                            nv: cfg.num_v_heads,
16412                            nk: cfg.num_k_heads,
16413                            dk: cfg.key_head_dim,
16414                            dv: cfg.value_head_dim,
16415                            kk: cfg.conv_kernel,
16416                            cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
16417                            preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
16418                        }
16419                    }
16420                    _ => return None,
16421                };
16422                // Gemma-4's sandwich norms and layer scalar (`gemma_batch`);
16423                // every other model hands the graph none, as before.
16424                let g4 = self.global_attn.is_some() || self.attn_v_norm;
16425                layers.push(crate::gpu::GraphLayer {
16426                    input_norm: &lw.input_norm,
16427                    attn,
16428                    post_norm: &lw.post_norm,
16429                    attn_out_norm: if g4 {
16430                        lw.attn_out_norm.as_deref()
16431                    } else {
16432                        None
16433                    },
16434                    ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
16435                    layer_scale: if g4 { lw.layer_scale } else { None },
16436                    ffn: gffn,
16437                });
16438            }
16439            Some((layers, model?))
16440        })();
16441        let Some((layers, model)) = built else {
16442            {
16443                use std::sync::atomic::{AtomicBool, Ordering};
16444                static SAID: AtomicBool = AtomicBool::new(false);
16445                if !SAID.swap(true, Ordering::Relaxed) {
16446                    tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
16447                }
16448            }
16449            return crate::gpu::BatchGraphOutcome::Declined;
16450        };
16451        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
16452            eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
16453        }
16454        crate::gpu::forward_batch_graph(
16455            &model,
16456            self.graph_kv_id,
16457            &layers,
16458            &self.inv_freq,
16459            hiddens,
16460            nh,
16461            nkv,
16462            hd,
16463            rd,
16464            self.hidden_size,
16465            self.intermediate_size,
16466            positions,
16467            self.kv_cache.max_seq_len,
16468            gemma,
16469            self.rms_eps as f32,
16470            self.attn_scale,
16471            k,
16472            &(0..graph_end)
16473                .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
16474                .collect::<Vec<_>>(),
16475            self.o1_epoch,
16476            spec,
16477            layers_run,
16478        )
16479    }
16480
16481    /// Same, stopping after layer `upto` inclusive (routing probe φ).
16482    /// `CMF_DSV4_DRAFT_PROBE=1` — grade the draft against what the trunk goes on
16483    /// to produce. Off by default; it runs a whole draft per decoded token.
16484    fn draft_probe() -> bool {
16485        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16486        *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
16487    }
16488
16489    /// `CMF_DSV4_DRAFT_PROBE=1`: measure how much of the draft the trunk
16490    /// would have agreed with, WITHOUT verifying or rolling anything back.
16491    ///
16492    /// The number this produces decides the whole speculation design — at
16493    /// acceptance a, a block of B positions yields 1 + a + a² + ... tokens
16494    /// per trunk pass — so it is worth measuring before any of the machinery
16495    /// that would exploit it exists. Each draft is parked with the position
16496    /// it was made at, and graded as the real tokens arrive.
16497    /// `CMF_DSV4_SPEC=1` — the DeepSeek-V4 speculative decode: draft five
16498    /// on the card, verify them in one batched trunk pass, commit the
16499    /// accepted prefix, roll the rest back.
16500    #[cfg(feature = "gpu")]
16501    fn dsv4_spec_on() -> bool {
16502        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16503        *ON.get_or_init(|| {
16504            // Test-only runtime gate: model loading still performs the same
16505            // reservation and trunk packing, which gives rollback parity a
16506            // topology-identical non-speculative control arm.
16507            if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
16508                return v != "0";
16509            }
16510            // An explicit value is a diagnostic force/escape hatch.  With no
16511            // knob, speculation is eligible only when model loading reserved
16512            // its bounded pack.  On small q4tp cards the geometric reserve
16513            // gate deliberately leaves this at zero: trying to build DSpark
16514            // after the exact trunk filled VRAM is both slower and a device
16515            // OOM (measured on A40).
16516            std::env::var("CMF_DSV4_SPEC")
16517                .map(|v| v != "0")
16518                .unwrap_or_else(|_| {
16519                    crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
16520                })
16521        })
16522    }
16523
16524    /// One speculative round at the decode tip. `t_next` is the token the
16525    /// sampler just committed for `next_pos`. Returns the EXTRA accepted
16526    /// tokens (possibly none) and the new position, with `graph_logits`
16527    /// left holding the last accepted position's logits — exactly what the
16528    /// loop top expects. `None` means "speculate not this round": nothing
16529    /// was committed, the caller forwards normally.
16530    #[cfg(feature = "gpu")]
16531    fn dsv4_spec_step(
16532        &mut self,
16533        tip_token: u32,
16534        t_next: u32,
16535        next_pos: usize,
16536        max_extra: usize,
16537        drafted: &mut usize,
16538        accepted_ctr: &mut usize,
16539    ) -> Option<(Vec<u32>, usize)> {
16540        let t_all = std::time::Instant::now();
16541        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
16542            thread_local! {
16543                static LAST: std::cell::Cell<Option<std::time::Instant>> =
16544                    const { std::cell::Cell::new(None) };
16545            }
16546            LAST.with(|l| {
16547                if let Some(prev) = l.get() {
16548                    eprintln!(
16549                        "between rounds {:.1} ms",
16550                        prev.elapsed().as_secs_f64() * 1e3
16551                    );
16552                }
16553                l.set(Some(std::time::Instant::now()));
16554            });
16555        }
16556        if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16557            eprintln!("spec_step: enter pos={next_pos}");
16558        }
16559        let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
16560        let cfg = self.dsv4.as_ref().map(|b| b.2)?;
16561        // The draft state and its capture, armed exactly as the probe does.
16562        if self.dspark.is_none() {
16563            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16564            if t.is_empty() {
16565                return None;
16566            }
16567            crate::dsv4::dspark_arm(&t, cfg.dim);
16568            self.dspark = Some(crate::dsv4::DsparkState::new(
16569                self.dsv4_mtp.len(),
16570                &cfg,
16571                t.len(),
16572            ));
16573        }
16574        let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16575        let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
16576        if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16577            eprintln!("spec_step: the pack was not built (targets {targets:?})");
16578        }
16579        let pack = pack?;
16580        let block = crate::dsv4::dspark_block();
16581        let b_box = self.dsv4.as_mut()?;
16582        let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
16583        let ds = self.dspark.as_mut()?;
16584        // The tip's captures: either this token ran on a normal path that
16585        // filled the thread-local, or the previous spec round left them.
16586        let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
16587        if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
16588            if dbg {
16589                eprintln!("spec_step: no capture");
16590            }
16591            return None;
16592        }
16593        ds.have_hidden = true;
16594        let tip_pos = next_pos.checked_sub(1)?;
16595        let draft_started = std::time::Instant::now();
16596        let mut conf = Vec::new();
16597        let props = crate::dsv4::dspark_draft_gpu(
16598            g,
16599            &self.dsv4_mtp,
16600            &cfg,
16601            ds,
16602            pack,
16603            st.kv_id,
16604            tip_token,
16605            tip_pos,
16606            self.pool.as_deref(),
16607            &mut conf,
16608        );
16609        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
16610        *drafted += block;
16611        if props.is_empty() || props[0] != t_next {
16612            if dbg {
16613                eprintln!(
16614                    "spec_step: draft {} (props0={:?} t_next={t_next})",
16615                    if props.is_empty() {
16616                        "empty"
16617                    } else {
16618                        "miss"
16619                    },
16620                    props.first()
16621                );
16622            }
16623            return None;
16624        }
16625        // `fed[0]` is `t_next`, which the outer loop has already committed;
16626        // only `fed[1..]` become additional output tokens. Cap the verify
16627        // transaction itself to the caller's remaining output budget instead
16628        // of merely truncating the returned vector: otherwise the KV/state
16629        // would advance past `max_tokens` and a 64-token request could return
16630        // 66 tokens (and poison a reused session with two invisible steps).
16631        let mut k_verify = crate::dsv4::dspark_verify_k()
16632            .min(props.len())
16633            .min(max_extra.saturating_add(1));
16634        // Adaptive depth: positions the draft itself doubts are paid for on
16635        // every verify and delivered almost never (natural-text survival
16636        // [.67 .50 .29 .08 .04]). `CMF_DSPARK_CONF_MIN=p` trims the fed
16637        // prefix at the first proposal whose confidence drops below p; on
16638        // predictable text the confidences stay high and nothing changes.
16639        let conf_min = {
16640            static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
16641            *M.get_or_init(|| {
16642                std::env::var("CMF_DSPARK_CONF_MIN")
16643                    .ok()
16644                    .and_then(|v| v.parse().ok())
16645                    .unwrap_or(0.0)
16646            })
16647        };
16648        if conf_min > 0.0 && conf.len() >= props.len() {
16649            let mut keep = 1usize;
16650            while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
16651                keep += 1;
16652            }
16653            k_verify = k_verify.min(keep.max(2));
16654        }
16655        if k_verify < 2 {
16656            return None;
16657        }
16658        let mut fed = Vec::with_capacity(k_verify);
16659        fed.push(t_next);
16660        fed.extend_from_slice(&props[1..k_verify]);
16661        let mut argmax = Vec::new();
16662        let mut logits_all = Vec::new();
16663        let mut walked = Vec::new();
16664        let txn = crate::dsv4::dsv4_verify_chunk(
16665            g,
16666            layers,
16667            &cfg,
16668            st,
16669            &fed,
16670            next_pos,
16671            &self.inv_freq,
16672            self.pool.as_deref(),
16673            &targets,
16674            &mut argmax,
16675            &mut logits_all,
16676            &mut walked,
16677        );
16678        if txn.is_none() && dbg {
16679            eprintln!("spec_step: verify declined");
16680        }
16681        let txn = txn?;
16682        let spec_gpu_end = txn.gpu_end;
16683        let b = fed.len();
16684        let mut accepted = 1usize;
16685        while accepted < b && fed[accepted] == argmax[accepted - 1] {
16686            accepted += 1;
16687        }
16688        // `CMF_DSV4_SPEC_FORCE_REJECT=1` — accept nothing beyond the known
16689        // token, every round: the pure rollback exerciser. The output must
16690        // stay byte-identical to the plain walk; anything else is a
16691        // transaction bug, isolated from the acceptance logic.
16692        if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
16693            accepted = 1;
16694        }
16695        if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
16696            eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
16697        }
16698        let t_fin = std::time::Instant::now();
16699        if !crate::dsv4::dsv4_spec_finish(
16700            g,
16701            layers,
16702            &cfg,
16703            st,
16704            txn,
16705            accepted,
16706            &fed,
16707            &self.inv_freq,
16708            self.pool.as_deref(),
16709        ) {
16710            tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
16711            return None;
16712        }
16713        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
16714            eprintln!(
16715                "finish(k={accepted}): {:.1} ms",
16716                t_fin.elapsed().as_secs_f64() * 1e3
16717            );
16718        }
16719        *accepted_ctr += accepted - 1;
16720        // Captures per accepted token: device targets photographed by the
16721        // batch, host targets from the verify's own walk. The last one
16722        // becomes the new tip's draft input; every one owes the ring an
16723        // entry for its position.
16724        let (hc, dim) = (cfg.hc_mult, cfg.dim);
16725        // Complete-chain layers are photographed by the fused submission;
16726        // partial device layers overwrite that slot after exact host cold-
16727        // expert correction.  Thus every target in the contiguous device
16728        // prefix has a valid per-token capture.
16729        let dev_caps: Vec<usize> = targets
16730            .iter()
16731            .copied()
16732            .filter(|&t| t < spec_gpu_end)
16733            .collect();
16734        let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
16735        if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
16736            return None;
16737        }
16738        for t in 0..accepted {
16739            let tip = t + 1 == accepted;
16740            for (slot, &tl) in targets.iter().enumerate() {
16741                if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
16742                    let lo = (di * b + t) * hc * dim;
16743                    crate::dsv4::dspark_capture(
16744                        &caps_all[lo..lo + hc * dim],
16745                        &cfg,
16746                        slot,
16747                        &mut ds.main_hidden,
16748                    );
16749                } else if tip
16750                    && crate::dsv4::dspark_peek_slot(slot, dim, {
16751                        let lo = slot * dim;
16752                        &mut ds.main_hidden[lo..lo + dim]
16753                    })
16754                {
16755                    // The tip's host-layer captures are the walk's own
16756                    // per-layer notes — exact. (The walk that ran last ended
16757                    // on exactly this token, on both the accept-all and the
16758                    // rollback path.)
16759                } else {
16760                    // Intermediate tokens: the post-tail state stands in for
16761                    // the per-layer capture on host targets below the last
16762                    // layer. Ring-entry quality only; the tip is exact.
16763                    crate::dsv4::dspark_capture(
16764                        &walked[t * hc * dim..(t + 1) * hc * dim],
16765                        &cfg,
16766                        slot,
16767                        &mut ds.main_hidden,
16768                    );
16769                }
16770            }
16771            crate::dsv4::dspark_ring_append(
16772                g,
16773                &self.dsv4_mtp,
16774                &cfg,
16775                ds,
16776                next_pos + t,
16777                self.pool.as_deref(),
16778            );
16779        }
16780        let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
16781        self.graph_logits = Some(row);
16782        // The speculative loop never runs the probe, so the trunk tally has
16783        // no other place to cycle. Armed only when someone asked for the
16784        // dump; the host tail is the only tallying path here, which is
16785        // precisely the population a partial pack would serve.
16786        if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
16787            crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
16788            crate::dsv4::pick_tally_arm();
16789        }
16790        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
16791            eprintln!(
16792                "spec_step total {:.1} ms (k={accepted})",
16793                t_all.elapsed().as_secs_f64() * 1e3
16794            );
16795        }
16796        Some((fed[1..accepted].to_vec(), next_pos + accepted))
16797    }
16798
16799    fn dspark_probe(&mut self, position: usize, token_id: u32) {
16800        if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
16801            return;
16802        }
16803        // What the trunk just routed to, for this token.
16804        let trunk_now = crate::dsv4::pick_tally_take();
16805        crate::dsv4::trunk_freq_note(&trunk_now);
16806        if !trunk_now.is_empty() {
16807            self.dspark_trunk_picks.push(trunk_now);
16808            let keep = crate::dsv4::dspark_block();
16809            if self.dspark_trunk_picks.len() > keep {
16810                self.dspark_trunk_picks.remove(0);
16811            }
16812        }
16813        // Grade whatever is waiting: the token just decoded sits at
16814        // `position`, so it answers the draft made at `position - 1 - i`.
16815        for p in std::mem::take(&mut self.dspark_pending) {
16816            let Some(i) = position.checked_sub(p.0 + 1) else {
16817                continue;
16818            };
16819            let mut p = p;
16820            if i < p.1.len() {
16821                if p.2 && p.1[i] == token_id {
16822                    p.3 = i + 1;
16823                } else {
16824                    p.2 = false;
16825                }
16826                if i + 1 < p.1.len() {
16827                    self.dspark_pending.push(p);
16828                    continue;
16829                }
16830            }
16831            self.dspark_hist.push(p.3);
16832            self.dspark_real.push(token_id);
16833        }
16834        let Some(b) = &mut self.dsv4 else { return };
16835        let (g, layers, cfg) = (&b.0, &b.1, b.2);
16836        let n_layers = layers.len();
16837        if self.dspark.is_none() {
16838            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16839            if t.is_empty() {
16840                return;
16841            }
16842            eprintln!(
16843                "DSpark: capture from layers {t:?}, block {}",
16844                crate::dsv4::dspark_block()
16845            );
16846            crate::dsv4::dspark_arm(&t, cfg.dim);
16847            self.dspark = Some(crate::dsv4::DsparkState::new(
16848                self.dsv4_mtp.len(),
16849                &cfg,
16850                t.len(),
16851            ));
16852        }
16853        let ds = self.dspark.as_mut().unwrap();
16854        if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
16855            return; // this token ran on a path that captures nothing
16856        }
16857        let mut conf = Vec::new();
16858        crate::dsv4::pick_tally_arm();
16859        // The trunk has already consumed the adaptive VRAM budget. Until the
16860        // draft owns an explicit bounded device pack, its tensors are an
16861        // out-of-core CPU/disk tier by contract: never let per-op probes try
16862        // to squeeze another multi-gigabyte MTP expert cache onto the card.
16863        let draft_started = std::time::Instant::now();
16864        #[cfg(feature = "gpu")]
16865        let gpu_draft = crate::dsv4::dspark_gpu_on();
16866        #[cfg(not(feature = "gpu"))]
16867        let gpu_draft = false;
16868        let props = if gpu_draft {
16869            #[cfg(feature = "gpu")]
16870            {
16871                let kv_id = b.3.kv_id;
16872                match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
16873                    Some(pk) => crate::dsv4::dspark_draft_gpu(
16874                        g,
16875                        &self.dsv4_mtp,
16876                        &cfg,
16877                        ds,
16878                        pk,
16879                        kv_id,
16880                        token_id,
16881                        position,
16882                        self.pool.as_deref(),
16883                        &mut conf,
16884                    ),
16885                    None => Vec::new(),
16886                }
16887            }
16888            #[cfg(not(feature = "gpu"))]
16889            Vec::new()
16890        } else {
16891            crate::gpu::cpu_scope(|| {
16892                crate::dsv4::dspark_draft(
16893                    g,
16894                    &self.dsv4_mtp,
16895                    &cfg,
16896                    ds,
16897                    token_id,
16898                    position,
16899                    self.pool.as_deref(),
16900                    &mut conf,
16901                )
16902            })
16903        };
16904        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
16905        let draft_picks = crate::dsv4::pick_tally_take();
16906        crate::dsv4::dspark_freq_note(&draft_picks);
16907        // Re-arm for the NEXT trunk token; the probe runs after the forward,
16908        // so this is the only place that can.
16909        crate::dsv4::pick_tally_arm();
16910        if !props.is_empty() {
16911            // Two ratios, side by side: what a batched verify over the trunk
16912            // would read against what it asks for, and the same for the
16913            // draft's three stages. Near 1.0 means a batch amortises nothing.
16914            let (tu, tt) = {
16915                let flat: Vec<(usize, Vec<usize>)> = self
16916                    .dspark_trunk_picks
16917                    .iter()
16918                    .flat_map(|v| v.iter().cloned())
16919                    .collect();
16920                // Per layer, across the window of tokens.
16921                let mut per: std::collections::HashMap<usize, Vec<usize>> =
16922                    std::collections::HashMap::new();
16923                for (li, picks) in flat {
16924                    per.entry(li).or_default().extend(picks);
16925                }
16926                let n = per.len().max(1);
16927                let mut u = 0usize;
16928                let mut t = 0usize;
16929                for (_, v) in per {
16930                    t += v.len();
16931                    u += v.iter().collect::<std::collections::HashSet<_>>().len();
16932                }
16933                (u / n, t / n)
16934            };
16935            let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
16936            self.dspark_exp.push((tu, tt, du, dt));
16937            self.dspark_pending.push((position, props, true, 0));
16938        }
16939        if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
16940            let n = self.dspark_hist.len() as f32;
16941            let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
16942            let block = crate::dsv4::dspark_block();
16943            let mut at = vec![0usize; block + 1];
16944            for &k in &self.dspark_hist {
16945                at[k] += 1;
16946            }
16947            // Prefix survival: S_i = P(the first i positions all held).
16948            let mut surv = Vec::with_capacity(block);
16949            for i in 1..=block {
16950                let k = at[i..].iter().sum::<usize>() as f32 / n;
16951                surv.push(format!("{k:.2}"));
16952            }
16953            let distinct = self
16954                .dspark_real
16955                .iter()
16956                .collect::<std::collections::HashSet<_>>()
16957                .len();
16958            let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
16959                (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
16960            });
16961            let m = self.dspark_exp.len().max(1);
16962            eprintln!(
16963                "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
16964                 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
16965                self.dspark_hist.len(),
16966                mean + 1.0,
16967                surv.join(" ")
16968            );
16969            eprintln!(
16970                "DSpark: {distinct} distinct tokens of {} (degeneracy), \
16971                 trunk experts {}/{} per layer over {block} tokens, \
16972                 draft {}/{} per block, draft {:.2} ms/block",
16973                self.dspark_real.len(),
16974                tu / m,
16975                tt / m,
16976                du / m,
16977                dt / m,
16978                self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
16979            );
16980        }
16981    }
16982
16983    fn forward_layers_upto(
16984        &mut self,
16985        hidden: &[f32],
16986        position: usize,
16987        task_mask: Option<&TaskMask>,
16988        upto: Option<usize>,
16989    ) -> Vec<f32> {
16990        // In-process multi-GPU: each segment runs pinned to its card,
16991        // and the only thing crossing the boundary is one hidden vector
16992        // that never leaves this address space. Same layer split the
16993        // network mode does, minus the second process, the socket, the
16994        // serialization and the dir_hash handshake.
16995        if let Some(plan) = self.gpu_plan.clone() {
16996            if upto.is_none() && plan.len() > 1 {
16997                let mut h = hidden.to_vec();
16998                for &(dev, from, upto_incl) in plan.iter() {
16999                    h = crate::gpu::with_device(dev, || {
17000                        self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
17001                    });
17002                }
17003                return h;
17004            }
17005        }
17006        self.forward_layers_span(hidden, position, task_mask, 0, upto)
17007    }
17008
17009    /// Split this pipeline's layer stack across local GPUs: segment i
17010    /// runs on `devices[i]`. Contiguous and even by layer count — the
17011    /// VRAM-weighted planner is the next step, and an uneven card pair
17012    /// is why it will be needed. `None` clears the plan.
17013    pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
17014        self.set_gpu_plan_at(devices, None)
17015    }
17016
17017    /// The same, with an explicit first boundary (`--peer-split`): card
17018    /// 0 takes layers `[0..at)`, the rest split what remains. Uneven
17019    /// cards, or an attention-heavy head, are why this knob exists.
17020    pub fn set_gpu_plan_at(
17021        &mut self,
17022        devices: Option<&[usize]>,
17023        at: Option<usize>,
17024    ) -> Result<(), String> {
17025        let Some(devs) = devices.filter(|d| d.len() > 1) else {
17026            self.gpu_plan = None;
17027            return Ok(());
17028        };
17029        self.split_supported()?;
17030        let n = self.num_layers;
17031        if devs.len() > n {
17032            return Err(format!("{} devices for {n} layers", devs.len()));
17033        }
17034        if let Some(k) = at {
17035            if k == 0 || k >= n {
17036                return Err(format!("split at {k}: the model has {n} layers"));
17037            }
17038            if devs.len() == 2 {
17039                self.gpu_plan = Some(std::sync::Arc::new(vec![
17040                    (devs[0], 0, k - 1),
17041                    (devs[1], k, n - 1),
17042                ]));
17043                return Ok(());
17044            }
17045            return Err(format!(
17046                "an explicit split point takes exactly 2 devices, got {}",
17047                devs.len()
17048            ));
17049        }
17050        let per = n.div_ceil(devs.len());
17051        let mut plan = Vec::with_capacity(devs.len());
17052        let mut from = 0usize;
17053        for &d in devs {
17054            if from >= n {
17055                break;
17056            }
17057            let upto = (from + per - 1).min(n - 1);
17058            plan.push((d, from, upto));
17059            from = upto + 1;
17060        }
17061        self.gpu_plan = Some(std::sync::Arc::new(plan));
17062        Ok(())
17063    }
17064
17065    /// The active in-process split, if any: (device, first layer, last).
17066    pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
17067        self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
17068    }
17069
17070    /// Layer span [from ..= upto] (upto None = last layer): the building
17071    /// block the network pipeline-split rides on. `from > 0` skips the
17072    /// arch escape hatches (the pub `forward_span` refuses those archs
17073    /// first) and the whole-token graph — the plain per-layer loop is
17074    /// the canonical executor for a partial stack.
17075    fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
17076        if let Some(x) = t.as_f32() {
17077            return x.to_vec();
17078        }
17079        let mut out = vec![0.0; t.rows() * t.cols()];
17080        for r in 0..t.rows() {
17081            t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
17082        }
17083        out
17084    }
17085
17086    fn embryo_resident_eligible(&self) -> bool {
17087        // One mixer family per file: vmf_phase (kind 0/1) or
17088        // gated_delta_net (kind 4); the anchors are full (2) or bounded (3).
17089        if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
17090            || self.num_layers != self.physical_layers
17091            || self.loop_final_norm
17092            || self.weights.layers.len() != self.num_layers
17093            || self.head_clusters.is_none()
17094            || self.final_softcap.is_some()
17095            || self.logit_multiplier.is_some()
17096            || self.attn_softcap != 0.0
17097            || self.mtp.is_some()
17098            || self.per_token_stack()
17099            || self.dsv4.is_some()
17100            || self.dsv41.is_some()
17101            || self.qwen4_exp.is_some()
17102            // Dynamic routing swaps FFN weights mid-sequence under the
17103            // packed graph; a blend has no single overlay. A STATIC skill
17104            // (`from_model_with_skill`) is fine: the pack reads the live
17105            // `weights.layers[*].ffn`, i.e. the skill's tensors, and a
17106            // later `set_active_skill` change drops the pack
17107            // (`invalidate_for_weight_change`).
17108            || self.dyn_router.is_some()
17109            || self.dyn_phi_layer.is_some()
17110            || self.dyn_blend_loaded
17111            || self.o1_cfg.is_some()
17112            || self.swa.is_some()
17113            || self.sliding_layers.is_some()
17114            || self.global_attn.is_some()
17115            || self.attention_heads_per_layer.is_some()
17116            || self.attn_v_norm
17117            || self
17118                .kv_cache
17119                .layers
17120                .iter()
17121                .any(|l| l.mode != crate::kv_cache::KvMode::F32)
17122            || self.rope_scale != 1.0
17123            || self.rope_scale_local != 1.0
17124            || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
17125            || self.hidden_size == 0
17126            || self.hidden_size > 1024
17127            || self.intermediate_size > 1024
17128            || self.num_heads == 0
17129            || self.num_kv_heads == 0
17130            || self.num_heads % self.num_kv_heads != 0
17131            || self.num_heads.saturating_mul(self.head_dim) > 1024
17132            || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
17133            || self.vocab_size == 0
17134            || self.kv_cache.max_seq_len == 0
17135            || self.rotary_dim == 0
17136            || self.rotary_dim > self.head_dim
17137            || self.rotary_dim % 2 != 0
17138            || self.inv_freq.len() < self.rotary_dim / 2
17139        {
17140            return false;
17141        }
17142        // The resident shader is deliberately an f32 profile.  Dequantizing
17143        // a Q4/Q8 tensor into the packed buffer would silently change the
17144        // operator relative to the CPU quantized path, so quantized CMFs
17145        // retain the exact ordinary executor instead of claiming parity.
17146        // Measured consequence (RTX PRO 4000, S4 bounded export requantized
17147        // with `cortiq requant --quant q4tp-quantize`): `eligible=false`,
17148        // the generic wgpu whole-token graph refuses too, and the per-op
17149        // path decodes at ~73 tok/s against ~200 tok/s on the CPU q4tp
17150        // path — a q4tp Embryo-O1 file is a CPU artifact today; the
17151        // resident graph serves the f32 export.
17152        if self.weights.lm_head.as_f32().is_none()
17153            || self.weights.embed_tokens.as_f32().is_none()
17154            || self.weights.lm_head.rows() < self.vocab_size
17155            || self.weights.lm_head.cols() != self.hidden_size
17156            || self.weights.embed_tokens.rows() < self.vocab_size
17157            || self.weights.embed_tokens.cols() != self.hidden_size
17158            || self.weights.final_norm.len() != self.hidden_size
17159        {
17160            return false;
17161        }
17162        if let Some(cfg) = self.vmf_cfg {
17163            if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
17164                || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
17165                || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
17166                || cfg.state_len() == 0
17167            {
17168                return false;
17169            }
17170        }
17171        if let Some(g) = self.gdn_cfg {
17172            // The resident GDN kernels (gpu_wgpu.rs `embryo_core_gdn_*`):
17173            // fused projection ≤ 2048 rows, nv·dv ≤ 1024, dk ≤ 128 lanes,
17174            // dv ≤ 256 lanes in vec4 rows, SiLU output gate (the Embryo
17175            // export), same rms eps as the stack.
17176            if g.num_v_heads == 0
17177                || g.num_k_heads == 0
17178                || g.num_v_heads % g.num_k_heads != 0
17179                || g.key_head_dim == 0
17180                || g.key_head_dim > 128
17181                || g.value_head_dim == 0
17182                || g.value_head_dim > 256
17183                || g.value_head_dim % 4 != 0
17184                || g.conv_kernel == 0
17185                || g.num_v_heads > 512
17186                || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
17187                || g.conv_dim() > 2048
17188                || g.conv_dim() % 4 != 0
17189                || g.hidden_size != self.hidden_size
17190                || g.output_gate_sigmoid
17191                || g.rms_eps != self.rms_eps
17192                || g.state_len() == 0
17193            {
17194                return false;
17195            }
17196        }
17197        let mut full_seen = false;
17198        for lw in &self.weights.layers {
17199            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
17200                return false;
17201            }
17202            match &lw.attn {
17203                AttnKind::LinearGdn(w) => {
17204                    let Some(g) = self.gdn_cfg else {
17205                        return false;
17206                    };
17207                    let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
17208                    if w.in_proj_qkv.rows() != g.conv_dim()
17209                        || w.in_proj_qkv.cols() != self.hidden_size
17210                        || w.in_proj_qkv.as_f32().is_none()
17211                        || w.in_proj_z.rows() != nv * dv
17212                        || w.in_proj_z.cols() != self.hidden_size
17213                        || w.in_proj_z.as_f32().is_none()
17214                        || w.in_proj_a.rows() != nv
17215                        || w.in_proj_a.cols() != self.hidden_size
17216                        || w.in_proj_a.as_f32().is_none()
17217                        || w.in_proj_b.rows() != nv
17218                        || w.in_proj_b.cols() != self.hidden_size
17219                        || w.in_proj_b.as_f32().is_none()
17220                        || w.conv1d.len() != g.conv_dim() * kk
17221                        || w.a_log.len() != nv
17222                        || w.dt_bias.len() != nv
17223                        || w.norm.len() != dv
17224                        || w.out_proj.rows() != self.hidden_size
17225                        || w.out_proj.cols() != nv * dv
17226                        || w.out_proj.as_f32().is_none()
17227                    {
17228                        return false;
17229                    }
17230                }
17231                AttnKind::Linear(w) => {
17232                    let Some(cfg) = self.vmf_cfg else {
17233                        return false;
17234                    };
17235                    if w.thq.rows() != cfg.num_heads * cfg.nphase
17236                        || w.thq.cols() != self.hidden_size
17237                        || w.thq.as_f32().is_none()
17238                        || w.thk.rows() != cfg.num_heads * cfg.nphase
17239                        || w.thk.cols() != self.hidden_size
17240                        || w.thk.as_f32().is_none()
17241                        || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
17242                        || w.v_proj.cols() != self.hidden_size
17243                        || w.v_proj.as_f32().is_none()
17244                        || w.out_proj.rows() != self.hidden_size
17245                        || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
17246                        || w.out_proj.as_f32().is_none()
17247                        || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
17248                    {
17249                        return false;
17250                    }
17251                    if let Some((kg, kb)) = &w.k_gate {
17252                        if kg.rows() != cfg.num_heads
17253                            || kg.cols() != self.hidden_size
17254                            || kg.as_f32().is_none()
17255                            || kb.len() != cfg.num_heads
17256                        {
17257                            return false;
17258                        }
17259                    }
17260                    if let Some(conv) = &w.conv {
17261                        if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
17262                            return false;
17263                        }
17264                    }
17265                }
17266                AttnKind::Full {
17267                    wq,
17268                    wk,
17269                    wv,
17270                    wo,
17271                    q_norm,
17272                    k_norm,
17273                    output_gate,
17274                    softplus_gate,
17275                    bias,
17276                } => {
17277                    if full_seen
17278                        || q_norm.is_some()
17279                        || k_norm.is_some()
17280                        || *output_gate
17281                        || softplus_gate.is_some()
17282                        || bias.is_some()
17283                        || wq.as_f32().is_none()
17284                        || wk.as_f32().is_none()
17285                        || wv.as_f32().is_none()
17286                        || wo.as_f32().is_none()
17287                        || wq.rows() != self.num_heads * self.head_dim
17288                        || wk.rows() != self.num_kv_heads * self.head_dim
17289                        || wv.rows() != self.num_kv_heads * self.head_dim
17290                        || wq.cols() != self.hidden_size
17291                        || wk.cols() != self.hidden_size
17292                        || wv.cols() != self.hidden_size
17293                        || wo.rows() != self.hidden_size
17294                        || wo.cols() != self.num_heads * self.head_dim
17295                    {
17296                        return false;
17297                    }
17298                    full_seen = true;
17299                }
17300                AttnKind::Bounded(w) => {
17301                    // The resident bounded attend scores S + W lanes in one
17302                    // 256-lane chunk; the format caps S + W at 160.
17303                    let Some(ac) = self.anchor_core.as_ref() else {
17304                        return false;
17305                    };
17306                    if self.bounded_rope.is_none()
17307                        || w.window != ac.window
17308                        || w.sink != ac.sink
17309                        || w.window == 0
17310                        || w.window + w.sink > 256
17311                        || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
17312                        || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
17313                        || w.wq.as_f32().is_none()
17314                        || w.wk.as_f32().is_none()
17315                        || w.wv.as_f32().is_none()
17316                        || w.wo.as_f32().is_none()
17317                        || w.wq.rows() != self.num_heads * self.head_dim
17318                        || w.wk.rows() != self.num_kv_heads * self.head_dim
17319                        || w.wv.rows() != self.num_kv_heads * self.head_dim
17320                        || w.wq.cols() != self.hidden_size
17321                        || w.wk.cols() != self.hidden_size
17322                        || w.wv.cols() != self.hidden_size
17323                        || w.wo.rows() != self.hidden_size
17324                        || w.wo.cols() != self.num_heads * self.head_dim
17325                    {
17326                        return false;
17327                    }
17328                }
17329                _ => return false,
17330            }
17331            match &lw.ffn {
17332                FfnKind::Dense(d) => {
17333                    if d.act != Act::Silu
17334                        || !d.segs.is_empty()
17335                        || d.gate_proj.as_f32().is_none()
17336                        || d.up_proj.as_f32().is_none()
17337                        || d.down_proj.as_f32().is_none()
17338                        || d.gate_proj.rows() != self.intermediate_size
17339                        || d.gate_proj.cols() != self.hidden_size
17340                        || d.up_proj.rows() != self.intermediate_size
17341                        || d.up_proj.cols() != self.hidden_size
17342                        || d.down_proj.rows() != self.hidden_size
17343                        || d.down_proj.cols() != self.intermediate_size
17344                    {
17345                        return false;
17346                    }
17347                }
17348                FfnKind::Moe(m) => {
17349                    if m.resonance.is_none()
17350                        || m.top_k != 1
17351                        || m.router_sigmoid
17352                        || !m.norm_topk_prob
17353                        || m.expert_bias.is_some()
17354                        || m.routed_scaling != 1.0
17355                        || m.route_tau.is_some()
17356                        || m.shared.is_none()
17357                        || m.mask.is_some()
17358                        || m.per_expert_scale.is_some()
17359                        || m.router_input_norm
17360                        || m.experts.is_empty()
17361                        || m.experts.len() > 8
17362                    {
17363                        return false;
17364                    }
17365                    let r = m.resonance.as_ref().unwrap();
17366                    if r.mu.len() != m.experts.len() * self.hidden_size
17367                        || r.bias.len() != m.experts.len()
17368                        || r.u.len() != m.experts.len() * r.k * self.hidden_size
17369                        || r.k > 128
17370                    {
17371                        return false;
17372                    }
17373                    let Some((shared, gate)) = &m.shared else {
17374                        return false;
17375                    };
17376                    if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
17377                        return false;
17378                    }
17379                    if shared.gate_proj.as_f32().is_none()
17380                        || shared.up_proj.as_f32().is_none()
17381                        || shared.down_proj.as_f32().is_none()
17382                        || shared.gate_proj.rows() != self.intermediate_size
17383                        || shared.gate_proj.cols() != self.hidden_size
17384                        || shared.up_proj.rows() != self.intermediate_size
17385                        || shared.up_proj.cols() != self.hidden_size
17386                        || shared.down_proj.rows() != self.hidden_size
17387                        || shared.down_proj.cols() != self.intermediate_size
17388                    {
17389                        return false;
17390                    }
17391                    for e in &m.experts {
17392                        if e.act != Act::Silu
17393                            || !e.segs.is_empty()
17394                            || e.gate_proj.as_f32().is_none()
17395                            || e.up_proj.as_f32().is_none()
17396                            || e.down_proj.as_f32().is_none()
17397                            || e.gate_proj.rows() != self.intermediate_size
17398                            || e.gate_proj.cols() != self.hidden_size
17399                            || e.up_proj.rows() != self.intermediate_size
17400                            || e.up_proj.cols() != self.hidden_size
17401                            || e.down_proj.rows() != self.hidden_size
17402                            || e.down_proj.cols() != self.intermediate_size
17403                        {
17404                            return false;
17405                        }
17406                    }
17407                }
17408                FfnKind::DenseMoe(_) => return false,
17409            }
17410            if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
17411                return false;
17412            }
17413        }
17414        if full_seen && self.anchor_core.is_some() {
17415            return false;
17416        }
17417        full_seen || self.num_layers > 0
17418    }
17419
17420    /// The resident Embryo graph is the owner of this pipeline's forward:
17421    /// the same gate `forward_layers_span` applies before handing a token
17422    /// to `forward_embryo_graph` (both graph phases on, the explicit
17423    /// opt-in, a wgpu device, no earlier refusal, an eligible stack).
17424    fn embryo_resident_wanted(&self) -> bool {
17425        crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17426            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17427            && matches!(
17428                std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17429                Ok("1") | Ok("parallel")
17430            )
17431            && crate::gpu::enabled_here()
17432            && !self.graph_refused()
17433            && self.embryo_resident_eligible()
17434    }
17435
17436    /// Chunked prefill on the resident graph: `ids` from `start` in
17437    /// chunks of `EMBRYO_CHUNK_MAX`, one submit each, projections/FFN as
17438    /// chunk GEMMs and the recurrent layers walked in time on the device.
17439    /// Returns the last position's logits when the whole span ran there.
17440    /// `None` = refused before any device work (the per-position path
17441    /// takes the span).  `CMF_EMBRYO_CHUNK=0` keeps the per-position
17442    /// prefill (A/B and the parity reference).
17443    fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
17444        if ids.len() < 2
17445            || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
17446            || !self.embryo_resident_wanted()
17447        {
17448            return None;
17449        }
17450        let model = self.ensure_embryo_graph()?;
17451        let cmax = std::env::var("CMF_EMBRYO_CHUNK")
17452            .ok()
17453            .and_then(|v| v.parse::<usize>().ok())
17454            .filter(|&v| v >= 1)
17455            .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
17456            .min(crate::gpu::EMBRYO_CHUNK_MAX);
17457        let hs = self.hidden_size;
17458        let n = ids.len();
17459        let mut pos = start;
17460        let mut last = None;
17461        let mut rows = Vec::with_capacity(cmax * hs);
17462        while pos < n {
17463            let end = (pos + cmax).min(n);
17464            rows.clear();
17465            for &id in &ids[pos..end] {
17466                rows.extend_from_slice(&self.embed_single(id));
17467            }
17468            let mut lg = Vec::new();
17469            if !crate::gpu::forward_embryo_graph_chunk(
17470                &model,
17471                self.graph_kv_id,
17472                &rows,
17473                pos,
17474                end - pos,
17475                &mut lg,
17476            ) {
17477                if pos == start {
17478                    if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17479                        eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
17480                    }
17481                    return None;
17482                }
17483                // The device sequence advanced through the earlier chunks;
17484                // a host continuation would mix two owners of the state.
17485                // Fail this sequence alone, leaving no stale state or key.
17486                self.kv_cache.clear();
17487                self.clear_history();
17488                crate::gpu::graph_kv_reset(self.graph_kv_id);
17489                panic!(
17490                    "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
17491                );
17492            }
17493            last = Some(lg);
17494            pos = end;
17495        }
17496        if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17497            eprintln!(
17498                "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
17499                n - start,
17500                (n - start).div_ceil(cmax)
17501            );
17502        }
17503        last
17504    }
17505
17506    fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
17507        if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
17508            const UMAX: u32 = u32::MAX;
17509            const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
17510            const REC: usize = 64;
17511            struct Pack {
17512                data: Vec<f32>,
17513            }
17514            impl Pack {
17515                fn put(&mut self, x: &[f32]) -> u32 {
17516                    if x.is_empty() {
17517                        return u32::MAX;
17518                    }
17519                    let off = self.data.len();
17520                    self.data.extend_from_slice(x);
17521                    off as u32
17522                }
17523            }
17524            // The mixer family of the file: vmf_phase geometry fills the
17525            // phase header words, gated_delta_net fills words 24..29.  A
17526            // file has exactly one linear core, so at most one is live.
17527            let vmf = self.vmf_cfg;
17528            let gdn = self.gdn_cfg;
17529            let mut pack = Pack { data: Vec::new() };
17530            let mut meta = vec![0u32; HEADER];
17531            meta[0] = self.hidden_size as u32;
17532            meta[1] = self.intermediate_size as u32;
17533            meta[2] = self.vocab_size as u32;
17534            meta[3] = self.num_layers as u32;
17535            meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
17536            meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
17537            meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
17538            if let Some(g) = gdn {
17539                meta[24] = g.num_v_heads as u32;
17540                meta[25] = g.num_k_heads as u32;
17541                meta[26] = g.key_head_dim as u32;
17542                meta[27] = g.value_head_dim as u32;
17543                meta[28] = g.conv_kernel as u32;
17544                meta[29] = g.conv_dim() as u32;
17545            }
17546            meta[7] = self.num_heads as u32;
17547            meta[8] = self.num_kv_heads as u32;
17548            meta[9] = self.head_dim as u32;
17549            meta[10] = self.kv_cache.max_seq_len as u32;
17550            let clusters = self.head_clusters.as_ref().unwrap();
17551            let cluster_count = clusters.len() / self.hidden_size;
17552            if clusters.len() % self.hidden_size != 0
17553                || cluster_count == 0
17554                || cluster_count > 1024
17555                || self.vocab_size % cluster_count != 0
17556                || self.weights.lm_head.rows() < self.vocab_size
17557                || self.weights.final_norm.len() != self.hidden_size
17558            {
17559                return None;
17560            }
17561            meta[11] = cluster_count as u32;
17562            meta[12] = (self.vocab_size / cluster_count) as u32;
17563            meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
17564            meta[16] = self.rotary_dim as u32;
17565            meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
17566            meta[19] = (self.rms_eps as f32).to_bits();
17567            let max_conv = self
17568                .weights
17569                .layers
17570                .iter()
17571                .filter_map(|lw| match &lw.attn {
17572                    AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
17573                    _ => None,
17574                })
17575                .max()
17576                .unwrap_or(1);
17577            // One state slot per recurrent layer: the phase state plus its
17578            // hidden-wide conv ring, or the GDN record `[conv ring | S]`
17579            // (`GdnCfg::state_len`).  A file carries one mixer family, so
17580            // the stride is exactly that family's record and the device
17581            // state buffer equals the header's recurrent bytes.
17582            let phase_stride = vmf
17583                .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
17584                .unwrap_or(0);
17585            let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
17586            let state_stride = phase_stride.max(gdn_stride);
17587            // Bounded genome: the KV plane of an anchor is its ring
17588            // `[kvh][W][hd]` K + V, and only anchors own one.  Legacy full
17589            // anchors keep the `max_seq` planes indexed by layer.
17590            let bounded = self.anchor_core.clone();
17591            let (anchor_window, anchor_sink) = bounded
17592                .as_ref()
17593                .map(|ac| (ac.window, ac.sink))
17594                .unwrap_or((0, 0));
17595            let kv_stride = if bounded.is_some() {
17596                2usize
17597                    .saturating_mul(self.num_kv_heads)
17598                    .saturating_mul(anchor_window)
17599                    .saturating_mul(self.head_dim)
17600            } else {
17601                2usize
17602                    .saturating_mul(self.num_kv_heads)
17603                    .saturating_mul(self.kv_cache.max_seq_len)
17604                    .saturating_mul(self.head_dim)
17605            };
17606            meta[14] = state_stride as u32;
17607            meta[15] = kv_stride as u32;
17608            meta[18] = anchor_window as u32;
17609            meta[20] = anchor_sink as u32;
17610            meta[21] = match &self.bounded_rope {
17611                Some(rope) => {
17612                    // [W][half] cos then [W][half] sin, one contiguous table.
17613                    let off = pack.put(&rope.cos);
17614                    let _ = pack.put(&rope.sin);
17615                    off
17616                }
17617                None => UMAX,
17618            };
17619            let mut full_seen = false;
17620            let mut bounded_seen = 0usize;
17621            // Recurrent state slots belong to mixer layers only (phase or
17622            // GDN): an anchor owns a ring, not a state stride, so the
17623            // device state buffer is exactly the header's recurrent bytes.
17624            let mut phase_seen = 0usize;
17625            let mut gdn_seen = 0usize;
17626            for (li, lw) in self.weights.layers.iter().enumerate() {
17627                let base = meta.len();
17628                meta.resize(base + REC, UMAX);
17629                meta[base] = match &lw.attn {
17630                    AttnKind::Linear(w) if w.phase_delta => 1,
17631                    AttnKind::Linear(_) => 0,
17632                    AttnKind::Full { .. } => 2,
17633                    AttnKind::Bounded(_) => 3,
17634                    AttnKind::LinearGdn(_) => 4,
17635                    _ => UMAX,
17636                };
17637                meta[base + 1] = pack.put(&lw.input_norm);
17638                meta[base + 2] = pack.put(&lw.post_norm);
17639                meta[base + 25] = match &lw.attn {
17640                    AttnKind::Linear(_) => {
17641                        let off = ((phase_seen + gdn_seen) * state_stride) as u32;
17642                        phase_seen += 1;
17643                        off
17644                    }
17645                    AttnKind::LinearGdn(_) => {
17646                        let off = ((phase_seen + gdn_seen) * state_stride) as u32;
17647                        gdn_seen += 1;
17648                        off
17649                    }
17650                    _ => UMAX,
17651                };
17652                match &lw.attn {
17653                    AttnKind::LinearGdn(w) => {
17654                        // Layer record words 56..63 + 29, as the resident
17655                        // kernels read them (gpu_wgpu.rs `embryo_core_gdn_*`).
17656                        meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
17657                        meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
17658                        meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
17659                        meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
17660                        meta[base + 60] = pack.put(&w.conv1d);
17661                        meta[base + 61] = pack.put(&w.a_log);
17662                        meta[base + 62] = pack.put(&w.dt_bias);
17663                        meta[base + 63] = pack.put(&w.norm);
17664                        meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
17665                        meta[base + 24] = 0;
17666                    }
17667                    AttnKind::Linear(w) => {
17668                        meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
17669                        meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
17670                        meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
17671                        meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
17672                        let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
17673                        meta[base + 7] = pack.put(&decay);
17674                        if let Some((kg, kb)) = &w.k_gate {
17675                            meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
17676                            meta[base + 9] = pack.put(kb);
17677                        }
17678                        if let Some(conv) = &w.conv {
17679                            meta[base + 10] = pack.put(conv);
17680                            meta[base + 24] = (conv.len() / self.hidden_size) as u32;
17681                        } else {
17682                            meta[base + 24] = 0;
17683                        }
17684                    }
17685                    AttnKind::Full { wq, wk, wv, wo, .. } => {
17686                        full_seen = true;
17687                        meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
17688                        meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
17689                        meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
17690                        meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
17691                        meta[base + 26] = (li * kv_stride) as u32;
17692                    }
17693                    AttnKind::Bounded(w) => {
17694                        meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
17695                        meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
17696                        meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
17697                        meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
17698                        // Ring slot of this anchor (anchors only, packed).
17699                        meta[base + 26] = (bounded_seen * kv_stride) as u32;
17700                        meta[base + 27] = pack.put(&w.sink_k);
17701                        meta[base + 28] = pack.put(&w.sink_v);
17702                        bounded_seen += 1;
17703                    }
17704                    _ => return None,
17705                }
17706                match &lw.ffn {
17707                    FfnKind::Dense(d) => {
17708                        meta[base + 15] = 0;
17709                        meta[base + 16] = 0;
17710                        meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
17711                        meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
17712                        meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
17713                    }
17714                    FfnKind::Moe(m) => {
17715                        let r = m.resonance.as_ref().unwrap();
17716                        let (shared, _) = m.shared.as_ref().unwrap();
17717                        meta[base + 15] = 1;
17718                        meta[base + 16] = m.experts.len() as u32;
17719                        meta[base + 17] = pack.put(&r.mu);
17720                        meta[base + 18] = pack.put(&r.u);
17721                        meta[base + 19] = pack.put(&r.bias);
17722                        meta[base + 20] = r.k as u32;
17723                        // Word 30: the growth shell as the runtime applies
17724                        // it now (`+inf` on trunk rows, the stored finite
17725                        // shell on grown rows, all `+inf` under
17726                        // `CMF_GROWTH_SHELL=off`), followed by one `−∞`
17727                        // sentinel at index E the kernel writes as the
17728                        // score of an expert outside its shell — WGSL has
17729                        // no infinity literal, so the value travels as
17730                        // data (`embryo_core_route_finalize`).
17731                        let mut shell = r.effective_shell(m.experts.len());
17732                        shell.push(f32::NEG_INFINITY);
17733                        meta[base + 30] = pack.put(&shell);
17734                        meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
17735                        meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
17736                        meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
17737                        for (e, ex) in m.experts.iter().enumerate() {
17738                            meta[base + 32 + e * 3] =
17739                                pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
17740                            meta[base + 33 + e * 3] =
17741                                pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
17742                            meta[base + 34 + e * 3] =
17743                                pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
17744                        }
17745                    }
17746                    FfnKind::DenseMoe(_) => return None,
17747                }
17748            }
17749            if !full_seen && self.num_layers == 0 {
17750                return None;
17751            }
17752            let id = {
17753                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
17754                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
17755            };
17756            let model = crate::gpu::EmbryoGraphModel {
17757                id,
17758                hidden: self.hidden_size,
17759                intermediate: self.intermediate_size,
17760                vocab: self.vocab_size,
17761                layers: self.num_layers,
17762                phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
17763                nphase: vmf.map(|c| c.nphase).unwrap_or(0),
17764                phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
17765                anchor_q_heads: self.num_heads,
17766                anchor_kv_heads: self.num_kv_heads,
17767                anchor_head_dim: self.head_dim,
17768                rotary_dim: self.rotary_dim,
17769                max_seq: self.kv_cache.max_seq_len,
17770                cluster_count,
17771                cluster_size: self.vocab_size / cluster_count,
17772                phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
17773                state_stride,
17774                kv_stride,
17775                norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
17776                phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
17777                weights: pack.data,
17778                meta,
17779                lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
17780                clusters: clusters.as_ref().clone(),
17781                final_norm: self.weights.final_norm.clone(),
17782                inv_freq: self.inv_freq.as_ref().clone(),
17783                bounded: bounded.is_some(),
17784                kv_layers: if bounded.is_some() {
17785                    bounded_seen
17786                } else {
17787                    self.num_layers
17788                },
17789                state_layers: phase_seen + gdn_seen,
17790                anchor_window,
17791                anchor_sink,
17792                phase_layers: phase_seen,
17793                gdn_layers: gdn_seen,
17794                gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
17795                gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
17796                gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
17797                gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
17798                gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
17799            };
17800            self.embryo_graph = Some(std::sync::Arc::new(model));
17801        }
17802        self.embryo_graph.clone()
17803    }
17804
17805    fn forward_layers_span(
17806        &mut self,
17807        hidden: &[f32],
17808        position: usize,
17809        task_mask: Option<&TaskMask>,
17810        from: usize,
17811        upto: Option<usize>,
17812    ) -> Vec<f32> {
17813        #[cfg(target_os = "macos")]
17814        let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
17815            position,
17816            from,
17817            upto.map_or(self.num_layers, |u| u.saturating_add(1)),
17818        );
17819        debug_assert!(
17820            from == 0
17821                || (self.dsv4.is_none()
17822                    && self.dsv41.is_none()
17823                    && self.qwen4_exp.is_none()
17824                    && !self.per_token_stack())
17825        );
17826        // Every plain forward — the whole-token Metal graph (`q1_graph_gpu`
17827        // wraps the GDN owners zero-copy and reallocates them on a size
17828        // change) and the CPU layer loop (reads/swaps `linear_state`) —
17829        // must see the previous speculative commit's asynchronous replay
17830        // complete. One mutex probe when nothing is pending.
17831        #[cfg(target_os = "macos")]
17832        if !crate::gpu_metal::wait_replay() {
17833            self.fail_metal_graph("the pending async replay failed before a plain forward");
17834            return vec![0.0; self.hidden_size];
17835        }
17836        if let Some(b) = &mut self.qwen4_exp {
17837            let _ = (task_mask, upto);
17838            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17839            let mut logits = Vec::new();
17840            crate::qwen4_exp::forward_token(
17841                &b.0,
17842                &b.1,
17843                &b.2,
17844                &mut b.3,
17845                token_id,
17846                position,
17847                &self.inv_freq,
17848                self.pool.as_deref(),
17849                &mut logits,
17850                true,
17851            );
17852            self.graph_logits = Some(logits);
17853            return vec![0.0; self.hidden_size];
17854        }
17855        // DeepSeek-V4 runs its own stack: the state is hc_mult copies, and
17856        // the forward returns LOGITS, not a hidden — the head is inside it
17857        // (the final fold sits between the last layer and the norm). The
17858        // token id rides in `hidden[0]`, written by embed_single, because
17859        // the hash layers route by id rather than by content.
17860        if let Some(b) = &mut self.dsv4 {
17861            let _ = (task_mask, upto);
17862            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17863            let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
17864            st.pos = position;
17865            let mut logits = Vec::new();
17866            crate::dsv4::forward_token(
17867                g,
17868                layers,
17869                &cfg,
17870                st,
17871                token_id,
17872                &self.inv_freq,
17873                self.pool.as_deref(),
17874                &mut logits,
17875            );
17876            self.graph_logits = Some(logits);
17877            self.dspark_probe(position, token_id);
17878            // The caller expects a hidden; the logits went out of band, as
17879            // with the fused lm_head path.
17880            return vec![0.0; self.hidden_size];
17881        }
17882        // DeepSeek-V4.1 owns its complete stack and emits logits out of band.
17883        if let Some(b) = &mut self.dsv41 {
17884            let _ = (task_mask, upto);
17885            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17886            let mut logits = Vec::new();
17887            crate::dsv41::forward_token(
17888                &b.0,
17889                &b.1,
17890                &b.2,
17891                &mut b.3,
17892                token_id,
17893                position,
17894                self.pool.as_deref(),
17895                &mut logits,
17896            );
17897            self.graph_logits = Some(logits);
17898            return vec![0.0; self.hidden_size];
17899        }
17900        // Gemma-3n runs its own stack (4 AltUp replicas don't fit this
17901        // loop); `hidden` is the extended embedding from embed_single.
17902        // Gemma-4 E-series: same hand-off — its dual-geometry attention
17903        // with shared-KV tail layers and the PLE injection live in
17904        // `g4e_forward`.
17905        if let Some(b) = &self.g4e {
17906            let _ = (task_mask, upto);
17907            return crate::g4e::g4e_forward(
17908                &b.0,
17909                &b.1,
17910                hidden,
17911                position,
17912                &mut self.kv_cache.layers,
17913                self.num_heads,
17914                self.num_kv_heads,
17915                self.pool.as_deref(),
17916            );
17917        }
17918        if let Some(b) = &self.g3n {
17919            let _ = (task_mask, upto);
17920            return crate::g3n::g3n_forward(
17921                &b.0,
17922                &b.1,
17923                hidden,
17924                position,
17925                &mut self.kv_cache.layers,
17926                self.num_heads,
17927                self.num_kv_heads,
17928                self.head_dim,
17929                self.pool.as_deref(),
17930            );
17931        }
17932        // Cortiq Embryo owns a separate resident graph: phase recurrent
17933        // state, resonance routing, the GQA anchor KV and hierarchical head
17934        // all execute in one Vulkan submit. It is limited to a complete
17935        // unmasked stack; spans and task masks retain the exact host path.
17936        // `CMF_EMBRYO_DBG=1` names the gate that keeps a token off the
17937        // resident graph — every refusal below is otherwise silent.
17938        if from == 0
17939            && upto.is_none()
17940            && task_mask.is_none()
17941            && self.anchor_core.is_some()
17942            && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
17943        {
17944            static ONCE: std::sync::Once = std::sync::Once::new();
17945            ONCE.call_once(|| {
17946                eprintln!(
17947                    "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
17948                     unsupported={} eligible={}",
17949                    crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
17950                    crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
17951                    std::env::var("CMF_EMBRYO_RESIDENT").ok(),
17952                    crate::gpu::enabled_here(),
17953                    self.graph_refused(),
17954                    self.embryo_resident_eligible(),
17955                );
17956            });
17957        }
17958        if from == 0
17959            && upto.is_none()
17960            && task_mask.is_none()
17961            // Embryo's recurrent/KV state has no host import path.  Do not
17962            // seed it for a prefill-only graph and then silently decode from
17963            // an empty CPU cache; both phases must select the resident owner.
17964            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17965            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17966            // This whole-token Embryo path remains explicitly opt-in.
17967            // `CMF_GPU_WGPU_GRAPH=1` still enables the mature generic graph,
17968            // but must not silently select this model-specific resident path.
17969            && matches!(
17970                std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17971                Ok("1") | Ok("parallel")
17972            )
17973            && crate::gpu::enabled_here()
17974            && !self.graph_refused()
17975            // Sequence owner: past position zero the device continues only
17976            // a sequence it holds. A host-owned sequence (the graph refused
17977            // at its start, or its prefix was prefilled on the host) keeps
17978            // the host path to its end — never a device attempt at p > 0
17979            // over an empty device image.
17980            && (position == 0 || self.device_sequence_position().is_some())
17981            && self.embryo_resident_eligible()
17982            && let Some(model) = self.ensure_embryo_graph()
17983        {
17984            let mut lg = Vec::new();
17985            if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
17986            {
17987                self.graph_logits = Some(lg);
17988                return vec![0.0; self.hidden_size];
17989            }
17990            if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17991                eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
17992            }
17993            // The refusal is THIS pipeline's: falling through once is safe
17994            // at position zero (the host owns the sequence from here),
17995            // while a refusal at a later position of a device-owned
17996            // sequence would mix a host KV/state path with a partial
17997            // device sequence.
17998            self.mark_graph_refused();
17999            if position != 0 {
18000                // Fail this sequence alone and leave nothing stale behind:
18001                // no reuse key, no host or device state for the next
18002                // request on this slot to "extend".
18003                self.kv_cache.clear();
18004                self.clear_history();
18005                crate::gpu::graph_kv_reset(self.graph_kv_id);
18006                panic!(
18007                    "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
18008                );
18009            }
18010        }
18011        let mut h = hidden.to_vec();
18012        // MiMo-V2 expert placement: decided before the graph or the per-op
18013        // arena can claim the budget the expert bank needs.
18014        self.mimo_moe_prepare();
18015        let _mimo_q8 = self
18016            .mimo_moe
18017            .is_on()
18018            .then(crate::qtensor::enter_full_gpu_q8_scope);
18019        // Split borrows: copy scalars / clone handles so the per-layer
18020        // cfg does not hold `&self` while the KV cache is `&mut`.
18021        let (nh, _nkv, _hd, hs, _rd, eps) = (
18022            self.num_heads,
18023            self.num_kv_heads,
18024            self.head_dim,
18025            self.hidden_size,
18026            self.rotary_dim,
18027            self.rms_eps,
18028        );
18029        let pool = self.pool.clone();
18030        // Opt-in wgpu token-graph attention (discrete Vulkan/DX12): the whole
18031        // attention sub-block runs resident in one submit. Off by default.
18032        // Whole-token wgpu graph: eligibility + arbitration.
18033        //  - explicit CMF_GPU_WGPU_GRAPH forces it on/off;
18034        //  - discrete adapters (4090: decode 76 -> 137 tok/s) and GDN
18035        //    hybrids (recurrent state device-resident, no CPU twin to
18036        //    race) TRUST it;
18037        //  - integrated/mobile adapters RACE it against the normal path
18038        //    at generation granularity (gpu::graph_race_*) — tiled
18039        //    mobile GPUs can turn the ~300-dispatch graph into seconds
18040        //    per token, while a fast phone GPU keeps its win.
18041        let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
18042        let graph_on = match graph_env.as_deref() {
18043            Some("0") => false,
18044            Some("prefill") => false, // decode keeps the per-op path
18045            Some(_) => true,
18046            // Unset: same discrete-only default as every other graph
18047            // site. "Is the GPU on" used to stand in here — which made
18048            // the 0.2 tok/s whole-token graph race-eligible on mobile
18049            // adapters and cost 12-14× on first tokens (cmfmobile
18050            // TUNING.md); integrated GPUs keep the per-op probe path.
18051            None => crate::gpu::wgpu_graph_default(),
18052        };
18053        let graph_trusted =
18054            graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
18055        let race_eligible =
18056            graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
18057        let mut tail_start = 0usize;
18058        if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
18059            let t_graph = std::time::Instant::now();
18060            let mut lg = Vec::new();
18061            let mut gl = 0usize;
18062            let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
18063            let declined = built.is_none();
18064            let built = match built {
18065                Some(Ok(hh)) => Some(hh),
18066                Some(Err(())) => {
18067                    // O(1) state was admitted before the device failure; the
18068                    // CPU mirrors are stale by construction.  Clear the whole
18069                    // sequence and stop rather than walking that stale state.
18070                    self.clear_sequence_state();
18071                    self.graph_failed
18072                        .store(true, std::sync::atomic::Ordering::Relaxed);
18073                    self.cancel
18074                        .store(true, std::sync::atomic::Ordering::Relaxed);
18075                    tracing::error!("token graph failed after admission; sequence state cleared");
18076                    return vec![0.0; self.hidden_size];
18077                }
18078                None => None,
18079            };
18080            // Past the transient guards (o1 still collecting, a softcap)
18081            // a refusal is about the weights and will never change —
18082            // remember it instead of walking every layer again next
18083            // token.
18084            if declined && !self.o1_active() && self.attn_softcap == 0.0 {
18085                self.mark_graph_refused();
18086            }
18087            graph_note(built.is_some(), gl, self.num_layers);
18088            if let Some(hh) = built {
18089                let dur = t_graph.elapsed();
18090                if std::env::var("CMF_GRAPH_PROF").is_ok() {
18091                    eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
18092                }
18093                if gl > 0 && gl < self.num_layers {
18094                    // Device prefix: the graph ran layers 0..gl and handed
18095                    // back the boundary hidden — the loop below owns the
18096                    // tail. The prefix layers' KV/state advanced on the
18097                    // device; the tail's advances on the host below. One
18098                    // boundary crossing per token.
18099                    h = hh;
18100                    tail_start = gl;
18101                } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
18102                    if !graph_trusted {
18103                        crate::gpu::graph_race_record(true, dur);
18104                    }
18105                    if !lg.is_empty() {
18106                        // Graph produced logits (final-norm + lm_head folded in) —
18107                        // pad/cap to vocab and hand them to the sampler directly.
18108                        lg.resize(self.vocab_size, 0.0);
18109                        if let Some(c) = self.final_softcap {
18110                            softcap_in_place(&mut lg, c, self.pool.as_deref());
18111                        }
18112                        self.graph_logits = Some(lg);
18113                    }
18114                    return hh;
18115                }
18116                // Hopeless first graph token: discard it and fall through
18117                // to the normal path. Safe exactly here — the prompt KV is
18118                // still CPU-owned (chunked prefill), so recomputing this
18119                // position is exact; the mirror's extra row is never read
18120                // (the race just settled on the normal path).
18121            }
18122        }
18123        // KIMI-LINEAR HAS NO SPLIT BUG. The 2.6× reported from the
18124        // model rotation (12.2 tok/s on one card against 4.6 on two)
18125        // was a single measurement of a model whose arm arbitration is
18126        // borderline, and it did not survive repetition. Three runs an
18127        // arm, same binary, back to back:
18128        //   probe on : 1 GPU 9.5 / 5.7 / 5.9   2 GPU 7.8 / 13.0 / 13.3
18129        //   pinned   : 1 GPU 5.6 / 5.3 / 5.2   2 GPU 3.5 / 4.2 / 3.4
18130        // With the arms pinned the split costs about 1.45×, which is
18131        // what a layer split costs. With the probe free, TWO CARDS RUN
18132        // FASTER — because for this model the CPU arm wins some op
18133        // classes and the probe finds that.
18134        //
18135        // Two things do stand, and both are measured. The token graph
18136        // builds NOTHING here (`covered 0 of 14 layers [0..14)`), so
18137        // every layer walks per-op on either arm — that is where the
18138        // headroom is, not in the split. And this model's benchmark is
18139        // unusable without `CMF_GPU_PROBE=0`: the arbitration alone
18140        // moves it by more than 2×.
18141        //
18142        // Span runs (network split): the graph covers exactly [from..=upto]
18143        // — one submit per SEGMENT per token. No race: its state is global
18144        // and calibrated on full stacks, so spans take the graph only where
18145        // it is trusted by default (discrete adapters / CMF_GPU_WGPU_GRAPH).
18146        let span = from > 0 || upto.is_some();
18147        if span && graph_on && task_mask.is_none() && graph_trusted {
18148            let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
18149            let mut lg = Vec::new();
18150            let mut gl = 0usize;
18151            let span_res =
18152                self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
18153            let span_res = match span_res {
18154                Some(Ok(hh)) => Some(hh),
18155                Some(Err(())) => {
18156                    self.clear_sequence_state();
18157                    self.graph_failed
18158                        .store(true, std::sync::atomic::Ordering::Relaxed);
18159                    self.cancel
18160                        .store(true, std::sync::atomic::Ordering::Relaxed);
18161                    tracing::error!(
18162                        "span token graph failed after admission; sequence state cleared"
18163                    );
18164                    return vec![0.0; self.hidden_size];
18165                }
18166                None => None,
18167            };
18168            graph_note(span_res.is_some(), gl, upto_excl - from);
18169            if std::env::var("CMF_GPU_DEBUG").is_ok() {
18170                // How much of the span the graph actually covered. A
18171                // prefix of nothing means every layer walks per-op and
18172                // the split's extra cost is elsewhere.
18173                static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
18174                if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
18175                    eprintln!(
18176                        "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
18177                        upto_excl - from,
18178                        span_res.is_some()
18179                    );
18180                }
18181            }
18182            if let Some(hh) = span_res {
18183                if gl == upto_excl - from {
18184                    if !lg.is_empty() {
18185                        lg.resize(self.vocab_size, 0.0);
18186                        if let Some(c) = self.final_softcap {
18187                            softcap_in_place(&mut lg, c, self.pool.as_deref());
18188                        }
18189                        self.graph_logits = Some(lg);
18190                    }
18191                    crate::gpu::set_layer(-1);
18192                    return hh;
18193                }
18194                // Partial device prefix of the span: CPU owns the tail.
18195                h = hh;
18196                tail_start = from + gl;
18197            }
18198        }
18199        // Layers the host is about to run whose device mirror moved ahead
18200        // of the host cache (a device prefix that shrank since the prompt,
18201        // a batched-prefill prefix longer than this token's): bring their
18202        // rows over first. One comparison per layer when nothing lags.
18203        let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
18204
18205        // A partial graph is an explicit GPU-prefix / CPU-tail split. Keep
18206        // the tail PURE host-side: letting its QTensor hooks re-enter the
18207        // residency arena streams every omitted layer through Vulkan and the
18208        // driver's freed-allocation cache can grow to the full model size
18209        // (25.4 GiB observed with a 14 GiB budget on Granite 30B Q8_2F).
18210        // With a MiMo expert bank the tail is not a whole-layer host
18211        // stream: its experts run from the bank (never the arena) and its
18212        // projections stay per-op on the device, which the bank's placement
18213        // left room for.
18214        let host_tail = tail_start > from;
18215        let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
18216        let automatic_gpu_prefix = self.automatic_gpu_prefix();
18217
18218        let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
18219        #[cfg(target_os = "macos")]
18220        let mut gpu_skip_until = 0usize;
18221        for li in tail_start.max(from)..self.num_layers {
18222            let _capacity_tail = automatic_gpu_prefix
18223                .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
18224                .map(|_| crate::gpu::enter_cpu_scope());
18225            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU (CMF_GPU_LAYERS)
18226            if let Some(u) = upto {
18227                if li > u {
18228                    break;
18229                }
18230            }
18231            if let Some(mask) = task_mask {
18232                if !mask.layer_alive(li) {
18233                    continue; // dead layer: residual pass-through
18234                }
18235            }
18236            // MiniMax's dynamic tail: attention + pooled MoE of this layer as
18237            // ONE device frame (single-layer graph call), cold experts on
18238            // the host. Any decline falls through to the ordinary host
18239            // layer; a post-admission device failure stops the sequence.
18240            if host_tail && task_mask.is_none() {
18241                match self.m3_pooled_layer(li, position, &mut h) {
18242                    Ok(true) => {
18243                        crate::gpu::set_layer(-1);
18244                        continue;
18245                    }
18246                    Ok(false) => {}
18247                    Err(()) => {
18248                        self.clear_sequence_state();
18249                        self.graph_failed
18250                            .store(true, std::sync::atomic::Ordering::Relaxed);
18251                        self.cancel
18252                            .store(true, std::sync::atomic::Ordering::Relaxed);
18253                        tracing::error!(
18254                            "pooled tail token graph failed after admission; sequence state cleared"
18255                        );
18256                        return vec![0.0; self.hidden_size];
18257                    }
18258                }
18259            }
18260            // Whole-block q1 token graph: a run of consecutive q1
18261            // layers — GDN and full attention — executes with one sync
18262            // per CPU attend instead of per op (macOS/Metal).
18263            #[cfg(target_os = "macos")]
18264            {
18265                if li < gpu_skip_until {
18266                    continue;
18267                }
18268                if task_mask.is_none() {
18269                    let end = self.q1_graph_gpu(li, upto, position, &mut h);
18270                    if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
18271                        // The graph may have mutated device state before a
18272                        // command-buffer error. Never continue with a CPU
18273                        // tail or read a stale host mirror after admission.
18274                        return vec![0.0; self.hidden_size];
18275                    }
18276                    if end > li {
18277                        gpu_skip_until = end;
18278                        // Looped Transformer: the graph stopped at a loop
18279                        // boundary — apply final norm before the next iteration.
18280                        if self.is_loop_end(end - 1) && end < self.num_layers {
18281                            h = inference::rms_norm(
18282                                &h,
18283                                &self.weights.final_norm,
18284                                self.rms_eps,
18285                                self.norm_style,
18286                            );
18287                        }
18288                        continue;
18289                    }
18290                }
18291            }
18292
18293            if task_mask.is_none() {
18294                match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
18295                    crate::gpu::BatchGraphOutcome::Completed => continue,
18296                    crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
18297                    crate::gpu::BatchGraphOutcome::Declined => {}
18298                }
18299            }
18300            #[cfg(feature = "gpu")]
18301            self.pull_lagging_host_kv(li, li + 1, position);
18302            let lw = &self.weights.layers[self.phys_layer(li)];
18303            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
18304                if tp.parse::<usize>().ok() == Some(position) {
18305                    let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
18306                    eprintln!(
18307                        "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
18308                        h[0], h[1]
18309                    );
18310                }
18311            }
18312            // Norm into the pipeline scratch — the returning rms_norm
18313            // allocated twice per layer per token (roadmap §3 P0).
18314            let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
18315            inference::rms_norm_into(
18316                &h,
18317                &lw.input_norm,
18318                self.rms_eps,
18319                self.norm_style,
18320                &mut self.ws.n1,
18321            );
18322            drop(prof);
18323
18324            let attn_out = match &lw.attn {
18325                AttnKind::Mla(w) => {
18326                    let inv_freq_l = self.layer_inv_freq(li);
18327                    let rs = self.layer_rope_scale(li);
18328                    let eps = self.rms_eps;
18329                    let pool = self.pool.clone();
18330                    mla_attention(
18331                        w,
18332                        &self.ws.n1,
18333                        &mut self.kv_cache.layers[li],
18334                        position,
18335                        &inv_freq_l,
18336                        rs,
18337                        eps,
18338                        pool.as_deref(),
18339                    )
18340                }
18341                AttnKind::Linear(w) => {
18342                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
18343                    vmf_phase_forward(
18344                        &self.ws.n1,
18345                        w,
18346                        &cfg,
18347                        &mut self.kv_cache.layers[li].linear_state,
18348                        self.pool.as_deref(),
18349                    )
18350                }
18351                AttnKind::Kda(w) => {
18352                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
18353                    crate::linear_core::kda_forward(
18354                        &self.ws.n1,
18355                        w,
18356                        &cfg,
18357                        &mut self.kv_cache.layers[li].linear_state,
18358                        self.pool.as_deref(),
18359                    )
18360                }
18361                AttnKind::LinearGdn(w) => {
18362                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
18363                    gdn_forward(
18364                        &self.ws.n1,
18365                        w,
18366                        &cfg,
18367                        &mut self.kv_cache.layers[li].linear_state,
18368                        self.pool.as_deref(),
18369                    )
18370                }
18371                AttnKind::ShortConv(w) => {
18372                    let cfg = self
18373                        .short_conv_cfg
18374                        .expect("short-conv layer without short_conv_cfg");
18375                    short_conv_forward(
18376                        &self.ws.n1,
18377                        w,
18378                        &cfg,
18379                        &mut self.kv_cache.layers[li].linear_state,
18380                        self.pool.as_deref(),
18381                    )
18382                }
18383                AttnKind::Bounded(w) => {
18384                    // Natively bounded anchor: insert into the ring, attend
18385                    // over sinks ∪ window. No position, nothing appended.
18386                    let rope = self
18387                        .bounded_rope
18388                        .clone()
18389                        .expect("bounded layer without an installed rotation table");
18390                    let cfg = crate::bounded::BoundedAttnCfg {
18391                        num_heads: self.num_heads,
18392                        num_kv_heads: self.num_kv_heads,
18393                        head_dim: self.head_dim,
18394                        hidden_size: hs,
18395                        scale: self.attn_scale,
18396                        rope: &rope,
18397                        pool: pool.as_deref(),
18398                    };
18399                    crate::bounded::bounded_attention(
18400                        &self.ws.n1,
18401                        w,
18402                        &mut self.kv_cache.layers[li],
18403                        &cfg,
18404                    )
18405                }
18406                AttnKind::Full {
18407                    wq,
18408                    wk,
18409                    wv,
18410                    wo,
18411                    q_norm,
18412                    k_norm,
18413                    output_gate,
18414                    softplus_gate,
18415                    bias,
18416                } if self.kv_cache.layers[li].o1_sealed() => {
18417                    // O(1) override: decode on the sealed Nyström state
18418                    // instead of the growing KV cache.
18419                    let inv_freq_l = self.layer_inv_freq(li);
18420                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18421                    let cfg = QwenAttnCfg {
18422                        num_heads: self.layer_num_heads(li),
18423                        num_kv_heads: nkv_l,
18424                        head_dim: hd_l,
18425                        hidden_size: hs,
18426                        position,
18427                        inv_freq: &inv_freq_l,
18428                        rotary_dim: rd_l,
18429                        scale: self.attn_scale,
18430                        softcap: self.attn_softcap,
18431                        window: None,
18432                        v_norm: self.attn_v_norm,
18433                        qk_norm_after_rope: self.qk_norm_after_rope,
18434                        gate_sigmoid: self.proj_gate_sigmoid,
18435                        q_norm: q_norm.as_deref(),
18436                        k_norm: k_norm.as_deref(),
18437                        output_gate: *output_gate,
18438                        softplus_gate: softplus_gate
18439                            .as_ref()
18440                            .map(|(gate, per_head)| (gate, *per_head)),
18441                        rope_scale: self.layer_rope_scale(li),
18442                        bias: bias
18443                            .as_ref()
18444                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18445                        rms_eps: eps,
18446                        norm_style: self.norm_style,
18447                        pool: pool.as_deref(),
18448                        v_head_dim: self.layer_v_dim(li),
18449                    };
18450                    attention::qwen_attention_nystrom(
18451                        &self.ws.n1,
18452                        wq,
18453                        wk,
18454                        wv,
18455                        wo,
18456                        &mut self.kv_cache.layers[li],
18457                        &cfg,
18458                    )
18459                }
18460                AttnKind::Full {
18461                    wq,
18462                    wk,
18463                    wv,
18464                    wo,
18465                    q_norm,
18466                    k_norm,
18467                    output_gate,
18468                    softplus_gate,
18469                    bias,
18470                } => 'attn: {
18471                    // wgpu token-graph attention (opt-in): whole sub-block in
18472                    // one submit, device K/V mirror. q1 only, no gate/bias/mask.
18473                    // Its kernel has no window, sink or narrow-V slot and
18474                    // one mirror geometry: such models stay on the CPU attend.
18475                    let dropin_reason =
18476                        graph_on.then(|| self.graph_attn_decline_reason()).flatten();
18477                    if let Some(reason) = dropin_reason {
18478                        self.note_graph_decline("wgpu attn dropin", reason);
18479                    }
18480                    if graph_on
18481                        && dropin_reason.is_none()
18482                        && !*output_gate
18483                        && softplus_gate.is_none()
18484                        && self.attention_heads_per_layer.is_none()
18485                        && bias.is_none()
18486                        && task_mask.is_none()
18487                    {
18488                        let inv_freq_l = self.layer_inv_freq(li);
18489                        let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18490                        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
18491                        if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
18492                            wq.mapped_q1(),
18493                            wk.mapped_q1(),
18494                            wv.mapped_q1(),
18495                            wo.mapped_q1(),
18496                        ) {
18497                            let gm = gm.clone();
18498                            let mut out = vec![0f32; hs];
18499                            let cache = &self.kv_cache.layers[li];
18500                            if crate::gpu::attn_dropin(
18501                                &gm,
18502                                self.graph_kv_id,
18503                                li,
18504                                &self.ws.n1,
18505                                qi,
18506                                ki,
18507                                vi,
18508                                oi,
18509                                q_norm.as_deref(),
18510                                k_norm.as_deref(),
18511                                self.qk_norm_after_rope,
18512                                &inv_freq_l,
18513                                nh,
18514                                nkv_l,
18515                                hd_l,
18516                                rd_l,
18517                                hs,
18518                                position,
18519                                self.kv_cache.max_seq_len,
18520                                gemma,
18521                                eps as f32,
18522                                cache.k_heads(),
18523                                cache.v_heads(),
18524                                &mut out,
18525                            ) {
18526                                break 'attn out;
18527                            }
18528                        }
18529                    }
18530                    let masked = task_mask
18531                        .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
18532                        .unwrap_or(false);
18533                    let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
18534                    // The masked kernel knows one pipeline-wide geometry and
18535                    // RoPE table, no window and no sink.
18536                    let plain = self.layer_attn_plain(li);
18537                    match (masked, f32_view) {
18538                        // Historical masked path (f32 slices; the loader
18539                        // keeps masked models in f32).
18540                        (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
18541                            let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
18542                            attention::multi_head_attention(
18543                                &self.ws.n1,
18544                                q,
18545                                k,
18546                                v,
18547                                o,
18548                                &mut self.kv_cache.layers[li],
18549                                self.num_heads,
18550                                self.num_kv_heads,
18551                                self.head_dim,
18552                                self.hidden_size,
18553                                position,
18554                                &active_heads,
18555                                &self.inv_freq,
18556                            )
18557                        }
18558                        (masked, _) => {
18559                            if masked {
18560                                tracing::warn!(
18561                                    "layer {li}: head mask on quantized weights or on a \
18562                                     window/sink/per-layer-geometry layer not supported \
18563                                     yet — executing dense"
18564                                );
18565                            }
18566                            let inv_freq_l = self.layer_inv_freq(li);
18567                            let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18568                            let cfg = QwenAttnCfg {
18569                                num_heads: self.layer_num_heads(li),
18570                                num_kv_heads: nkv_l,
18571                                head_dim: hd_l,
18572                                hidden_size: hs,
18573                                position,
18574                                inv_freq: &inv_freq_l,
18575                                rotary_dim: rd_l,
18576                                scale: self.attn_scale,
18577                                softcap: self.attn_softcap,
18578                                window: self.layer_window(li),
18579                                v_norm: self.attn_v_norm,
18580                                qk_norm_after_rope: self.qk_norm_after_rope,
18581                                gate_sigmoid: self.proj_gate_sigmoid,
18582                                q_norm: q_norm.as_deref(),
18583                                k_norm: k_norm.as_deref(),
18584                                output_gate: *output_gate,
18585                                softplus_gate: softplus_gate
18586                                    .as_ref()
18587                                    .map(|(gate, per_head)| (gate, *per_head)),
18588                                rope_scale: self.layer_rope_scale(li),
18589                                bias: bias
18590                                    .as_ref()
18591                                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18592                                rms_eps: eps,
18593                                norm_style: self.norm_style,
18594                                pool: pool.as_deref(),
18595                                v_head_dim: self.layer_v_dim(li),
18596                            };
18597                            attention::qwen_attention(
18598                                &self.ws.n1,
18599                                wq,
18600                                wk,
18601                                wv,
18602                                wo,
18603                                &mut self.kv_cache.layers[li],
18604                                &cfg,
18605                            )
18606                        }
18607                    }
18608                }
18609            };
18610            // Gemma sandwich norm: normalize the attention branch before
18611            // it joins the residual stream.
18612            let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
18613                Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
18614                None => attn_out,
18615            };
18616            let lw = &self.weights.layers[self.phys_layer(li)];
18617            let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
18618            inference::add_rmsnorm_fused_into(
18619                &mut h,
18620                &attn_out,
18621                &lw.post_norm,
18622                self.rms_eps,
18623                self.norm_style,
18624                &mut self.ws.p1,
18625            );
18626            drop(prof);
18627            let mut attn_out = attn_out;
18628            attention::recycle_buf(&mut attn_out);
18629            let post_normed = &self.ws.p1;
18630
18631            let ffn_masked = task_mask
18632                .map(|m| m.ffn_active_count(li) < self.intermediate_size)
18633                .unwrap_or(false);
18634            // One masked dense CONTRACT, dispatched by cost. The
18635            // activation-zeroing arm (the batched sweep's, validated
18636            // against the replica to 0.8%) computes the FULL fused FFN
18637            // and zeroes the dead — right whenever most neurons live.
18638            // The sparse arm reads ONLY active rows and down columns —
18639            // per-row dots are slower per element than the fused kernel,
18640            // so it pays only once the mask is deep enough. The 0.5
18641            // crossover is first-principles (fused kernels run ~2x the
18642            // per-row dot throughput); a shallow specialist (95% alive)
18643            // stays fused, a --target-sparsity bake flips arms on its
18644            // own weight.
18645            let ffn_out = match (ffn_masked, &lw.ffn) {
18646                // A defragged tube layer answers its own mask: the core
18647                // always runs, each tube runs when its bit is on, and
18648                // the tubes that are off are never read from the mmap.
18649                (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
18650                    let row = task_mask
18651                        .and_then(|tm| tm.ffn_masks.get(li))
18652                        .map(|v| v.as_slice());
18653                    tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
18654                }
18655                (true, FfnKind::Dense(d)) => {
18656                    let tm = task_mask.unwrap();
18657                    let alive = tm.ffn_active_count(li);
18658                    let deep = alive * 2 <= self.intermediate_size;
18659                    if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
18660                        let active = tm.ffn_active_indices(li);
18661                        sparse_ffn_quant(
18662                            d,
18663                            post_normed,
18664                            &active,
18665                            self.hidden_size,
18666                            self.pool.as_deref(),
18667                        )
18668                    } else if deep
18669                        && let (Some(g), Some(u), Some(dn)) = (
18670                            d.gate_proj.as_f32(),
18671                            d.up_proj.as_f32(),
18672                            d.down_proj.as_f32(),
18673                        )
18674                    {
18675                        let active = tm.ffn_active_indices(li);
18676                        inference::sparse_ffn_forward(
18677                            post_normed,
18678                            g,
18679                            u,
18680                            dn,
18681                            self.hidden_size,
18682                            self.intermediate_size,
18683                            &active,
18684                            self.pool.as_deref(),
18685                        )
18686                    } else {
18687                        let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
18688                        dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
18689                    }
18690                }
18691                (true, FfnKind::Moe(m)) => {
18692                    // MoE is sparse by expert selection; a task mask
18693                    // narrows the ROUTABLE set via its expert fields
18694                    // (spec §5) when it carries them.
18695                    let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
18696                    ffn_forward(
18697                        &lw.ffn,
18698                        post_normed,
18699                        self.pool.as_deref(),
18700                        allowed.as_deref(),
18701                    )
18702                }
18703                (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
18704                    dm,
18705                    post_normed,
18706                    &h,
18707                    self.rms_eps,
18708                    self.norm_style,
18709                    self.pool.as_deref(),
18710                ),
18711                (false, _) => match &lw.ffn {
18712                    FfnKind::DenseMoe(dm) => dense_moe_ffn(
18713                        dm,
18714                        post_normed,
18715                        &h,
18716                        self.rms_eps,
18717                        self.norm_style,
18718                        self.pool.as_deref(),
18719                    ),
18720                    FfnKind::Moe(m)
18721                        if task_mask.is_none()
18722                            && host_tail
18723                            && li >= tail_start
18724                            && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
18725                    {
18726                        moe_ffn_minicpm_tail(
18727                            &mut self.minicpm_tail,
18728                            li,
18729                            self.num_layers,
18730                            m,
18731                            post_normed,
18732                            self.pool.as_deref(),
18733                        )
18734                    }
18735                    FfnKind::Moe(m)
18736                        if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
18737                    {
18738                        moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
18739                    }
18740                    _ => {
18741                        let allowed = match (&lw.ffn, task_mask) {
18742                            (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
18743                            _ => None,
18744                        };
18745                        ffn_forward(
18746                            &lw.ffn,
18747                            post_normed,
18748                            self.pool.as_deref(),
18749                            allowed.as_deref(),
18750                        )
18751                    }
18752                },
18753            };
18754            let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
18755                Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
18756                None => ffn_out,
18757            };
18758            for (i, &f) in ffn_out.iter().enumerate() {
18759                h[i] += f;
18760            }
18761            let mut ffn_out = ffn_out;
18762            attention::recycle_buf(&mut ffn_out);
18763
18764            // Gemma-4: the layer output is scaled by a learned scalar.
18765            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
18766                for v in h.iter_mut() {
18767                    *v *= sc;
18768                }
18769            }
18770            // CMF_LAYER_DUMP: this position's hidden after layer li.
18771            if self.layer_dump.is_some() {
18772                self.dump_layer_row(position, li, &h);
18773            }
18774
18775            // Looped Transformer: apply final norm at the end of each loop iteration.
18776            // Nanbeige 4.2: after layer 21 (virtual), apply norm before looping back to layer 0.
18777            if self.is_loop_end(li) && li + 1 < self.num_layers {
18778                h = inference::rms_norm(
18779                    &h,
18780                    &self.weights.final_norm,
18781                    self.rms_eps,
18782                    self.norm_style,
18783                );
18784            }
18785
18786            // Dynamic routing φ capture (on-policy): the
18787            // EMA of the post-residual hidden at the router's phi_layer,
18788            // updated as the context evolves during decode.
18789            if self.dyn_phi_layer == Some(li) {
18790                self.update_dyn_phi(&h);
18791            }
18792        }
18793        crate::gpu::set_layer(-1); // layers done — lm_head outside layer-split
18794        if let Some(t) = t_race_cpu {
18795            crate::gpu::graph_race_record(false, t.elapsed());
18796        }
18797
18798        h
18799    }
18800
18801    /// EMA of φ at the router layer (rolling, weight 0.2 = ~5-token
18802    /// horizon). First observation seeds it exactly.
18803    fn update_dyn_phi(&mut self, h: &[f32]) {
18804        const A: f32 = 0.2;
18805        if self.dyn_phi_ema.len() != h.len() {
18806            self.dyn_phi_ema = vec![0.0; h.len()];
18807            self.dyn_phi_seen = 0;
18808        }
18809        if self.dyn_phi_seen == 0 {
18810            self.dyn_phi_ema.copy_from_slice(h);
18811        } else {
18812            for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
18813                *e = (1.0 - A) * *e + A * v;
18814            }
18815        }
18816        self.dyn_phi_seen += 1;
18817    }
18818
18819    /// Current router φ (EMA at phi_layer); empty until first capture.
18820    pub fn dyn_phi(&self) -> &[f32] {
18821        &self.dyn_phi_ema
18822    }
18823
18824    /// Enable/disable φ capture at the router layer, reset the EMA.
18825    pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
18826        self.dyn_phi_layer = layer;
18827        self.dyn_phi_ema.clear();
18828        self.dyn_phi_seen = 0;
18829    }
18830
18831    /// Skills eligible for dynamic switching: (index, id, phi_layer).
18832    pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
18833        let Some(model) = &self.model else {
18834            return Vec::new();
18835        };
18836        model
18837            .header
18838            .skills
18839            .iter()
18840            .enumerate()
18841            .filter_map(|(i, sk)| {
18842                // A v2 record routes only through the request-level
18843                // backbone-gated decision (its status and gate are
18844                // checked there), never per token.
18845                if sk.is_v2() {
18846                    return None;
18847                }
18848                let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
18849                let sel = sk.selection.as_ref()?;
18850                (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
18851            })
18852            .collect()
18853    }
18854
18855    /// Index of the currently overlaid skill (None = backbone).
18856    pub fn active_skill(&self) -> Option<usize> {
18857        self.dyn_active
18858    }
18859
18860    /// Enable dynamic per-token skill routing: build the hysteresis
18861    /// router from the container's routable skills, start φ capture at
18862    /// their (shared) phi_layer. Returns the number of routable skills
18863    /// (0 = nothing to route; router stays off). Idempotent.
18864    pub fn enable_dynamic_routing(&mut self) -> usize {
18865        use crate::swarm::{DynRouter, RoutableSkill};
18866        let Some(model) = self.model.clone() else {
18867            return 0;
18868        };
18869        // Router policy v2 (spec §9.4) routes per REQUEST: the backbone is
18870        // the default and only the backbone-gated decision may pick a
18871        // skill. A per-token switch would bypass that gate (and change
18872        // the O(1) state mid-sequence), so the hysteresis router never
18873        // runs on such a file; the caller keeps the request-level
18874        // decision.
18875        if let Some(r) = &model.header.router {
18876            tracing::warn!(
18877                "dynamic routing disabled: this file declares router policy '{}' with \
18878                 granularity \"{}\" — the request-level decision applies instead",
18879                r.policy,
18880                r.granularity
18881            );
18882            return 0;
18883        }
18884        // Format-v2 skill records (bit SKILLS_V2) without a router policy:
18885        // their status/gate contract ("auto-routing requires active +
18886        // measured") lives in the backbone-gated decision only — the
18887        // hysteresis router would switch into a quarantined record
18888        // (fail-open). Refuse the whole file, not just its v2 records.
18889        if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
18890            || model.header.skills.iter().any(|s| s.is_v2())
18891        {
18892            tracing::warn!(
18893                "dynamic routing disabled: this file carries format-v2 skill records \
18894                 (SKILLS_V2) — they route per request through a router policy only"
18895            );
18896            return 0;
18897        }
18898        // A blend materialized f32 working tensors into the layers; there
18899        // is no single skill index to revert from → refuse (honest).
18900        if self.dyn_blend_loaded {
18901            tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
18902            return 0;
18903        }
18904        // A statically-overlaid skill that is NOT FFN-eligible can't be
18905        // cheaply reverted at generation start → refuse rather than
18906        // silently keep it overlaid.
18907        if let Some(a) = self.dyn_active {
18908            if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
18909                tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
18910                return 0;
18911            }
18912        }
18913        let hidden = self.hidden_size;
18914        let mut skills = Vec::new();
18915        for (idx, id, _phi) in self.dynamic_skills() {
18916            if let Some(sel) = model.header.skills[idx].selection.as_ref() {
18917                if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
18918                    skills.push(rs);
18919                }
18920            }
18921        }
18922        if skills.is_empty() {
18923            return 0;
18924        }
18925        // Skills should share a phi_layer; warn (not fail) if they don't.
18926        let phi = skills[0].phi_layer;
18927        if skills.iter().any(|s| s.phi_layer != phi) {
18928            tracing::warn!("routable skills disagree on phi_layer; using {phi}");
18929        }
18930        let n = skills.len();
18931        self.set_dyn_phi_layer(Some(phi));
18932        self.dyn_router = Some(DynRouter::new(skills));
18933        n
18934    }
18935
18936    /// Human-readable switch log from the last dynamic-routed generation.
18937    pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
18938        self.dyn_router
18939            .as_ref()
18940            .map(|r| r.switches.clone())
18941            .unwrap_or_default()
18942    }
18943
18944    /// LM head: hidden → logits [vocab_size]. The dominant matvec of
18945    /// every decode step — row-parallel on the worker pool.
18946    fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
18947        let _mimo_q8 = self
18948            .mimo_moe
18949            .is_on()
18950            .then(crate::qtensor::enter_full_gpu_q8_scope);
18951        let rows = self.weights.lm_head.rows();
18952        let mut logits = attention::take_buf(rows.min(self.vocab_size));
18953        // Banked MiMo uses the same exact projection family for the
18954        // plain/draft head and the batched verification head. Read both
18955        // scale planes in-place instead of preparing per-op scale buffers.
18956        let served = self.mimo_moe.is_on()
18957            && crate::gpu::mimo_q8_short_enabled()
18958            && rows == self.vocab_size
18959            && !self.weights.lm_head.has_prism_contract()
18960            && self
18961                .weights
18962                .lm_head
18963                .graph_weight()
18964                .is_some_and(|(model, idx, kind, _)| {
18965                    kind == 7
18966                        && crate::gpu::q82_short_rows(
18967                            model,
18968                            idx,
18969                            hidden,
18970                            1,
18971                            rows,
18972                            self.hidden_size,
18973                            &mut logits,
18974                        )
18975                });
18976        if !served {
18977            self.weights
18978                .lm_head
18979                .matvec(hidden, &mut logits, self.pool.as_deref());
18980        }
18981        logits.resize(self.vocab_size, 0.0);
18982        if let Some(m) = self.logit_multiplier {
18983            for l in logits.iter_mut() {
18984                *l *= m;
18985            }
18986        }
18987        if let Some(c) = self.final_softcap {
18988            softcap_in_place(&mut logits, c, self.pool.as_deref());
18989        }
18990        if let Some(cm) = self.head_clusters.as_ref() {
18991            self.hierarchical_head_logprobs(hidden, cm, &mut logits);
18992        }
18993        logits
18994    }
18995
18996    /// Two-level head (Cortiq Embryo): in place, logits[v] ← log p(v) =
18997    /// (lc[c] − lse(lc)) + (logit[v] − lse over v's cluster block), c = v / S.
18998    fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
18999        let h = hidden.len();
19000        let ncl = cm.len() / h.max(1);
19001        if ncl == 0 || logits.len() % ncl != 0 {
19002            return;
19003        }
19004        let cs = logits.len() / ncl;
19005        // cluster logits + log-softmax
19006        let mut lc = vec![0.0f32; ncl];
19007        for c in 0..ncl {
19008            let row = &cm[c * h..(c + 1) * h];
19009            let mut s = 0.0f32;
19010            for j in 0..h {
19011                s += row[j] * hidden[j];
19012            }
19013            lc[c] = s;
19014        }
19015        let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19016        let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
19017        for c in 0..ncl {
19018            let blk = &mut logits[c * cs..(c + 1) * cs];
19019            let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19020            let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
19021            let add = lc[c] - lse - bl;
19022            for v in blk.iter_mut() {
19023                *v += add;
19024            }
19025        }
19026    }
19027
19028    /// Prefill `ids` and return the next-token logits — what the model
19029    /// would predict next, WITHOUT committing to generation (introspection
19030    /// for `cortiq explain`). Clears and repopulates the KV cache; leaves
19031    /// the active overlay untouched.
19032    pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
19033        #[cfg(target_os = "macos")]
19034        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
19035        self.clear_sequence_state();
19036        // This helper is used by the pooled classification endpoint, where
19037        // every request is a fresh sequence. The shared reset also clears the
19038        // wgpu token graph's device-side recurrent state.
19039        crate::gpu::graph_race_begin_generation();
19040        if task_mask.is_none() {
19041            self.o1_begin();
19042        }
19043        let mut hidden = vec![0.0f32; self.hidden_size];
19044        for (pos, &id) in ids.iter().enumerate() {
19045            let emb = self.embed_single(id);
19046            hidden = self.forward_layers(&emb, pos, task_mask);
19047        }
19048        if let Err(err) = self.o1_seal_checked() {
19049            self.o1_fail(err);
19050        }
19051        // Stacks that own their head (V4, V4.1, Qwen3.8-Flash-Next, GLM-5)
19052        // return a zero hidden and hand the logits out of band.
19053        if let Some(logits) = self.graph_logits.take() {
19054            return logits;
19055        }
19056        inference::rms_norm_into(
19057            &hidden,
19058            &self.weights.final_norm,
19059            self.rms_eps,
19060            self.norm_style,
19061            &mut self.ws.n1,
19062        );
19063        self.lm_head_forward(&self.ws.n1)
19064    }
19065}
19066
19067/// Convenience: deterministic tiny pipeline for tests.
19068pub fn create_test_pipeline(
19069    hidden_size: usize,
19070    intermediate_size: usize,
19071    num_heads: usize,
19072    num_kv_heads: usize,
19073    head_dim: usize,
19074    num_layers: usize,
19075    vocab_size: usize,
19076) -> Pipeline {
19077    // Small pseudo-random weights: constant weights make attention
19078    // degenerate and hide indexing bugs.
19079    let synth = |n: usize, salt: usize| -> Vec<f32> {
19080        (0..n)
19081            .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
19082            .collect()
19083    };
19084    let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
19085        QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19086    };
19087    let layer_weights: Vec<LayerWeights> = (0..num_layers)
19088        .map(|li| LayerWeights {
19089            input_norm: vec![1.0; hidden_size],
19090            post_norm: vec![1.0; hidden_size],
19091            attn_out_norm: None,
19092            ffn_out_norm: None,
19093            layer_scale: None,
19094            ffn: FfnKind::Dense(DenseFfn {
19095                gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
19096                up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
19097                down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
19098                act: Act::Silu,
19099                down_t: None,
19100                segs: Vec::new(),
19101            }),
19102            attn: AttnKind::Full {
19103                bias: None,
19104                wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
19105                wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
19106                wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
19107                wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
19108                q_norm: None,
19109                k_norm: None,
19110                output_gate: false,
19111                softplus_gate: None,
19112            },
19113        })
19114        .collect();
19115
19116    Pipeline::new(
19117        Tokenizer::byte_level(),
19118        PipelineWeights {
19119            embed_tokens: qt(vocab_size, hidden_size, 100),
19120            layers: layer_weights,
19121            lm_head: qt(vocab_size, hidden_size, 200),
19122            final_norm: vec![1.0; hidden_size],
19123        },
19124        hidden_size,
19125        intermediate_size,
19126        num_heads,
19127        num_kv_heads,
19128        head_dim,
19129        num_layers,
19130        num_layers, // physical_layers = num_layers (non-looped)
19131        false,      // loop_final_norm
19132        vocab_size,
19133        1e-6,
19134        10_000.0,
19135        NormStyle::Qwen,
19136        4096,
19137        SamplerConfig {
19138            seed: Some(42),
19139            ..Default::default()
19140        },
19141    )
19142}
19143
19144/// Batched dense-FFN: gate/up/down via matmat (element-wise the same
19145/// math as b × dense_ffn — the same dot kernels).
19146/// One mask bit, LSB-first per byte — `TaskMask::ffn_active_indices`'s
19147/// convention.
19148#[inline]
19149fn mask_bit(row: &[u8], j: usize) -> bool {
19150    (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
19151}
19152
19153/// Zero the CLOSED neurons' activations in a [rows × inter] panel — the
19154/// masked-inference fast path's whole trick: full fused quant compute,
19155/// then the mask lands on the ACTIVATIONS, which is arithmetically the
19156/// pruned network without touching a quantized weight byte. Whole open
19157/// bytes (0xFF = 8 open neurons) skip in one test.
19158/// `CMF_FFN_MASK_GAIN` — Patent 12 FIG. 4, variance-preserving
19159/// rescaling: truncation removes a share of the layer's output energy,
19160/// so the survivors are scaled up to put the variance back where the
19161/// downstream norm expects it. A scalar here; per layer it is
19162/// `sqrt(total energy / kept energy)`.
19163fn mask_gain() -> f32 {
19164    static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
19165    *G.get_or_init(|| {
19166        std::env::var("CMF_FFN_MASK_GAIN")
19167            .ok()
19168            .and_then(|v| v.parse().ok())
19169            .unwrap_or(1.0)
19170    })
19171}
19172
19173fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
19174    // With CMF_FFN_MEANFILL a closed neuron contributes its average
19175    // instead of nothing — same bytes read, one constant restored.
19176    let fill = meanfill().and_then(|(i, v)| {
19177        let li = crate::gpu::cur_layer();
19178        (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
19179    });
19180    for r in 0..rows {
19181        let base = r * inter;
19182        for (bi, &byte) in row.iter().enumerate() {
19183            if byte == 0xFF {
19184                continue;
19185            }
19186            let j0 = bi * 8;
19187            for bit in 0..8 {
19188                let j = j0 + bit;
19189                if j < inter && byte & (1 << bit) == 0 {
19190                    g[base + j] = fill.map_or(0.0, |f| f[j]);
19191                }
19192            }
19193        }
19194    }
19195    let gain = mask_gain();
19196    if gain != 1.0 {
19197        for v in g[..rows * inter].iter_mut() {
19198            *v *= gain;
19199        }
19200    }
19201}
19202
19203/// True when neuron `i`'s bit is set (no mask = everything runs).
19204#[inline]
19205fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
19206    row.is_none_or(|r| mask_bit(r, i))
19207}
19208
19209/// Every bit below `n` set — the common case for a tube file's CORE,
19210/// where only the tube bits vary per task.
19211fn all_bits_on(row: &[u8], n: usize) -> bool {
19212    (0..n).all(|i| mask_bit(row, i))
19213}
19214
19215/// `CMF_TUBE_TOPK` — how many tubes a TOKEN may open (0 = the task mask
19216/// decides alone). This is the dense FFN read as a mixture: the tubes
19217/// are the experts a k-means over `gate_proj` rows found, and the token
19218/// picks among them. `CMF_TUBE_SCORE=gate` scores a tube by its own
19219/// gate (realizable: only `up`/`down` of the losers go unread),
19220/// `=oracle` scores by the true `silu(gate)·up` mass (the ceiling —
19221/// only `down` is saved, and the selection has read what it predicts).
19222fn tube_topk() -> usize {
19223    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19224    *K.get_or_init(|| {
19225        std::env::var("CMF_TUBE_TOPK")
19226            .ok()
19227            .and_then(|v| v.parse().ok())
19228            .unwrap_or(0)
19229    })
19230}
19231
19232fn tube_score_oracle() -> bool {
19233    static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19234    *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
19235}
19236
19237/// The routed arm of `tube_ffn`: a token opens only its best `k` tubes.
19238/// At `b == 1` (decode) the losers are genuinely never read — that is
19239/// the speed. At `b > 1` (the scoring sweep) every tube is computed and
19240/// the losers' activations are zeroed instead: same arithmetic, so the
19241/// perplexity is the routed model's, measured without a per-token
19242/// gather in the middle of a GEMM.
19243fn tube_ffn_routed(
19244    d: &DenseFfn,
19245    xs: &[f32],
19246    b: usize,
19247    pool: Option<&Pool>,
19248    mask_row: Option<&[u8]>,
19249    k: usize,
19250) -> Vec<f32> {
19251    let hidden = d.down_proj.rows();
19252    let core = d.gate_proj.rows();
19253    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19254    let mut out = match (b, core_full, mask_row) {
19255        (1, true, _) => dense_ffn(d, xs, pool),
19256        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19257        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19258        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19259    };
19260    let cand: Vec<usize> = (0..d.segs.len())
19261        .filter(|&i| tube_bit(mask_row, d.segs[i].start))
19262        .collect();
19263    if cand.is_empty() {
19264        return out;
19265    }
19266    // gate (and, where the score or the batch needs it, up) per tube.
19267    // The SCORE is taken at the point the serving path could take it:
19268    // off the gate alone, or off the finished activation for the oracle.
19269    let oracle = tube_score_oracle();
19270    let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
19271    let mut scores = vec![0f32; b * cand.len()];
19272    for (ci, &i) in cand.iter().enumerate() {
19273        let seg = &d.segs[i];
19274        let w = seg.width;
19275        let mut g = vec![0.0f32; b * w];
19276        if b == 1 {
19277            seg.gate.matvec(xs, &mut g, pool);
19278        } else {
19279            seg.gate.matmat(xs, b, &mut g, pool);
19280        }
19281        for v in g.iter_mut() {
19282            *v = Act::Silu.combine(*v, 1.0);
19283        }
19284        if !oracle {
19285            for t in 0..b {
19286                scores[t * cand.len() + ci] =
19287                    g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19288            }
19289        }
19290        if oracle || b > 1 {
19291            let mut u = vec![0.0f32; b * w];
19292            if b == 1 {
19293                seg.up.matvec(xs, &mut u, pool);
19294            } else {
19295                seg.up.matmat(xs, b, &mut u, pool);
19296            }
19297            for (a, &v) in g.iter_mut().zip(u.iter()) {
19298                *a *= v;
19299            }
19300            if oracle {
19301                for t in 0..b {
19302                    scores[t * cand.len() + ci] =
19303                        g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19304                }
19305            }
19306        }
19307        acts.push(g);
19308    }
19309    // per-token scores and the winners
19310    let keep = k.min(cand.len());
19311    let mut scratch: Vec<f32> = Vec::new();
19312    for t in 0..b {
19313        let mut sc: Vec<(f32, usize)> = (0..cand.len())
19314            .map(|ci| (scores[t * cand.len() + ci], ci))
19315            .collect();
19316        sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
19317        let mut alive = vec![false; cand.len()];
19318        for &(_, ci) in sc.iter().take(keep) {
19319            alive[ci] = true;
19320        }
19321        if b > 1 {
19322            for (ci, a) in acts.iter_mut().enumerate() {
19323                if !alive[ci] {
19324                    let w = d.segs[cand[ci]].width;
19325                    a[t * w..(t + 1) * w].fill(0.0);
19326                }
19327            }
19328        } else {
19329            // decode: finish only the winners — the losers' up/down
19330            // (and, with the gate score, everything but their gate)
19331            // are never touched.
19332            for (ci, &i) in cand.iter().enumerate() {
19333                if !alive[ci] {
19334                    continue;
19335                }
19336                let seg = &d.segs[i];
19337                let w = seg.width;
19338                let g = &mut acts[ci];
19339                if !tube_score_oracle() {
19340                    scratch.clear();
19341                    scratch.resize(w, 0.0);
19342                    seg.up.matvec(xs, &mut scratch, pool);
19343                    for (a, &v) in g.iter_mut().zip(scratch.iter()) {
19344                        *a *= v;
19345                    }
19346                }
19347                let mut acc = vec![0.0f32; hidden];
19348                seg.down.matvec(g, &mut acc, pool);
19349                for (o, a) in out.iter_mut().zip(&acc) {
19350                    *o += *a;
19351                }
19352            }
19353        }
19354    }
19355    if b > 1 {
19356        for (ci, &i) in cand.iter().enumerate() {
19357            let seg = &d.segs[i];
19358            let mut acc = vec![0.0f32; b * hidden];
19359            seg.down.matmat(&acts[ci], b, &mut acc, pool);
19360            for (o, a) in out.iter_mut().zip(&acc) {
19361                *o += *a;
19362            }
19363        }
19364    }
19365    out
19366}
19367
19368/// FFN of a defragged tube layer: the always-on core plus the tubes the
19369/// task mask switches on. Each tube is a normal tensor triple, so the
19370/// same kernels run it and an inactive tube's bytes are never read —
19371/// that is the whole point of the defrag (a scattered mask cannot skip
19372/// bytes; a contiguous one is just a smaller matrix).
19373fn tube_ffn(
19374    d: &DenseFfn,
19375    xs: &[f32],
19376    b: usize,
19377    pool: Option<&Pool>,
19378    mask_row: Option<&[u8]>,
19379) -> Vec<f32> {
19380    if tube_topk() > 0 {
19381        return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
19382    }
19383    let hidden = d.down_proj.rows();
19384    let core = d.gate_proj.rows();
19385    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19386    let mut out = match (b, core_full, mask_row) {
19387        (1, true, _) => dense_ffn(d, xs, pool),
19388        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19389        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19390        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19391    };
19392    TUBE_SCRATCH.with(|sc| {
19393        let mut sc = sc.borrow_mut();
19394        let [g, u, acc] = &mut *sc;
19395        for seg in &d.segs {
19396            if !tube_bit(mask_row, seg.start) {
19397                continue;
19398            }
19399            let w = seg.width;
19400            g.resize(b * w, 0.0);
19401            if b == 1
19402                && d.act == Act::Silu
19403                && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
19404            {
19405                // g holds silu(gate)·up.
19406            } else {
19407                u.resize(b * w, 0.0);
19408                if b == 1 {
19409                    QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
19410                } else {
19411                    seg.gate.matmat(xs, b, g, pool);
19412                    seg.up.matmat(xs, b, u, pool);
19413                }
19414                for i in 0..b * w {
19415                    g[i] = d.act.combine(g[i], u[i]);
19416                }
19417            }
19418            acc.resize(b * hidden, 0.0);
19419            acc.fill(0.0);
19420            if b == 1 {
19421                seg.down.matvec(g, acc, pool);
19422            } else {
19423                seg.down.matmat(g, b, acc, pool);
19424            }
19425            for (o, a) in out.iter_mut().zip(acc.iter()) {
19426                *o += *a;
19427            }
19428        }
19429        out
19430    })
19431}
19432
19433thread_local! {
19434    /// gate / up / down-accumulator scratch for the tube loop — a tube
19435    /// runs once per layer per token, and a fresh Vec each time is a
19436    /// malloc per tube per layer per token.
19437    static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
19438        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
19439}
19440
19441/// CMF_PREFILL_PROF: cumulative ns of the batched walk's attention and
19442/// FFN halves (all layers, all chunks).
19443static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
19444    std::sync::atomic::AtomicU64::new(0),
19445    std::sync::atomic::AtomicU64::new(0),
19446];
19447
19448/// The wgpu side of `CMF_PREFILL_PROF`: q|k|v GEMMs' enqueue and readback
19449/// wait, the chunk attend's readback wait (ms, cumulative) and the int8
19450/// GEMMs the `zi_mm` route took.
19451fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
19452    #[cfg(feature = "gpu")]
19453    {
19454        use std::sync::atomic::Ordering::Relaxed;
19455        let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
19456        (
19457            ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
19458            ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
19459            ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
19460            crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
19461        )
19462    }
19463    #[cfg(not(feature = "gpu"))]
19464    (0.0, 0.0, 0.0, 0)
19465}
19466
19467/// The wgpu prefill mirror attend's outcome counters
19468/// (`gpu_wgpu::MIRROR_EVENTS`).
19469fn wgpu_mirror_counters() -> [u64; 6] {
19470    #[cfg(feature = "gpu")]
19471    {
19472        crate::gpu_wgpu::MIRROR_EVENTS
19473            .each_ref()
19474            .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
19475    }
19476    #[cfg(not(feature = "gpu"))]
19477    [0; 6]
19478}
19479
19480fn prefill_prof_on() -> bool {
19481    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19482    *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
19483}
19484
19485/// Accumulate the dense FFN activations which feed the down projection.
19486///
19487/// MTP calibration intentionally calls this independently of the trunk's
19488/// `FFN_PROBE` TLS recorder: an MTP sidecar can use a different FFN width,
19489/// and borrowing the trunk's layer-numbered accumulator would silently turn
19490/// draft statistics into trunk proxies.  The caller owns the execution scope
19491/// (the public MTP probe uses `gpu::cpu_scope`) and supplies one accumulator
19492/// per actual MTP FFN channel.
19493pub(crate) fn accumulate_dense_ffn_activation_mass(
19494    d: &DenseFfn,
19495    xs: &[f32],
19496    b: usize,
19497    pool: Option<&Pool>,
19498    mass: &mut [f64],
19499    squared_mass: &mut [f64],
19500) -> Result<(), String> {
19501    let inter = d.gate_proj.rows();
19502    let hidden = d.gate_proj.cols();
19503    if mass.len() != inter || squared_mass.len() != inter {
19504        return Err(format!(
19505            "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
19506            mass.len(),
19507            squared_mass.len()
19508        ));
19509    }
19510    if d.up_proj.rows() != inter
19511        || d.up_proj.cols() != hidden
19512        || d.down_proj.cols() != inter
19513        || d.down_proj.rows() != hidden
19514    {
19515        return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
19516    }
19517    if !d.segs.is_empty() {
19518        return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
19519    }
19520    if xs.len() != b.saturating_mul(hidden) {
19521        return Err(format!(
19522            "MTP FFN probe: {} input values for {b} × {hidden} rows",
19523            xs.len()
19524        ));
19525    }
19526    if b == 0 {
19527        return Ok(());
19528    }
19529    let mut gate = vec![0.0f32; b * inter];
19530    let mut up = vec![0.0f32; b * inter];
19531    d.gate_proj.matmat(xs, b, &mut gate, pool);
19532    d.up_proj.matmat(xs, b, &mut up, pool);
19533    for row in 0..b {
19534        for i in 0..inter {
19535            let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
19536            let a = a as f64;
19537            mass[i] += a.abs();
19538            squared_mass[i] += a * a;
19539        }
19540    }
19541    Ok(())
19542}
19543
19544fn dense_ffn_batch(
19545    d: &DenseFfn,
19546    xs: &[f32],
19547    b: usize,
19548    pool: Option<&Pool>,
19549    mask_row: Option<&[u8]>,
19550) -> Vec<f32> {
19551    let inter = d.gate_proj.rows();
19552    let hidden = d.down_proj.rows();
19553    // Fused on-device SwiGLU when the device is in play: three separate
19554    // `matmat` calls are three round trips per layer, and the gate/up
19555    // panels (b × inter — 22 MB each at a 512-token chunk) cross the bus
19556    // twice for nothing. The kernel already existed for the image DiT;
19557    // the LLM prefill was simply never wired to it. A task mask needs the
19558    // activations on the host between the halves, so it keeps the CPU
19559    // arm below.
19560    // SiLU/GELU have device arms everywhere they are admitted.  MiniMax
19561    // M3's frozen OAI contract has an additional WGPU-only arm; callers on
19562    // native Metal still get an honest decline from `q4_ffn_act` rather than
19563    // an accidental SiLU evaluation.
19564    let fused_act = d.act.wgpu_graph_act();
19565    if mask_row.is_none()
19566        && fused_act.is_some()
19567        && b >= 32
19568        && crate::gpu::enabled_here()
19569        && !crate::gpu::mm_killed()
19570        // The refit pass needs this layer's activations on the host; the
19571        // fused chain keeps them on the device. Refusing it here costs
19572        // one round trip and keeps every GEMM on the card — the
19573        // alternative was running the whole calibration on the CPU.
19574        && refit_dir().is_none()
19575        // Same for the mass/hit probes. The accumulator at the bottom of
19576        // this function only sees `g` when `g` came back to the host, so
19577        // a fused batch would leave it summing nothing — a probe that
19578        // reports zeros rather than failing, which is worse.
19579        && !ffn_probe_active()
19580        // GPTQ calibration folds every projection's input on the host
19581        // (`QTensor::matmat`); the fused chain would hand gate/up/down
19582        // their inputs on the card and leave the FFN uncalibrated.
19583        && !crate::gptq_capture::capturing()
19584    {
19585        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
19586            d.gate_proj.mapped_q4t(),
19587            d.up_proj.mapped_q4t(),
19588            d.down_proj.mapped_q4t(),
19589        ) {
19590            let mut out = vec![0.0f32; b * hidden];
19591            let act = fused_act.expect("checked above");
19592            if crate::gpu::q4_ffn_act(
19593                model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
19594            ) {
19595                return out;
19596            }
19597        }
19598        // The q4tp twin (same kernel family, scale from the row ladder) —
19599        // the DiT has run it in production since the pipeline containers;
19600        // the LLM prefill was simply never wired to it, so a q4tp model's
19601        // prefill panels stayed on the CPU.
19602        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
19603            d.gate_proj.mapped_q4tp(),
19604            d.up_proj.mapped_q4tp(),
19605            d.down_proj.mapped_q4tp(),
19606        ) {
19607            let mut out = vec![0.0f32; b * hidden];
19608            let act = fused_act.expect("checked above");
19609            if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
19610            {
19611                return out;
19612            }
19613        }
19614        // Every other device codec — int8, or gate/up and down in different
19615        // codecs: the three GEMMs with the panels kept on the card. Without
19616        // it a q8_2f prefill read both b·inter panels home, folded them on
19617        // one host thread, and sent the result back for down.
19618        // CMF_FFN_KEEP=0 keeps the per-GEMM path (A/B).
19619        if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
19620            std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
19621            d.gate_proj.mapped_device_gemm(),
19622            d.up_proj.mapped_device_gemm(),
19623            d.down_proj.mapped_device_gemm(),
19624        ) {
19625            let mut out = vec![0.0f32; b * hidden];
19626            let act = fused_act.expect("checked above");
19627            if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
19628                return out;
19629            }
19630        }
19631    }
19632    let mut g = vec![0.0f32; b * inter];
19633    d.gate_proj.matmat(xs, b, &mut g, pool);
19634    let mut u = vec![0.0f32; b * inter];
19635    d.up_proj.matmat(xs, b, &mut u, pool);
19636    if gate_topk() > 0 && d.act == Act::Silu {
19637        for t in 0..b {
19638            let row = &mut g[t * inter..(t + 1) * inter];
19639            for v in row.iter_mut() {
19640                *v = Act::Silu.combine(*v, 1.0);
19641            }
19642            keep_top_k(row, gate_topk());
19643        }
19644        for i in 0..b * inter {
19645            g[i] *= u[i];
19646        }
19647    } else {
19648        for i in 0..b * inter {
19649            g[i] = d.act.combine(g[i], u[i]);
19650        }
19651    }
19652    if let Some(row) = mask_row {
19653        zero_masked_cols(&mut g, b, inter, row);
19654    }
19655    if oracle_topk() > 0 {
19656        for t in 0..b {
19657            keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
19658        }
19659    }
19660    let mut out = vec![0.0f32; b * hidden];
19661    d.down_proj.matmat(&g, b, &mut out, pool);
19662    if refit_dir().is_some() {
19663        let li = crate::gpu::cur_layer();
19664        if li >= 0 {
19665            refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
19666        }
19667    }
19668    // The DTG-MA probe, on the batched path: one prefill sweep gives the
19669    // same per-neuron statistic the per-position probe does, and on a 27B
19670    // that is minutes instead of hours.
19671    FFN_PROBE.with(|pr| {
19672        if let Some(acc) = pr.borrow_mut().as_mut() {
19673            let li = crate::gpu::cur_layer();
19674            if li < 0 {
19675                return;
19676            }
19677            let Some(row) = acc.get_mut(li as usize) else {
19678                return;
19679            };
19680            let sq = probe_sq();
19681            for t in 0..b {
19682                for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
19683                    *a += if sq {
19684                        (v as f64) * (v as f64)
19685                    } else {
19686                        (v as f64).abs()
19687                    };
19688                }
19689            }
19690        }
19691    });
19692    out
19693}
19694
19695/// Batched MoE-FFN: router batched, positions are GROUPED by expert —
19696/// an expert's weights are read once for all its positions in the chunk
19697/// (the main prefill-GEMM win on MoE: 960MB/token of 35B experts).
19698/// Accumulate per-channel activation energy for `CMF_RMS_TRACE`.
19699fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
19700    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19701    static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19702    let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
19703    let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
19704    if (!on && !dump) || b == 0 {
19705        return;
19706    }
19707    let hidden = xs.len() / b;
19708    if on {
19709        let mut acc = m.act_sq.borrow_mut();
19710        if acc.len() < hidden {
19711            acc.resize(hidden, 0.0);
19712        }
19713        for t in 0..b {
19714            let row = &xs[t * hidden..(t + 1) * hidden];
19715            for (a, &v) in acc.iter_mut().zip(row) {
19716                *a += (v as f64) * (v as f64);
19717            }
19718        }
19719    }
19720    if dump {
19721        // Cap the capture: the covariance needs a few thousand rows, and a
19722        // whole prefill of every layer would be gigabytes for no extra rank.
19723        let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
19724            .ok()
19725            .and_then(|v| v.parse().ok())
19726            .unwrap_or(4096);
19727        let mut rows = m.act_rows.borrow_mut();
19728        if rows.len() < cap * hidden {
19729            let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
19730            rows.extend_from_slice(&xs[..take * hidden]);
19731        }
19732    }
19733}
19734
19735/// Send-able cursor over a Vec-of-Vecs: each pool worker writes only its
19736/// own slots (disjoint by construction in the caller).
19737#[derive(Clone, Copy)]
19738struct SendVecs(*mut Vec<f32>);
19739unsafe impl Send for SendVecs {}
19740unsafe impl Sync for SendVecs {}
19741impl SendVecs {
19742    #[inline]
19743    fn at(self, i: usize) -> *mut Vec<f32> {
19744        unsafe { self.0.add(i) }
19745    }
19746}
19747
19748fn moe_ffn_batch(
19749    m: &MoeFfn,
19750    xs: &[f32],
19751    b: usize,
19752    hidden: usize,
19753    pool: Option<&Pool>,
19754    allowed: Option<&[bool]>,
19755) -> Vec<f32> {
19756    accumulate_act(m, xs, b);
19757    let ne = m.experts.len();
19758    let mut logits = vec![0.0f32; b * ne];
19759    match &m.resonance {
19760        Some(r) => {
19761            let hdim = xs.len() / b.max(1);
19762            for bi in 0..b {
19763                r.scores(
19764                    &xs[bi * hdim..(bi + 1) * hdim],
19765                    &mut logits[bi * ne..(bi + 1) * ne],
19766                );
19767            }
19768        }
19769        None => m.router.matmat(xs, b, &mut logits, pool),
19770    }
19771
19772    // Assignments: expert → [(position, weight)] — same routing as
19773    // moe_ffn, per position (see `moe_route`).
19774    let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
19775    {
19776        let mut st = m.stats.borrow_mut();
19777        if st.len() < ne {
19778            st.resize(ne, 0);
19779        }
19780        for bi in 0..b {
19781            let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
19782            for &e in &idx {
19783                st[e] += 1;
19784                assign[e].push((bi, p[e] / wsum));
19785            }
19786        }
19787    }
19788
19789    let mut out = vec![0.0f32; b * hidden];
19790    let cols = m.experts[0].gate_proj.cols();
19791    let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
19792        let sb = list.len();
19793        let mut sub = vec![0.0f32; sb * cols];
19794        for (k, &(bi, _)) in list.iter().enumerate() {
19795            sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
19796        }
19797        let eo = dense_ffn_batch(d, &sub, sb, pool, None);
19798        for (k, &(bi, w)) in list.iter().enumerate() {
19799            for i in 0..hidden {
19800                out[bi * hidden + i] += w * eo[k * hidden + i];
19801            }
19802        }
19803    };
19804    // Routed experts: the panels are TINY (b·top_k spread over every
19805    // expert — a few positions each), so a pool dispatch per expert is
19806    // pure barrier cost. Invert the parallelism: workers take WHOLE
19807    // experts (serial math inside), then one deterministic scatter in
19808    // expert order — the exact accumulation order the serial loop had.
19809    let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
19810    if pool.is_some() && active.len() >= 8 {
19811        let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
19812        {
19813            let panel_ptr = SendVecs(panels.as_mut_ptr());
19814            // Capture only the expert table: `m` itself carries RefCell
19815            // stats and must not cross the pool boundary.
19816            let experts = &m.experts;
19817            let (active_r, assign_r) = (&active, &assign);
19818            let inherit_cpu = crate::gpu::inherit_cpu_scope();
19819            let run = |start: usize, end: usize| {
19820                let _cpu_scope = inherit_cpu();
19821                // Native Metal: the per-op device helpers (`q4tp_ffn`,
19822                // `q8_matmat`, …) stage through size-keyed shared io
19823                // buffers and are not re-entrant — two workers whose experts
19824                // drew the same row count wrote one buffer at once, so a
19825                // Mellum2.1 prompt came out different on every run. The
19826                // workers keep whole experts on the CPU there; the device
19827                // prefill is the MoE chunk graph.
19828                #[cfg(target_os = "macos")]
19829                let _metal_cpu = crate::gpu::enter_cpu_scope();
19830                for ai in start..end {
19831                    let e = active_r[ai];
19832                    let list = &assign_r[e];
19833                    let sb = list.len();
19834                    let mut sub = vec![0.0f32; sb * cols];
19835                    for (k, &(bi, _)) in list.iter().enumerate() {
19836                        sub[k * cols..(k + 1) * cols]
19837                            .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
19838                    }
19839                    // SAFETY: each worker owns a disjoint panels[ai].
19840                    unsafe {
19841                        *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
19842                    }
19843                }
19844            };
19845            match pool {
19846                Some(p) => p.run_rows(active.len(), &run),
19847                None => run(0, active.len()),
19848            }
19849        }
19850        for (ai, &e) in active.iter().enumerate() {
19851            for (k, &(bi, w)) in assign[e].iter().enumerate() {
19852                let eo = &panels[ai][k * hidden..(k + 1) * hidden];
19853                for i in 0..hidden {
19854                    out[bi * hidden + i] += w * eo[i];
19855                }
19856            }
19857        }
19858    } else {
19859        for &e in &active {
19860            run_expert(&m.experts[e], &assign[e], &mut out);
19861        }
19862    }
19863    if let Some((se, gate)) = &m.shared {
19864        let all: Vec<(usize, f32)> = if let Some(gate) = gate {
19865            let mut gl = vec![0.0f32; b];
19866            gate.matmat(xs, b, &mut gl, pool);
19867            (0..b)
19868                .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
19869                .collect()
19870        } else {
19871            (0..b).map(|bi| (bi, 1.0)).collect()
19872        };
19873        run_expert(se, &all, &mut out);
19874    }
19875    out
19876}
19877
19878/// Decode-exact multi-token MoE — the MiMo speculative verify's FFN. Row
19879/// `r` of the result is bit-identical to `moe_ffn(m, x_r)` on the CPU
19880/// (`moe_ffn_cpu` → `moe_ffn_cpu_batched`): router matvec per row, the same
19881/// routing, the same int8 gate/up/SiLU and down terms
19882/// (`QTensor::moe_gate_up_rows` / `moe_down_rows`), and the row's experts
19883/// summed in ITS route order from 0. What the rows share is the weight
19884/// traffic: each routed expert is read once for every row that picked it.
19885/// (`moe_ffn_batch`, the prompt path, groups the same way but sums in
19886/// expert-index order and runs blocked kernels on wide groups — close, not
19887/// bit-equal to decode.) Any layer the kernels do not cover, or a device
19888/// that could answer `moe_ffn` itself, walks `moe_ffn` row by row.
19889fn moe_ffn_rows_exact(
19890    m: &MoeFfn,
19891    xs: &[f32],
19892    b: usize,
19893    hidden: usize,
19894    pool: Option<&Pool>,
19895) -> Vec<f32> {
19896    let mut out = vec![0.0f32; b * hidden];
19897    let per_row = |out: &mut [f32]| {
19898        for r in 0..b {
19899            let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
19900            out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
19901        }
19902    };
19903    let covered = !crate::gpu::enabled_here()
19904        && moe_batch_enabled()
19905        && m.shared.is_none()
19906        && m.resonance.is_none()
19907        && FFN_PROBE.with(|pr| pr.borrow().is_none())
19908        && m.experts.iter().all(|d| d.act == Act::Silu);
19909    if !covered {
19910        per_row(&mut out);
19911        return out;
19912    }
19913    let ne = m.experts.len();
19914    // Routing, row by row, exactly as `moe_ffn`.
19915    let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
19916    for r in 0..b {
19917        let x = &xs[r * hidden..(r + 1) * hidden];
19918        accumulate_act(m, x, 1);
19919        let mut logits = vec![0.0f32; ne];
19920        m.router.matvec(x, &mut logits, pool);
19921        let (idx, p, wsum) = moe_route(&logits, m, None);
19922        {
19923            let mut st = m.stats.borrow_mut();
19924            if st.len() < ne {
19925                st.resize(ne, 0);
19926            }
19927            for &e in &idx {
19928                st[e] += 1;
19929            }
19930        }
19931        let w: Vec<f32> = idx
19932            .iter()
19933            .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
19934            .collect();
19935        routes.push((idx, w));
19936    }
19937    if routes.iter().any(|(idx, _)| idx.is_empty()) {
19938        per_row(&mut out);
19939        return out;
19940    }
19941    // Group the (row, expert) picks by expert, in first-seen order.
19942    let mut experts: Vec<usize> = Vec::new();
19943    let mut groups: Vec<Vec<usize>> = Vec::new();
19944    for (r, (idx, _)) in routes.iter().enumerate() {
19945        for &e in idx {
19946            match experts.iter().position(|&x| x == e) {
19947                Some(g) => groups[g].push(r),
19948                None => {
19949                    experts.push(e);
19950                    groups.push(vec![r]);
19951                }
19952            }
19953        }
19954    }
19955    let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
19956    let inter = m.experts[experts[0]].gate_proj.rows();
19957    let pairs: Vec<(&QTensor, &QTensor)> = experts
19958        .iter()
19959        .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
19960        .collect();
19961    let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
19962    if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
19963        per_row(&mut out);
19964        return out;
19965    }
19966    let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
19967    let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
19968    let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
19969    if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
19970        per_row(&mut out);
19971        return out;
19972    }
19973    // Where each (row, expert) term landed in the flat pair list.
19974    let mut slot = std::collections::HashMap::with_capacity(n_pairs);
19975    let mut p = 0usize;
19976    for (g, &e) in experts.iter().enumerate() {
19977        for &r in &groups[g] {
19978            slot.insert((r, e), p);
19979            p += 1;
19980        }
19981    }
19982    for (r, (idx, w)) in routes.iter().enumerate() {
19983        let terms: Vec<(&[f32], f32)> = idx
19984            .iter()
19985            .zip(w)
19986            .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
19987            .collect();
19988        let row = &mut out[r * hidden..(r + 1) * hidden];
19989        for (i, dst) in row.iter_mut().enumerate() {
19990            // `moe_down_many`'s per-row sum: from 0, in route order.
19991            let mut acc = 0f32;
19992            for (d, we) in &terms {
19993                acc += we * d[i];
19994            }
19995            *dst = acc;
19996        }
19997    }
19998    out
19999}
20000
20001/// Gemma's final-logit soft-capping, `c·tanh(x/c)`, over a whole vocab row —
20002/// element for element the serial expression, split across the pool: at a
20003/// 262k vocab the serial `tanh` pass was ~5 ms of a 36 ms decode token.
20004pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
20005    match pool {
20006        Some(p) if v.len() >= 1 << 15 => {
20007            let base = v.as_mut_ptr() as usize;
20008            p.run_rows(v.len(), &|s, e| {
20009                // SAFETY: run_rows hands out disjoint [s, e) ranges of `v`,
20010                // which outlives the (blocking) dispatch.
20011                let sl =
20012                    unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
20013                for x in sl {
20014                    *x = c * (*x / c).tanh();
20015                }
20016            });
20017        }
20018        _ => {
20019            for x in v.iter_mut() {
20020                *x = c * (*x / c).tanh();
20021            }
20022        }
20023    }
20024}
20025
20026thread_local! {
20027    /// gate/up activation scratch for the dense FFN paths (single uses
20028    /// two slots, the fused pair all four) — these were fresh
20029    /// intermediate-size Vecs on every layer of every token.
20030    static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
20031        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
20032}
20033
20034/// Dense SwiGLU FFN through QTensor matvecs (any storage).
20035fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20036    // Per-token sparsity, when the file was built for it: gate first,
20037    // then only the chosen neurons' up/down rows leave the mmap.
20038    if gate_topk() > 0
20039        && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
20040    {
20041        return out;
20042    }
20043    // Whole-FFN GPU submit (этап 4.2 increment): gate → silu·up → down
20044    // chained in ONE command buffer with the intermediate activations
20045    // resident on the device — 3 per-op polls become 1 per layer. The
20046    // moe_block backend already implements exactly this chain; a dense
20047    // FFN is one expert with weight 1. Runtime probe: the chain still
20048    // pays one submit+poll per layer — alternate it against the pure-CPU
20049    // FFN and keep whichever is faster on this machine.
20050    // q1 FFNs offload at any practical size: the q1 CPU kernel is
20051    // compute-bound, so the UMA threshold logic does not apply — the
20052    // probe measures and decides either way.
20053    // The fused GPU block has no descriptor-aware Prism path: it would either
20054    // consume an unrotated activation or decline after inspecting the mixed
20055    // q2tp/q4tp tensors.  Do not let that structural refusal enter the FFN
20056    // probe's CPU_ONLY scope; the ordinary body below dispatches each matrix
20057    // through QTensor::matvec, which owns the signed FWHT + affine q2tp route.
20058    let prism_body = d.gate_proj.has_prism_contract()
20059        || d.up_proj.has_prism_contract()
20060        || d.down_proj.has_prism_contract();
20061    if !prism_body
20062        && crate::gpu::enabled_here()
20063        && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
20064    {
20065        let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
20066            crate::gpu::ProbeArm::Gpu
20067        } else {
20068            crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
20069        };
20070        match arm {
20071            crate::gpu::ProbeArm::Gpu => {
20072                let t0 = std::time::Instant::now();
20073                if let Some(out) = dense_ffn_gpu(d, x, pool) {
20074                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
20075                    return out;
20076                }
20077                // Declined: no timing exists, so say so. Silence here is
20078                // what left `ffn` undecided for 9000 calls and cost a
20079                // failed device attempt on half of them.
20080                crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
20081            }
20082            crate::gpu::ProbeArm::CpuTimed => {
20083                let t0 = std::time::Instant::now();
20084                let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20085                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
20086                return out;
20087            }
20088            crate::gpu::ProbeArm::Cpu => {
20089                return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20090            }
20091        }
20092    }
20093    dense_ffn_cpu(d, x, pool)
20094}
20095
20096/// The pure-CPU dense-FFN body (also the fallback of every GPU refusal).
20097fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20098    let inter = d.gate_proj.rows();
20099    FFN_SCRATCH.with(|s| {
20100        let mut s = s.borrow_mut();
20101        let [g, u, ..] = &mut *s;
20102        g.resize(inter, 0.0);
20103        // Fused gate+up+silu: one dispatch, no separate silu pass.
20104        // Falls back to matvec_many + silu loop for unsupported dtypes.
20105        if gate_topk() > 0 {
20106            // Gate first, select, and only then pay for `up`: the
20107            // measurement arm computes both and zeroes the losers, which
20108            // is the same arithmetic.
20109            u.resize(inter, 0.0);
20110            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20111            for i in 0..inter {
20112                g[i] = Act::Silu.combine(g[i], 1.0);
20113            }
20114            keep_top_k(g, gate_topk());
20115            for i in 0..inter {
20116                g[i] *= u[i];
20117            }
20118        } else if d.act == Act::Silu && {
20119            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20120            QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
20121        } {
20122            // g now holds silu(gate)·up directly.
20123        } else {
20124            u.resize(inter, 0.0);
20125            // Multi-matrix job: gate+up under one pool dispatch.
20126            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20127            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20128            let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
20129            for i in 0..inter {
20130                g[i] = d.act.combine(g[i], u[i]);
20131            }
20132        }
20133        // DTG-MA bake probe (Patent 2): accumulate this layer's
20134        // per-neuron activation mass while a probe pass is active.
20135        // `CMF_FFN_PROBE_TOPK=k` switches the statistic from mass to a
20136        // HIT COUNT — how many tokens rank the neuron in their own top
20137        // k. Mass asks "how loud is this neuron overall", the count
20138        // asks "how often does this task actually need it", and the two
20139        // rank neurons differently whenever a few tokens are loud.
20140        FFN_PROBE.with(|pr| {
20141            if let Some(acc) = pr.borrow_mut().as_mut() {
20142                let li = crate::gpu::cur_layer();
20143                if li >= 0 {
20144                    if let Some(row) = acc.get_mut(li as usize) {
20145                        match probe_topk() {
20146                            0 if probe_sq() => {
20147                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20148                                    *a += (v as f64) * (v as f64);
20149                                }
20150                            }
20151                            0 if probe_signed() => {
20152                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20153                                    *a += v as f64;
20154                                }
20155                            }
20156                            0 => {
20157                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20158                                    *a += (v as f64).abs();
20159                                }
20160                            }
20161                            k => {
20162                                let n = g.len();
20163                                let k = k.min(n);
20164                                let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
20165                                let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
20166                                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20167                                });
20168                                let thr = *kth;
20169                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20170                                    if v.abs() >= thr {
20171                                        *a += 1.0;
20172                                    }
20173                                }
20174                            }
20175                        }
20176                    }
20177                }
20178            }
20179        });
20180        if oracle_topk() > 0 {
20181            keep_top_k(g, oracle_topk());
20182        }
20183        {
20184            let li = crate::gpu::cur_layer();
20185            if li >= 0 {
20186                adump_row(li as usize, g);
20187            }
20188        }
20189        let mut out = attention::take_buf(d.down_proj.rows());
20190        let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
20191        d.down_proj.matvec(g, &mut out, pool);
20192        out
20193    })
20194}
20195
20196/// Online accumulators for the AWNP refit of a narrowed FFN.
20197///
20198/// The refit needs `Gss = A_SᵀA_S` and `YA = YᵀA_S` per layer, where `A_S`
20199/// are the calibration activations of the KEPT neurons and `Y` the full
20200/// FFN output. Both are small enough to hold; the thing that is not is
20201/// the activations they are built from — a 27B layer would dump a
20202/// gigabyte per thousand tokens. So they are accumulated as the
20203/// calibration runs and written once at the end.
20204///
20205/// `CMF_FFN_REFIT=<dir>` holds `support.<L>.u32` (a u32 count then the
20206/// kept indices) for every layer to accumulate; `CMF_FFN_REFIT_FROM/TO`
20207/// bound the layer span so the accumulators fit in RAM.
20208pub struct RefitAcc {
20209    pub support: Vec<u32>,
20210    pub gss: Vec<f32>,
20211    pub ya: Vec<f32>,
20212    pub hidden: usize,
20213    pub tokens: u64,
20214    /// Activations staged transposed ([ns, t] and [hidden, t]) until the
20215    /// batch is worth a GEMM. The product costs `ns²` to move and add
20216    /// REGARDLESS of how many tokens went into it, so folding 16 chunks
20217    /// into one call cuts that cost 16× — it was 15 TB of traffic per
20218    /// calibration pass at one call per 256 tokens.
20219    pub buf_g: Vec<f32>,
20220    pub buf_o: Vec<f32>,
20221    pub buf_t: usize,
20222}
20223
20224/// The product buffer is SHARED across layers — one 473 MB allocation,
20225/// not one per layer (that was 30 GB of nothing on a 64-layer model).
20226/// It lives under the same lock as the accumulators.
20227type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
20228
20229static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
20230    std::sync::OnceLock::new();
20231
20232/// Is an FFN probe accumulator installed on this thread? The fused GPU
20233/// FFN must decline while one is, or the probe silently measures zero.
20234fn ffn_probe_active() -> bool {
20235    FFN_PROBE.with(|p| p.borrow().is_some())
20236}
20237
20238fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
20239    REFIT
20240        .get_or_init(|| {
20241            std::env::var("CMF_FFN_REFIT").ok().map(|d| {
20242                (
20243                    d,
20244                    std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
20245                )
20246            })
20247        })
20248        .as_ref()
20249}
20250
20251/// Accumulate one prefill panel into the layer's refit statistics.
20252fn refit_accumulate(
20253    li: usize,
20254    g: &[f32],
20255    b: usize,
20256    inter: usize,
20257    out: &[f32],
20258    hidden: usize,
20259    pool: Option<&Pool>,
20260) {
20261    let Some((dir, map)) = refit_dir() else {
20262        return;
20263    };
20264    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20265    let (from, to) = *SPAN.get_or_init(|| {
20266        let g = |k: &str, d: usize| {
20267            std::env::var(k)
20268                .ok()
20269                .and_then(|v| v.parse().ok())
20270                .unwrap_or(d)
20271        };
20272        (
20273            g("CMF_FFN_REFIT_FROM", 0),
20274            g("CMF_FFN_REFIT_TO", usize::MAX),
20275        )
20276    });
20277    if li < from || li > to {
20278        return;
20279    }
20280    let mut guard = map.lock().unwrap();
20281    let (map, shared) = &mut *guard;
20282    let acc = match map.entry(li) {
20283        std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
20284        std::collections::hash_map::Entry::Vacant(e) => {
20285            let path = format!("{dir}/support.{li}.u32");
20286            let Ok(bytes) = std::fs::read(&path) else {
20287                eprintln!("refit: no {path} — layer {li} skipped");
20288                return;
20289            };
20290            let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
20291            let support: Vec<u32> = bytes[4..4 + n * 4]
20292                .chunks_exact(4)
20293                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
20294                .collect();
20295            eprintln!(
20296                "refit: layer {li} support {n} ({:.0} MB of accumulator)",
20297                (n * n + hidden * n) as f64 * 4.0 / 1e6
20298            );
20299            e.insert(RefitAcc {
20300                gss: vec![0.0; n * n],
20301                ya: vec![0.0; hidden * n],
20302                buf_g: Vec::new(),
20303                buf_o: Vec::new(),
20304                buf_t: 0,
20305                support,
20306                hidden,
20307                tokens: 0,
20308            })
20309        }
20310    };
20311    let ns = acc.support.len();
20312    // Stage this chunk transposed; the GEMM fires once the batch is full.
20313    let cap = refit_batch();
20314    if acc.buf_g.is_empty() {
20315        acc.buf_g = vec![0.0; ns * cap];
20316        acc.buf_o = vec![0.0; hidden * cap];
20317    }
20318    let take = b.min(cap - acc.buf_t);
20319    for t in 0..take {
20320        let col = acc.buf_t + t;
20321        for (j, &n) in acc.support.iter().enumerate() {
20322            acc.buf_g[j * cap + col] = g[t * inter + n as usize];
20323        }
20324        for h in 0..hidden {
20325            acc.buf_o[h * cap + col] = out[t * hidden + h];
20326        }
20327    }
20328    acc.buf_t += take;
20329    acc.tokens += take as u64;
20330    if acc.buf_t < cap {
20331        return;
20332    }
20333    let bt = acc.buf_t;
20334    acc.buf_t = 0;
20335    // The GEMM WRITES its C (it zeroes the accumulators it uses), so the
20336    // chunk product lands in scratch and is added on — the one thing that
20337    // silently turns a Gram over 13 000 tokens into a Gram over 256.
20338    // Both products are `C[n, m] += X[n, b] · Yᵀ[b, m]` with X and Y
20339    // stored row-major [·, b] — exactly `gemm_nt_f32`'s shape, so the
20340    // card does them when it is up (this is the whole calibration's
20341    // cost: O(|S|²) per token, 2.9 PFLOP for a 27B pass). The tiled CPU
20342    // loop stays as the fallback. Neither accumulates, so the product
20343    // lands in scratch and is added on.
20344    let RefitAcc {
20345        gss,
20346        ya,
20347        buf_g,
20348        buf_o,
20349        ..
20350    } = acc;
20351    let need = (ns * ns).max(hidden * ns);
20352    if shared.len() < need {
20353        shared.resize(need, 0.0);
20354    }
20355    let scratch = &mut shared[..];
20356    let _ = bt;
20357    if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
20358        add_into(gss, &scratch[..ns * ns], pool);
20359        if crate::gpu::gemm_nt_f32_transient(
20360            buf_o,
20361            buf_g,
20362            &mut scratch[..hidden * ns],
20363            hidden,
20364            cap,
20365            ns,
20366        ) {
20367            add_into(ya, &scratch[..hidden * ns], pool);
20368        } else {
20369            accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20370        }
20371    } else {
20372        accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
20373        accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20374    }
20375    // No zeroing: the batch is always filled exactly (cap is a multiple
20376    // of the prefill chunk), and a memset of 178 MB a layer would cost
20377    // more than the GEMM.
20378}
20379
20380/// `CMF_FFN_REFIT_BATCH` — tokens staged before each GEMM (default 4096).
20381fn refit_batch() -> usize {
20382    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20383    *B.get_or_init(|| {
20384        std::env::var("CMF_FFN_REFIT_BATCH")
20385            .ok()
20386            .and_then(|v| v.parse().ok())
20387            .unwrap_or(4096)
20388    })
20389}
20390
20391/// `c[m, n] += Σ_t left[m, t]·right[n, t]` — both operands transposed,
20392/// the CPU fallback for the staged batch.
20393fn accum_outer_t(
20394    c: &mut [f32],
20395    m: usize,
20396    n: usize,
20397    b: usize,
20398    left: &[f32],
20399    right: &[f32],
20400    pool: Option<&Pool>,
20401) {
20402    let ptr = SendMut(c.as_mut_ptr());
20403    let body = |i: usize| {
20404        let ptr = &ptr;
20405        let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
20406        for t in 0..b {
20407            let a = left[i * b + t];
20408            if a == 0.0 {
20409                continue;
20410            }
20411            for (j, o) in row.iter_mut().enumerate() {
20412                *o += a * right[j * b + t];
20413            }
20414        }
20415    };
20416    match pool {
20417        Some(p) if m > 1 => p.run_rows(m, &|s, e| {
20418            for i in s..e {
20419                body(i);
20420            }
20421        }),
20422        _ => {
20423            for i in 0..m {
20424                body(i);
20425            }
20426        }
20427    }
20428}
20429
20430/// `dst += src`, spread over the pool — at 118 M floats a layer this is
20431/// not a loop to leave on one core.
20432fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
20433    let n = dst.len().min(src.len());
20434    match pool {
20435        Some(p) if n >= 1 << 16 => {
20436            let ptr = SendMut(dst.as_mut_ptr());
20437            let f = |s: usize, e: usize| {
20438                let ptr = &ptr;
20439                for blk in s..e {
20440                    let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
20441                    for i in a..b {
20442                        unsafe { *ptr.0.add(i) += src[i] };
20443                    }
20444                }
20445            };
20446            p.run_rows(n.div_ceil(4096), &f);
20447        }
20448        _ => {
20449            for (d, v) in dst.iter_mut().zip(&src[..n]) {
20450                *d += *v;
20451            }
20452        }
20453    }
20454}
20455
20456/// `c[m, n] += Σ_t left[t, m]·right[t, n]`, with `left` stored [m, t] and
20457/// `right` [t, n]. Tiled over the rows of `c` so a tile stays in cache
20458/// while each token's `right` row streams past it once, and parallel
20459/// over tiles.
20460fn accum_outer(
20461    c: &mut [f32],
20462    m: usize,
20463    n: usize,
20464    b: usize,
20465    left: &[f32],
20466    right: &[f32],
20467    pool: Option<&Pool>,
20468) {
20469    const TILE: usize = 32;
20470    let tiles = m.div_ceil(TILE);
20471    let cp = SendMut(c.as_mut_ptr());
20472    let body = |ti: usize| {
20473        let cp = &cp;
20474        let i0 = ti * TILE;
20475        let i1 = (i0 + TILE).min(m);
20476        for t in 0..b {
20477            let r = &right[t * n..t * n + n];
20478            for i in i0..i1 {
20479                let a = left[i * b + t];
20480                if a == 0.0 {
20481                    continue;
20482                }
20483                // SAFETY: tiles partition c's rows; workers never overlap.
20484                let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
20485                for (o, v) in row.iter_mut().zip(r) {
20486                    *o += a * *v;
20487                }
20488            }
20489        }
20490    };
20491    match pool {
20492        Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
20493            for ti in s..e {
20494                body(ti);
20495            }
20496        }),
20497        _ => {
20498            for ti in 0..tiles {
20499                body(ti);
20500            }
20501        }
20502    }
20503}
20504
20505/// Write what the calibration accumulated: `gss.<L>.f32` and `ya.<L>.f32`.
20506pub fn refit_flush() -> usize {
20507    let Some((dir, map)) = refit_dir() else {
20508        return 0;
20509    };
20510    let guard = map.lock().unwrap();
20511    let mut n = 0;
20512    for (li, acc) in guard.0.iter() {
20513        // A silently truncated write here is a Gram that reshapes to
20514        // nothing an hour later — say it out loud instead.
20515        let w = |name: &str, v: &[f32]| {
20516            let path = format!("{dir}/{name}.{li}.f32");
20517            let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
20518            match std::fs::write(&path, &bytes) {
20519                Ok(()) => {}
20520                Err(e) => eprintln!(
20521                    "refit: FAILED to write {path} ({} MB): {e}",
20522                    bytes.len() / 1_000_000
20523                ),
20524            }
20525        };
20526        w("gss", &acc.gss);
20527        w("ya", &acc.ya);
20528        println!(
20529            "refit L{li}: {} support, {} tokens, hidden {}",
20530            acc.support.len(),
20531            acc.tokens,
20532            acc.hidden
20533        );
20534        n += 1;
20535    }
20536    n
20537}
20538
20539/// `CMF_FFN_ADUMP=<prefix>` — append every probed token's FFN activation
20540/// row to `<prefix>.<layer>.f16`. The co-activation record: which
20541/// neurons fire together, which is what a tube has to group if a token
20542/// is ever going to open one tube instead of sixteen.
20543fn adump_row(li: usize, g: &[f32]) {
20544    use std::io::Write as _;
20545    static FILES: std::sync::OnceLock<
20546        Option<(
20547            String,
20548            std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
20549        )>,
20550    > = std::sync::OnceLock::new();
20551    let Some((prefix, map)) = FILES
20552        .get_or_init(|| {
20553            std::env::var("CMF_FFN_ADUMP")
20554                .ok()
20555                .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
20556        })
20557        .as_ref()
20558    else {
20559        return;
20560    };
20561    // `CMF_FFN_ADUMP_FROM/_TO` narrow the dump to a layer span, so a big
20562    // calibration run fits on disk in a few passes instead of one.
20563    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20564    let (from, to) = *SPAN.get_or_init(|| {
20565        let g = |k: &str, d: usize| {
20566            std::env::var(k)
20567                .ok()
20568                .and_then(|v| v.parse().ok())
20569                .unwrap_or(d)
20570        };
20571        (
20572            g("CMF_FFN_ADUMP_FROM", 0),
20573            g("CMF_FFN_ADUMP_TO", usize::MAX),
20574        )
20575    });
20576    if li < from || li > to {
20577        return;
20578    }
20579    let mut map = map.lock().unwrap();
20580    let f = map.entry(li).or_insert_with(|| {
20581        std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
20582    });
20583    let mut bytes = Vec::with_capacity(g.len() * 2);
20584    for v in g {
20585        bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
20586    }
20587    let _ = f.write_all(&bytes);
20588}
20589
20590/// `CMF_FFN_ORACLE_TOPK` — keep only the k largest |silu(g)·u| of each
20591/// token and zero the rest. Not a serving mode: it is the CEILING of
20592/// contextual sparsity — what a per-token router would be chasing —
20593/// measured by cheating, since the selection reads the very activations
20594/// it would have to predict.
20595fn oracle_topk() -> usize {
20596    if exact_mtp_calibration_active() {
20597        return 0;
20598    }
20599    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20600    *K.get_or_init(|| {
20601        std::env::var("CMF_FFN_ORACLE_TOPK")
20602            .ok()
20603            .and_then(|v| v.parse().ok())
20604            .unwrap_or(0)
20605    })
20606}
20607
20608/// `CMF_FFN_GATE_TOPK` — the REALIZABLE cousin of the oracle: rank the
20609/// neurons by their gate alone (which the kernel has computed anyway
20610/// before it reads `up`), keep the k best, and drop the rest. Every
20611/// dropped neuron's `up` row and `down` column stay unread, so this is
20612/// the sparsity a serving path can actually take without a router.
20613fn gate_topk() -> usize {
20614    if exact_mtp_calibration_active() {
20615        return 0;
20616    }
20617    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20618    *K.get_or_init(|| {
20619        std::env::var("CMF_FFN_GATE_TOPK")
20620            .ok()
20621            .and_then(|v| v.parse().ok())
20622            .unwrap_or(0)
20623    })
20624}
20625
20626/// `CMF_FFN_GATE_BLOCK` — select in blocks of B neurons instead of one
20627/// by one. A scattered per-neuron choice cannot be read efficiently (a
20628/// row at a time, no prefetch runway); a block of 32 is a contiguous
20629/// 32-row slab of `up` and of the transposed `down`, which the ordinary
20630/// kernels stream. The question the measurement answers is what the
20631/// block costs in quality.
20632fn gate_block() -> usize {
20633    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20634    *B.get_or_init(|| {
20635        std::env::var("CMF_FFN_GATE_BLOCK")
20636            .ok()
20637            .and_then(|v| v.parse().ok())
20638            .unwrap_or(1)
20639    })
20640}
20641
20642/// Zero all but the `k` largest BLOCKS (by summed square) of a row.
20643fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
20644    let n = g.len();
20645    let nb = n.div_ceil(block);
20646    let kb = (keep_n.div_ceil(block)).clamp(1, nb);
20647    if kb >= nb {
20648        return;
20649    }
20650    let mut score: Vec<f32> = (0..nb)
20651        .map(|b| {
20652            g[b * block..((b + 1) * block).min(n)]
20653                .iter()
20654                .map(|v| v * v)
20655                .sum::<f32>()
20656        })
20657        .collect();
20658    let mut ord = score.clone();
20659    let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
20660        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20661    });
20662    let thr = *kth;
20663    for b in 0..nb {
20664        if score[b] < thr {
20665            g[b * block..((b + 1) * block).min(n)].fill(0.0);
20666        }
20667    }
20668    score.clear();
20669}
20670
20671/// Zero all but the `k` largest magnitudes of one token's activation row.
20672fn keep_top_k(g: &mut [f32], k: usize) {
20673    if gate_block() > 1 {
20674        return keep_top_blocks(g, k, gate_block());
20675    }
20676    let n = g.len();
20677    if k == 0 || k >= n {
20678        return;
20679    }
20680    let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
20681    let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
20682        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20683    });
20684    let thr = *kth;
20685    for v in g.iter_mut() {
20686        if v.abs() < thr {
20687            *v = 0.0;
20688        }
20689    }
20690}
20691
20692/// `CMF_FFN_PROBE_SQ` — accumulate Σa², so the dump divided by the token
20693/// count and square-rooted is the RMS activation trace Patent 12 weights
20694/// its matrices by.
20695fn probe_sq() -> bool {
20696    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20697    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
20698}
20699
20700/// `CMF_FFN_PROBE_SIGNED` — accumulate the SIGNED activation sum
20701/// instead of its magnitude: what a dropped neuron contributes ON
20702/// AVERAGE, which is the bias a narrowed FFN can add back for free.
20703fn probe_signed() -> bool {
20704    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20705    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
20706}
20707
20708/// `CMF_FFN_MEANFILL=<file>` — a masked-out neuron contributes its MEAN
20709/// activation instead of zero (`u32 layers, u32 inter, f32[…]`, the mass
20710/// dump layout, holding per-neuron means). Dropping a neuron outright
20711/// also drops its average contribution, which shifts the layer output by
20712/// a constant; filling the mean back is one add per layer and costs no
20713/// bytes off the bus. This is the measurement arm — in a tube file the
20714/// same correction ships as a per-task bias vector.
20715fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
20716    static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
20717    M.get_or_init(|| {
20718        let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
20719        let b = std::fs::read(&p).ok()?;
20720        let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
20721        let vals: Vec<f32> = b[8..]
20722            .chunks_exact(4)
20723            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
20724            .collect();
20725        eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
20726        Some((inter, vals))
20727    })
20728    .as_ref()
20729}
20730
20731/// `CMF_FFN_PROBE_TOPK` — 0 (default) = accumulate mass, k>0 = count
20732/// how often a neuron lands in a token's top k.
20733fn probe_topk() -> usize {
20734    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20735    *K.get_or_init(|| {
20736        std::env::var("CMF_FFN_PROBE_TOPK")
20737            .ok()
20738            .and_then(|v| v.parse().ok())
20739            .unwrap_or(0)
20740    })
20741}
20742
20743thread_local! {
20744    /// DTG-MA activation probe: per-layer per-neuron Σ|silu(g)·u|
20745    /// accumulator, alive only during `Pipeline::probe_ffn_mass`.
20746    static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
20747        const { std::cell::RefCell::new(None) };
20748}
20749
20750/// Per-token structured sparsity, paid for in bytes.
20751///
20752/// The gate is the cheapest third of an FFN and it already says which
20753/// neurons matter: `silu(gate)` near zero means the neuron contributes
20754/// nothing whatever `up` says. So compute every gate, keep the `k`
20755/// loudest, and read ONLY those neurons' `up` rows and `down` rows —
20756/// the latter needs `down_proj` stored transposed, otherwise a neuron's
20757/// down weights are a strided column and "reading only those" costs a
20758/// full cache line each.
20759///
20760/// Returns `None` when the file has no transposed `down` (the caller
20761/// then runs the ordinary dense path).
20762fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
20763    // The scatter path reads individual rows/columns and cannot express the
20764    // per-matrix signed FWHT boundary.  Let the descriptor-aware dense path
20765    // handle Prism files rather than silently running an unrotated sparse
20766    // approximation.
20767    if d.gate_proj.has_prism_contract()
20768        || d.up_proj.has_prism_contract()
20769        || d.down_proj.has_prism_contract()
20770    {
20771        return None;
20772    }
20773    let dt = d.down_t.as_ref()?;
20774    let inter = d.gate_proj.rows();
20775    let hidden = dt.cols();
20776    if k == 0 || k >= inter || d.act != Act::Silu {
20777        return None;
20778    }
20779    DYN_SCRATCH.with(|sc| {
20780        let mut sc = sc.borrow_mut();
20781        let DynScratch {
20782            g,
20783            mag,
20784            live,
20785            parts,
20786        } = &mut *sc;
20787        g.resize(inter, 0.0);
20788        d.gate_proj.matvec(x, g, pool);
20789        for v in g.iter_mut() {
20790            *v = inference::silu(*v);
20791        }
20792        // The k-th largest |silu(gate)| is the threshold; ties keep more,
20793        // which is the safe side.
20794        mag.clear();
20795        mag.extend(g.iter().map(|v| v.abs()));
20796        let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
20797            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20798        });
20799        let thr = *kth;
20800        live.clear();
20801        live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
20802        let mut out = vec![0.0f32; hidden];
20803        match pool {
20804            Some(p) if live.len() >= 64 => {
20805                let nw = p.n_workers() + 1;
20806                parts.clear();
20807                parts.resize(nw * hidden, 0.0);
20808                let ptr = SendMut(parts.as_mut_ptr());
20809                let n = live.len();
20810                let live_ref: &[u32] = live;
20811                let g_ref: &[f32] = g;
20812                p.run(&|w, workers| {
20813                    let chunk = n.div_ceil(workers);
20814                    let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
20815                    if s >= e {
20816                        return;
20817                    }
20818                    WORKER_SCRATCH.with(|ws| {
20819                        let mut ws = ws.borrow_mut();
20820                        let [scratch, acc] = &mut *ws;
20821                        scratch.resize(hidden.max(x.len()), 0.0);
20822                        acc.clear();
20823                        acc.resize(hidden, 0.0);
20824                        for (o, &nrm) in live_ref[s..e].iter().enumerate() {
20825                            // One neuron of runway: the next row's lines
20826                            // start moving while this one is multiplied.
20827                            if let Some(&nx) = live_ref[s..e].get(o + 1) {
20828                                d.up_proj.prefetch_row(nx as usize);
20829                                dt.prefetch_row(nx as usize);
20830                            }
20831                            let idx = nrm as usize;
20832                            let up = d.up_proj.row_dot(idx, x, scratch);
20833                            let a = g_ref[idx] * up;
20834                            if a != 0.0 {
20835                                dt.add_row_scaled(idx, a, acc, scratch);
20836                            }
20837                        }
20838                        for (j, v) in acc.iter().enumerate() {
20839                            unsafe { *ptr.at(w * hidden + j) = *v };
20840                        }
20841                    });
20842                });
20843                for w in 0..nw {
20844                    for (j, o) in out.iter_mut().enumerate() {
20845                        *o += parts[w * hidden + j];
20846                    }
20847                }
20848            }
20849            _ => {
20850                WORKER_SCRATCH.with(|ws| {
20851                    let mut ws = ws.borrow_mut();
20852                    let [scratch, _acc] = &mut *ws;
20853                    scratch.resize(hidden.max(x.len()), 0.0);
20854                    for &nrm in live.iter() {
20855                        let idx = nrm as usize;
20856                        let up = d.up_proj.row_dot(idx, x, scratch);
20857                        let a = g[idx] * up;
20858                        if a != 0.0 {
20859                            dt.add_row_scaled(idx, a, &mut out, scratch);
20860                        }
20861                    }
20862                });
20863            }
20864        }
20865        Some(out)
20866    })
20867}
20868
20869/// Caller-side scratch of the dynamic path — one allocation per thread,
20870/// not one per layer per token (that alone cost a third of the decode).
20871struct DynScratch {
20872    g: Vec<f32>,
20873    mag: Vec<f32>,
20874    live: Vec<u32>,
20875    parts: Vec<f32>,
20876}
20877
20878thread_local! {
20879    static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
20880        std::cell::RefCell::new(DynScratch {
20881            g: Vec::new(),
20882            mag: Vec::new(),
20883            live: Vec::new(),
20884            parts: Vec::new(),
20885        })
20886    };
20887    /// Pool-worker scratch: the row buffer and this worker's partial sum.
20888    static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
20889        const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
20890}
20891
20892/// `dense_ffn_cpu` with a per-visit mask landing on the activations —
20893/// the masked-inference fast path's decode arm. Full fused quant
20894/// compute, closed neurons zeroed before down: arithmetically the
20895/// pruned network, no dequant, no weight bytes touched.
20896fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
20897    let inter = d.gate_proj.rows();
20898    FFN_SCRATCH.with(|s| {
20899        let mut s = s.borrow_mut();
20900        let [g, u, ..] = &mut *s;
20901        g.resize(inter, 0.0);
20902        if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
20903            // g holds silu(gate)·up.
20904        } else {
20905            u.resize(inter, 0.0);
20906            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20907            for i in 0..inter {
20908                g[i] = d.act.combine(g[i], u[i]);
20909            }
20910        }
20911        zero_masked_cols(g, 1, inter, mask_row);
20912        let mut out = attention::take_buf(d.down_proj.rows());
20913        d.down_proj.matvec(g, &mut out, pool);
20914        out
20915    })
20916}
20917
20918/// Dense FFN as one GPU submission via the MoE block path (single
20919/// expert, weight 1.0): gate → silu·up → down chained in one command
20920/// buffer, intermediate activations device-resident. None → weights
20921/// not q8-mapped in the primary shard / over the VRAM budget / backend
20922/// refusal → honest CPU path.
20923fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
20924    if d.gate_proj.has_prism_contract()
20925        || d.up_proj.has_prism_contract()
20926        || d.down_proj.has_prism_contract()
20927    {
20928        return None;
20929    }
20930    // The GPU block applies the standard graph activations plus the exact
20931    // MiniMax-M3 OAI contract; any other activation stays on the CPU.
20932    d.act.moe_job_graph_contract()?;
20933    // Threshold: tiny FFNs are not worth a submission (q1 excepted —
20934    // see the caller's gate).
20935    if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
20936        return None;
20937    }
20938    let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
20939    let mut model_ref = None;
20940    moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
20941    let model = model_ref?;
20942    let hidden = jobs[0].down.1;
20943    let mut out = attention::take_buf(hidden);
20944    if crate::gpu::moe_block(&model, &jobs, &mut out) {
20945        Some(out)
20946    } else {
20947        let mut out = out;
20948        attention::recycle_buf(&mut out);
20949        None
20950    }
20951}
20952
20953/// q8-mapped primary-shard tensor parts for a GPU job: q8_2f carries
20954/// its column field, q8_row runs with empty col slices (the backend
20955/// skips the multiply). Shared by the MoE block and the dense-FFN
20956/// single-job path.
20957#[allow(clippy::type_complexity)]
20958#[allow(clippy::type_complexity)]
20959pub(crate) fn moe_parts(
20960    t: &QTensor,
20961) -> Option<(
20962    &std::sync::Arc<cortiq_core::CmfModel>,
20963    usize,
20964    usize,
20965    usize,
20966    &[f32],
20967    &[f32],
20968    bool,
20969    bool,
20970    bool,
20971)> {
20972    match t {
20973        QTensor::Mapped {
20974            model,
20975            idx,
20976            dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
20977            rows,
20978            cols,
20979            row_scale,
20980            col_field,
20981            ..
20982        } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
20983            model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
20984        )),
20985        // q1: tile-embedded scales — empty rs/col slices, raw xs.
20986        QTensor::Mapped {
20987            model,
20988            idx,
20989            dtype: cortiq_core::TensorDtype::Q1,
20990            rows,
20991            cols,
20992            ..
20993        } => Some((
20994            model,
20995            *idx,
20996            *rows,
20997            *cols,
20998            &[][..],
20999            &[][..],
21000            true,
21001            false,
21002            false,
21003        )),
21004        // q4_tiled: 18-byte tiles with embedded f16 scales — raw xs.
21005        QTensor::Mapped {
21006            model,
21007            idx,
21008            dtype: cortiq_core::TensorDtype::Q4Tiled,
21009            rows,
21010            cols,
21011            ..
21012        } => Some((
21013            model,
21014            *idx,
21015            *rows,
21016            *cols,
21017            &[][..],
21018            &[][..],
21019            false,
21020            true,
21021            false,
21022        )),
21023        // q4tp: same raw-xs contract, different stride and scale plane.
21024        QTensor::Mapped {
21025            model,
21026            idx,
21027            dtype: cortiq_core::TensorDtype::Q4TiledP,
21028            rows,
21029            cols,
21030            ..
21031        } => Some((
21032            model,
21033            *idx,
21034            *rows,
21035            *cols,
21036            &[][..],
21037            &[][..],
21038            false,
21039            true,
21040            false,
21041        )),
21042        // q2tp: the 2-bit expert plane of the mixed profile — q4 family
21043        // for stride bookkeeping, flagged q2 so the trio validation can
21044        // demand a q4tp down.
21045        QTensor::Mapped {
21046            model,
21047            idx,
21048            dtype: cortiq_core::TensorDtype::Q2TiledP,
21049            rows,
21050            cols,
21051            ..
21052        } => Some((
21053            model,
21054            *idx,
21055            *rows,
21056            *cols,
21057            &[][..],
21058            &[][..],
21059            false,
21060            true,
21061            true,
21062        )),
21063        _ => None,
21064    }
21065}
21066
21067/// The routed experts of a MoE layer as the Metal chunk prefill takes
21068/// them: f32 router rows and every expert's q4tp (gate, up, down) tensor
21069/// indices, uniform SiLU shapes. Only the plain routed form — no shared
21070/// expert, no per-expert scale, no resonance or τ router, no router input
21071/// norm; the batched host walk keeps the rest. The routing itself stays the
21072/// host's (`moe_route`), so mask and selection bias need nothing here.
21073#[cfg(target_os = "macos")]
21074#[allow(clippy::type_complexity)]
21075fn chunk_moe_parts(
21076    m: &MoeFfn,
21077    hidden: usize,
21078) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
21079    if m.shared.is_some()
21080        || m.per_expert_scale.is_some()
21081        || m.resonance.is_some()
21082        || m.route_tau.is_some()
21083        || m.router_input_norm
21084        || m.experts.is_empty()
21085        || m.top_k == 0
21086        || std::env::var("CMF_RMS_TRACE").is_ok()
21087        || std::env::var("CMF_ACT_DUMP").is_ok()
21088        || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
21089    {
21090        return None;
21091    }
21092    let (rf, rr, rc) = m.router.f32_parts()?;
21093    if rr != m.experts.len() || rc != hidden {
21094        return None;
21095    }
21096    let inter = m.experts[0].gate_proj.rows();
21097    // All-q8_2f experts (Mellum2.1 q8_2f) ride the `moe_q8` GEMMs;
21098    // `CMF_METAL_MOE_Q8=0` keeps them on the host walk.
21099    let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
21100    let experts = m
21101        .experts
21102        .iter()
21103        .map(|e| {
21104            if e.act != Act::Silu
21105                || !e.segs.is_empty()
21106                || e.gate_proj.rows() != inter
21107                || e.up_proj.rows() != inter
21108                || e.down_proj.cols() != inter
21109                || e.gate_proj.cols() != hidden
21110                || e.down_proj.rows() != hidden
21111            {
21112                return None;
21113            }
21114            if q8 {
21115                let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21116                return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21117            }
21118            Some((
21119                e.gate_proj.mapped_q4tp()?.1,
21120                e.up_proj.mapped_q4tp()?.1,
21121                e.down_proj.mapped_q4tp()?.1,
21122            ))
21123        })
21124        .collect::<Option<Vec<_>>>()?;
21125    Some((rf, experts, q8))
21126}
21127
21128/// Map a MoE onto the Metal token graph's contract: f32 router, a
21129/// shared expert (gated — Qwen — or ungated at weight 1 — DeepSeek-V3 /
21130/// HunYuan hy_v3), softmax or sigmoid scores with an optional selection
21131/// bias and routed scale, experts uniformly q4tp (or the mixed profile:
21132/// q2tp gate/up over a q4tp down). τ routers, masks, per-expert scales
21133/// and Gemma's router-input norm refuse here — those semantics stay on
21134/// the CPU path.
21135/// Map Flint's released ragged MoE into the native page graph.  This stays
21136/// separate from `metal_moe_graph_parts`: its six routed slots are Q2TP and
21137/// 512-wide, while its ungated shared SwiGLU is Q4TP and 1024-wide.  Feeding
21138/// the shared tuple into the uniform jobs descriptor would make the shader
21139/// use the routed stride on a different payload.
21140#[cfg(target_os = "macos")]
21141fn metal_flint_moe_graph_parts(
21142    m: &MoeFfn,
21143    hidden: usize,
21144    scratch_key: usize,
21145) -> Option<crate::gpu::GpuFlintMoe<'_>> {
21146    use std::sync::Arc;
21147
21148    const HIDDEN: usize = 2_048;
21149    const ROUTED_INTER: usize = 512;
21150    const SHARED_INTER: usize = 1_024;
21151    const EXPERTS: usize = 256;
21152    const TOP_K: usize = 6;
21153
21154    if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
21155        return None;
21156    }
21157    let routed0 = m.experts.first()?;
21158    let (model, _) = routed0.gate_proj.mapped_q2tp()?;
21159    let (router, router_rows, router_cols) = m.router.f32_parts()?;
21160    if router_rows != EXPERTS || router_cols != HIDDEN {
21161        return None;
21162    }
21163    if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
21164        return None;
21165    }
21166    let same_q2 = |t: &QTensor| -> Option<usize> {
21167        let (owner, idx) = t.mapped_q2tp()?;
21168        Arc::ptr_eq(&owner, &model).then_some(idx)
21169    };
21170    let same_q4 = |t: &QTensor| -> Option<usize> {
21171        let (owner, idx) = t.mapped_q4tp()?;
21172        Arc::ptr_eq(&owner, &model).then_some(idx)
21173    };
21174    let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21175        if e.act != Act::Silu
21176            || !e.segs.is_empty()
21177            || e.gate_proj.rows() != ROUTED_INTER
21178            || e.gate_proj.cols() != HIDDEN
21179            || e.up_proj.rows() != ROUTED_INTER
21180            || e.up_proj.cols() != HIDDEN
21181            || e.down_proj.rows() != HIDDEN
21182            || e.down_proj.cols() != ROUTED_INTER
21183        {
21184            return None;
21185        }
21186        Some((
21187            same_q2(&e.gate_proj)?,
21188            same_q2(&e.up_proj)?,
21189            same_q4(&e.down_proj)?,
21190        ))
21191    };
21192    let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21193        if e.act != Act::Silu
21194            || !e.segs.is_empty()
21195            || e.gate_proj.rows() != SHARED_INTER
21196            || e.gate_proj.cols() != HIDDEN
21197            || e.up_proj.rows() != SHARED_INTER
21198            || e.up_proj.cols() != HIDDEN
21199            || e.down_proj.rows() != HIDDEN
21200            || e.down_proj.cols() != SHARED_INTER
21201        {
21202            return None;
21203        }
21204        Some((
21205            same_q4(&e.gate_proj)?,
21206            same_q4(&e.up_proj)?,
21207            same_q4(&e.down_proj)?,
21208        ))
21209    };
21210    let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
21211    if experts.len() != EXPERTS {
21212        return None;
21213    }
21214    let (shared_ffn, shared_gate) = m.shared.as_ref()?;
21215    if shared_gate.is_some() {
21216        return None;
21217    }
21218    Some(crate::gpu::GpuFlintMoe {
21219        router,
21220        bias: m.expert_bias.as_deref(),
21221        experts,
21222        scratch_key,
21223        shared: shared(shared_ffn)?,
21224        top_k: m.top_k,
21225        norm_topk: m.norm_topk_prob,
21226        route_scale: m.routed_scaling,
21227        sigmoid: m.router_sigmoid,
21228        sigmoid_denom_eps: m.sigmoid_denom_eps,
21229    })
21230}
21231
21232#[cfg(target_os = "macos")]
21233fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
21234    if m.router_input_norm
21235        || m.route_tau.is_some()
21236        || m.mask.is_some()
21237        || m.per_expert_scale.is_some()
21238        || m.experts.is_empty()
21239        || m.top_k == 0
21240        || m.resonance.is_some()
21241    {
21242        return None;
21243    }
21244    // Without a shared expert the select kernel fills the k routed jobs
21245    // only (flag 32). Measured on Mellum2.1's softmax router; the sigmoid /
21246    // biased routers without one (LFM2-MoE) keep the CPU path until they
21247    // are measured too.
21248    let (sh, sg) = match &m.shared {
21249        Some((sh, sg)) => (Some(sh), sg.as_ref()),
21250        None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
21251        None => return None,
21252    };
21253    let (rf, rr, rc) = m.router.f32_parts()?;
21254    if rr != m.experts.len() || rc != hidden {
21255        return None;
21256    }
21257    let shared_gated = sg.is_some();
21258    let sf = match sg {
21259        Some(sg) => {
21260            let (sf, sr, sc) = sg.f32_parts()?;
21261            if sr * sc != hidden {
21262                return None;
21263            }
21264            sf
21265        }
21266        // Ungated: the router's first row stands in for the gate matvec
21267        // (its logit is never read — the kernel pins weight 1).
21268        None => &rf[..hidden],
21269    };
21270    if let Some(b) = &m.expert_bias {
21271        if b.len() != m.experts.len() {
21272            return None;
21273        }
21274    }
21275    let inter = m.experts[0].gate_proj.rows();
21276    // The first expert's gate decides the profile; every trio (shared
21277    // included) must agree — the jobs ladder flips ONE kernel for all.
21278    let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
21279    // All-q8_2f routed experts (Mellum2.1 q8_2f: no shared expert, the
21280    // contract they were measured on) take the `moe_q8` kernels;
21281    // `CMF_METAL_MOE_Q8=0` keeps them off the graph (per-op path).
21282    let q8 = crate::gpu_metal::moe_q8::enabled()
21283        && m.shared.is_none()
21284        && m.experts[0].gate_proj.q8_2f_parts().is_some();
21285    let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21286        if e.act != Act::Silu
21287            || e.gate_proj.rows() != inter
21288            || e.gate_proj.cols() != hidden
21289            || e.up_proj.rows() != inter
21290            || e.up_proj.cols() != hidden
21291            || e.down_proj.rows() != hidden
21292            || e.down_proj.cols() != inter
21293        {
21294            return None;
21295        }
21296        if q8 {
21297            let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21298            return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21299        }
21300        let pick = |t: &QTensor| -> Option<usize> {
21301            if gu_q2 {
21302                t.mapped_q2tp().map(|(_, i)| i)
21303            } else {
21304                t.mapped_q4tp().map(|(_, i)| i)
21305            }
21306        };
21307        Some((
21308            pick(&e.gate_proj)?,
21309            pick(&e.up_proj)?,
21310            e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
21311        ))
21312    };
21313    let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
21314    let shared = match sh {
21315        Some(sh) => Some(trio(sh)?),
21316        None => None,
21317    };
21318    Some(crate::gpu::GpuMoe {
21319        router: rf,
21320        sgate: sf,
21321        experts,
21322        shared,
21323        n_exp: m.experts.len(),
21324        top_k: m.top_k,
21325        inter,
21326        norm_topk: m.norm_topk_prob,
21327        route_scale: m.routed_scaling,
21328        gu_q2,
21329        q8,
21330        sigmoid: m.router_sigmoid,
21331        sigmoid_denom_eps: m.sigmoid_denom_eps,
21332        bias: m.expert_bias.as_deref(),
21333        shared_gated,
21334    })
21335}
21336
21337/// `CMF_FORCE_IDS=<file>` (diagnostics): decode step n commits the n-th
21338/// whitespace-separated token id of the file instead of sampling, while the
21339/// logits are still computed (and dumped by `CMF_LOGIT_DUMP_ALL`) — one
21340/// backend teacher-forced along another's greedy sequence, so top-1
21341/// agreement can be counted past a near-tie divergence.
21342fn forced_ids_at(step: usize) -> Option<u32> {
21343    static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
21344    IDS.get_or_init(|| {
21345        std::env::var("CMF_FORCE_IDS")
21346            .ok()
21347            .and_then(|p| std::fs::read_to_string(p).ok())
21348            .map(|s| {
21349                s.split_whitespace()
21350                    .filter_map(|t| t.parse().ok())
21351                    .collect()
21352            })
21353            .unwrap_or_default()
21354    })
21355    .get(step)
21356    .copied()
21357}
21358
21359/// Build one gate/up/down GPU job from three tensors. `moe_push_job` is the
21360/// DenseFfn-shaped caller; architectures that keep their experts in their own
21361/// structs (DeepSeek-V4) come here directly.
21362pub(crate) fn moe_push_job_parts<'a>(
21363    gate: &'a QTensor,
21364    up: &'a QTensor,
21365    down: &'a QTensor,
21366    x: &[f32],
21367    w: f32,
21368    swiglu_limit: f32,
21369    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21370    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21371) -> Option<()> {
21372    use crate::qtensor::prescale;
21373    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
21374    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
21375    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
21376    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21377        return None; // mixed-dtype trio — honest CPU path
21378    }
21379    // The 2-bit profile is gate/up q2tp over a PLAIN q4tp down; any other
21380    // 2-bit arrangement stays on the CPU.
21381    if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
21382        return None;
21383    }
21384    if !gq2 && dq2 {
21385        return None;
21386    }
21387    model_ref.get_or_insert_with(|| gm.clone());
21388    let dt = |cf: &[f32]| {
21389        if cf.is_empty() {
21390            cortiq_core::TensorDtype::Q8Row
21391        } else {
21392            cortiq_core::TensorDtype::Q8_2f
21393        }
21394    };
21395    jobs.push(crate::gpu::MoeJob {
21396        gate: (gi, gr, gc, grs),
21397        up: (ui, ur, uc, urs),
21398        down: (di, dr, dc, drs),
21399        xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
21400        xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
21401        down_col: dcf,
21402        w,
21403        q1: gq1,
21404        q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
21405        q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
21406        gu_q2: gq2,
21407        swiglu_limit,
21408        act: 0,
21409    });
21410    Some(())
21411}
21412
21413/// Build one gate/up/down GPU job (see `moe_parts`).
21414fn moe_push_job<'a>(
21415    d: &'a DenseFfn,
21416    x: &[f32],
21417    w: f32,
21418    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21419    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21420) -> Option<()> {
21421    use crate::qtensor::prescale;
21422    // The block's act(gate)·up pass takes an exact device contract.  For
21423    // ordinary SiLU/GELU this is a zero clamp; MiniMax M3 contributes its
21424    // dedicated OAI marker and frozen 7.0 source limit.  Anything without a
21425    // device contract stays on the CPU.
21426    let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
21427    let act = graph_act.code();
21428    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
21429    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
21430    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
21431    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21432        return None; // mixed-dtype trio — honest CPU path
21433    }
21434    if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
21435        return None;
21436    }
21437    if !gq2 && dq2 {
21438        return None;
21439    }
21440    model_ref.get_or_insert_with(|| gm.clone());
21441    let gdt = if gcf.is_empty() {
21442        cortiq_core::TensorDtype::Q8Row
21443    } else {
21444        cortiq_core::TensorDtype::Q8_2f
21445    };
21446    let udt = if ucf.is_empty() {
21447        cortiq_core::TensorDtype::Q8Row
21448    } else {
21449        cortiq_core::TensorDtype::Q8_2f
21450    };
21451    jobs.push(crate::gpu::MoeJob {
21452        gate: (gi, gr, gc, grs),
21453        up: (ui, ur, uc, urs),
21454        down: (di, dr, dc, drs),
21455        xs_gate: prescale(x, gcf, gdt).into_owned(),
21456        xs_up: prescale(x, ucf, udt).into_owned(),
21457        down_col: dcf,
21458        w,
21459        q1: gq1,
21460        q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
21461        q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
21462        gu_q2: gq2,
21463        swiglu_limit,
21464        act,
21465    });
21466    Some(())
21467}
21468
21469/// Sparse dense-FFN directly on QUANTIZED weights (mask × mmap): reads
21470/// ONLY the active neurons' gate/up rows and down columns from the mmap
21471/// — no full-matrix dequant, no f32 model copy. This is what lets a
21472/// masked big model run at quantized RSS (the historical mask path
21473/// forced the whole model to f32). Semantics identical to the f32
21474/// sparse path within quant tolerance.
21475fn sparse_ffn_quant(
21476    d: &DenseFfn,
21477    x: &[f32],
21478    active: &[u16],
21479    hidden: usize,
21480    pool: Option<&Pool>,
21481) -> Vec<f32> {
21482    let n = active.len();
21483    let inter = d.gate_proj.rows();
21484    let mut act = vec![0.0f32; n];
21485    // Scratch is needed if EITHER projection is group-packed (q4/vbit);
21486    // gate/up normally share a dtype but sizing on both is robust.
21487    let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
21488    let compute = |ai: usize| -> f32 {
21489        let idx = active[ai] as usize;
21490        if idx >= inter {
21491            return 0.0; // defensive parity with the f32 sparse path
21492        }
21493        let mut s = if need_scratch {
21494            vec![0.0f32; hidden]
21495        } else {
21496            Vec::new()
21497        };
21498        let gate = d.gate_proj.row_dot(idx, x, &mut s);
21499        let up = d.up_proj.row_dot(idx, x, &mut s);
21500        d.act.combine(gate, up)
21501    };
21502    match pool {
21503        Some(p) if n >= 256 => {
21504            let ptr = SendMut(act.as_mut_ptr());
21505            p.run(&|widx, nw| {
21506                let chunk = n.div_ceil(nw);
21507                let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
21508                for ai in s..e {
21509                    unsafe { *ptr.at(ai) = compute(ai) };
21510                }
21511            });
21512        }
21513        _ => {
21514            for (ai, a) in act.iter_mut().enumerate() {
21515                *a = compute(ai);
21516            }
21517        }
21518    }
21519    // Scatter through active down columns (reads only those columns).
21520    let mut out = vec![0.0f32; hidden];
21521    for (ai, &idx) in active.iter().enumerate() {
21522        let w = act[ai];
21523        if w.abs() >= 1e-12 && (idx as usize) < inter {
21524            d.down_proj.add_col_scaled(idx as usize, w, &mut out);
21525        }
21526    }
21527    out
21528}
21529
21530/// Test-only re-export of the private sparse-quant FFN (mask × mmap gate).
21531#[doc(hidden)]
21532pub fn sparse_ffn_quant_for_test(
21533    d: &DenseFfn,
21534    x: &[f32],
21535    active: &[u16],
21536    hidden: usize,
21537) -> Vec<f32> {
21538    sparse_ffn_quant(d, x, active, hidden, None)
21539}
21540
21541/// Dequantize a DenseFfn's three matrices to f32 (transient; only the
21542/// q4/vbit-masked fallback uses it — the memory-lean path is
21543/// sparse_ffn_quant). Reuses row_f32 row-by-row.
21544fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
21545    let deq = |t: &QTensor| -> Vec<f32> {
21546        let (rows, cols) = (t.rows(), t.cols());
21547        let mut out = vec![0.0f32; rows * cols];
21548        for r in 0..rows {
21549            t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
21550        }
21551        out
21552    };
21553    (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
21554}
21555
21556/// Pointer wrapper for the worker-pool scatter (same pattern as qtensor).
21557struct SendMut(*mut f32);
21558unsafe impl Send for SendMut {}
21559unsafe impl Sync for SendMut {}
21560impl SendMut {
21561    #[inline]
21562    // Deliberate unsynchronized scatter: pool workers write disjoint indices
21563    // in parallel, so returning `&mut` from `&self` is intentional here.
21564    #[allow(clippy::mut_from_ref)]
21565    unsafe fn at(&self, i: usize) -> &mut f32 {
21566        unsafe { &mut *self.0.add(i) }
21567    }
21568}
21569
21570/// Router → (selected experts in torch.topk order, per-expert score
21571/// vector, normalizer). The final weight of expert `e` is `p[e] / wsum`.
21572///
21573/// Two regimes share this. Qwen: softmax over ALL experts, top-k of the
21574/// probabilities, optional renorm — `router_sigmoid=false`, no bias,
21575/// scale 1 → bit-identical to the historical path. LFM2-MoE /
21576/// DeepSeek-V3 `noaux_tc`: per-expert sigmoid scores, an optional
21577/// selection bias (top-k CHOICE only; weights stay unbiased), an
21578/// architecture-specified renorm floor and a routed scale.  The ordinary
21579/// loader contract is `1e-6`; Flint and MiniMax M3.1 preserve their
21580/// reference runtime's selected-score `sum + 1e-20`.
21581pub(crate) fn moe_route(
21582    logits: &[f32],
21583    m: &MoeFfn,
21584    allowed: Option<&[bool]>,
21585) -> (Vec<usize>, Vec<f32>, f32) {
21586    moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
21587}
21588
21589/// Router implementation with an explicit sigmoid renormalization floor.
21590///
21591/// Flint and MiniMax M3.1 reference runtimes compute
21592/// `sum(selected_scores) + 1e-20` in FP32. Using the generic 1e-6 floor is
21593/// not a harmless tolerance difference when all logits are very negative: it
21594/// collapses the routed branch toward zero instead of normalizing the selected
21595/// experts. Keeping the epsilon parameter here avoids changing the established
21596/// Qwen/LFM2 contract while allowing each architecture to preserve its own
21597/// numerical semantics.
21598pub(crate) fn moe_route_with_eps(
21599    logits: &[f32],
21600    m: &MoeFfn,
21601    allowed: Option<&[bool]>,
21602    sigmoid_denom_eps: f32,
21603) -> (Vec<usize>, Vec<f32>, f32) {
21604    let ne = logits.len();
21605    // Expert restriction: the static env mask (CMF_MOE_MASK) AND the
21606    // active task mask's expert fields (spec §5) both narrow the
21607    // candidate set; selection happens over the admitted experts only.
21608    // With norm_topk the kept weights renormalize below; without it
21609    // the excluded mass is honestly dropped.
21610    let admit = |e: usize| {
21611        m.mask.as_ref().is_none_or(|mk| mk[e])
21612            && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
21613    };
21614    // The resonance router (spec §9.5.1) selects by the RAW score: the
21615    // trainer (`resonance_winner`) and the resident graph
21616    // (`embryo_core_route_pick`) take the first maximum of the scores
21617    // and run the winner with weight 1.0. Selecting through the softmax
21618    // instead is not the same decision: `exp(l − max)` rounds two scores
21619    // closer than 2^-25 (possible below |score| 0.25) to the same 1.0, and
21620    // the lower index would take a token whose score is strictly smaller
21621    // — the trainer's trace and the graph would disagree with this path.
21622    // `−∞` (outside the shell) never wins; with no finite admitted expert
21623    // the generic path below degrades to uniform. The winner's
21624    // probability is 1.0 by construction (a one-hot `p`), so its
21625    // renormalized weight is `routed_scaling` on both norm_topk settings.
21626    if m.resonance.is_some() && m.top_k == 1 {
21627        let mut best: Option<usize> = None;
21628        for e in (0..ne).filter(|&e| admit(e)) {
21629            let l = logits[e];
21630            if l == f32::NEG_INFINITY || l.is_nan() {
21631                continue;
21632            }
21633            if best.is_none_or(|b| l > logits[b]) {
21634                best = Some(e);
21635            }
21636        }
21637        if let Some(b) = best {
21638            let mut p = vec![0.0f32; ne];
21639            p[b] = 1.0;
21640            return (vec![b], p, 1.0 / m.routed_scaling);
21641        }
21642    }
21643    // A `−∞` logit (a grown expert outside its shell, `Resonance::scores`)
21644    // takes probability 0 on both paths: sigmoid(−∞) = 0, exp(−∞ − max) = 0
21645    // — top-1 is the best FINITE expert, its renormalized weight exactly
21646    // 1.0. Every expert at −∞ cannot happen (trunk experts have no shell);
21647    // should it, the softmax would be NaN, so it degrades to uniform.
21648    let p: Vec<f32> = if m.router_sigmoid {
21649        logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
21650    } else {
21651        let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
21652        if mx == f32::NEG_INFINITY {
21653            vec![1.0 / ne.max(1) as f32; ne]
21654        } else {
21655            let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
21656            let s: f32 = e.iter().sum();
21657            for v in &mut e {
21658                *v /= s;
21659            }
21660            e
21661        }
21662    };
21663    let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
21664    // Descending by selection score, lower index wins ties (torch.topk).
21665    match &m.expert_bias {
21666        Some(b) => idx.sort_unstable_by(|&x, &y| {
21667            (p[y] + b[y])
21668                .partial_cmp(&(p[x] + b[x]))
21669                .unwrap()
21670                .then(x.cmp(&y))
21671        }),
21672        None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
21673    }
21674    idx.truncate(m.top_k);
21675    // Adaptive τ-routing: trim the tail experts once the kept mass is
21676    // enough. wsum below renormalizes over the KEPT set, so the output
21677    // stays a proper weighted average.
21678    if let Some(tau) = m.route_tau {
21679        let total: f32 = idx.iter().map(|&e| p[e]).sum();
21680        if total > 0.0 {
21681            let mut acc = 0.0f32;
21682            let mut keep = idx.len();
21683            for (i, &e) in idx.iter().enumerate() {
21684                acc += p[e];
21685                if acc >= tau * total {
21686                    keep = i + 1;
21687                    break;
21688                }
21689            }
21690            idx.truncate(keep);
21691        }
21692    }
21693    let wsum: f32 = if m.norm_topk_prob {
21694        let s: f32 = idx.iter().map(|&e| p[e]).sum();
21695        // Sigmoid routers use their architecture's reference floor; the
21696        // softmax path's probs already sum near 1, so it stays exactly as
21697        // before.
21698        (if m.router_sigmoid {
21699            s + sigmoid_denom_eps
21700        } else {
21701            s
21702        }) / m.routed_scaling
21703    } else {
21704        1.0 / m.routed_scaling
21705    };
21706    (idx, p, wsum)
21707}
21708
21709/// See the call site: one `layer:e1,e2,…` line per routed token.
21710fn moe_trace(idx: &[usize]) {
21711    moe_trace_at(crate::gpu::cur_layer() as i32, idx)
21712}
21713
21714/// The same, for callers that know their layer (DSV4 owns its layers and
21715/// never sets the pipeline's current-layer marker).
21716pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
21717    use std::io::Write;
21718    static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
21719        std::sync::OnceLock::new();
21720    let Some(f) = F.get_or_init(|| {
21721        let p = std::env::var("CMF_MOE_TRACE").ok()?;
21722        Some(std::sync::Mutex::new(
21723            std::fs::OpenOptions::new()
21724                .create(true)
21725                .append(true)
21726                .open(p)
21727                .ok()?,
21728        ))
21729    }) else {
21730        return;
21731    };
21732    let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
21733    let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
21734}
21735
21736/// MoE FFN: router → top-k experts (see `moe_route`). Only selected
21737/// experts' pages are touched in mmap.
21738pub(crate) fn moe_ffn(
21739    m: &MoeFfn,
21740    x: &[f32],
21741    pool: Option<&Pool>,
21742    allowed: Option<&[bool]>,
21743) -> Vec<f32> {
21744    let r = moe_ffn_route(m, x, pool, allowed);
21745    moe_ffn_experts(m, x, &r, pool)
21746}
21747
21748/// One token's host route through a MoE layer: the chosen experts in
21749/// selection order, the per-expert scores and the normalizer (see
21750/// `moe_route`), plus the raw router logits.
21751pub(crate) struct MoeRoute {
21752    pub idx: Vec<usize>,
21753    pub p: Vec<f32>,
21754    pub wsum: f32,
21755    pub logits: Vec<f32>,
21756}
21757
21758/// The routing half of `moe_ffn`, shared by every executor of the chosen
21759/// experts (the host/per-op path below and the MiMo dynamic device cache,
21760/// `crate::mimo_moe`): activation accounting, router logits, `moe_route`,
21761/// the selection statistics and the `CMF_MOE_TRACE` line — so switching
21762/// executors can never change which experts a token gets.
21763pub(crate) fn moe_ffn_route(
21764    m: &MoeFfn,
21765    x: &[f32],
21766    pool: Option<&Pool>,
21767    allowed: Option<&[bool]>,
21768) -> MoeRoute {
21769    accumulate_act(m, x, 1);
21770    let ne = m.experts.len();
21771    let mut logits = vec![0.0f32; ne];
21772    match &m.resonance {
21773        Some(r) => r.scores(x, &mut logits),
21774        None => m.router.matvec(x, &mut logits, pool),
21775    }
21776    let (idx, p, wsum) = moe_route(&logits, m, allowed);
21777    {
21778        let mut st = m.stats.borrow_mut();
21779        if st.len() < ne {
21780            st.resize(ne, 0);
21781        }
21782        for &e in &idx {
21783            st[e] += 1;
21784        }
21785    }
21786    // `CMF_MOE_TRACE=<file>`: append one line per (layer, token) with the
21787    // selected expert ids. The cumulative `stats` above answer "which
21788    // experts are popular"; a residency design needs the question they
21789    // cannot answer — whether CONSECUTIVE tokens reuse experts (the
21790    // temporal locality an LRU cache lives on, FreeToken §4).
21791    moe_trace(&idx);
21792    MoeRoute {
21793        idx,
21794        p,
21795        wsum,
21796        logits,
21797    }
21798}
21799
21800/// Flint's released mixed MoE profile has a native-Metal selected-expert
21801/// pager with a different numerical contract from the generic CPU probe:
21802/// the pager keeps activations in f32, whereas the fast host arm normally
21803/// uses its A8W8 activation path.  Timing those two approximations against
21804/// one another can make the probe reject the exact pager even when it is the
21805/// faster exact-f32 implementation.  Keep this gate deliberately narrower
21806/// than the pager itself; the pager revalidates every selected tuple before
21807/// it binds a no-copy view and an honest refusal still takes the CPU path.
21808#[cfg(target_os = "macos")]
21809fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
21810    const HIDDEN: usize = 2_048;
21811    const ROUTED_INTER: usize = 512;
21812    const SHARED_INTER: usize = 1_024;
21813    const EXPERTS: usize = 256;
21814    const TOP_K: usize = 6;
21815
21816    let Some(routed) = m.experts.first() else {
21817        return false;
21818    };
21819    let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
21820        return false;
21821    };
21822    let Some((shared, shared_gate)) = &m.shared else {
21823        return false;
21824    };
21825    if shared_gate.is_some()
21826        || model.header.arch.arch_name != "flint"
21827        || model.header.arch.prism_hadamard.is_some()
21828        || model.header.arch.hidden_size != HIDDEN
21829        || m.experts.len() != EXPERTS
21830        || m.top_k != TOP_K
21831        || !m.router_sigmoid
21832        || !m.norm_topk_prob
21833        || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
21834        || m.routed_scaling.to_bits() != 1.0f32.to_bits()
21835        || m.route_tau.is_some()
21836        || m.mask.is_some()
21837        || m.per_expert_scale.is_some()
21838        || m.router_input_norm
21839        || m.resonance.is_some()
21840        || !m.grown.is_empty()
21841    {
21842        return false;
21843    }
21844
21845    let routed_q2 = routed.act == Act::Silu
21846        && routed.segs.is_empty()
21847        && routed.gate_proj.rows() == ROUTED_INTER
21848        && routed.gate_proj.cols() == HIDDEN
21849        && routed.up_proj.rows() == ROUTED_INTER
21850        && routed.up_proj.cols() == HIDDEN
21851        && routed.down_proj.rows() == HIDDEN
21852        && routed.down_proj.cols() == ROUTED_INTER
21853        && routed.up_proj.mapped_q2tp().is_some()
21854        && routed.down_proj.mapped_q4tp().is_some();
21855    let shared_q4 = shared.act == Act::Silu
21856        && shared.segs.is_empty()
21857        && shared.gate_proj.rows() == SHARED_INTER
21858        && shared.gate_proj.cols() == HIDDEN
21859        && shared.up_proj.rows() == SHARED_INTER
21860        && shared.up_proj.cols() == HIDDEN
21861        && shared.down_proj.rows() == HIDDEN
21862        && shared.down_proj.cols() == SHARED_INTER
21863        && shared.gate_proj.mapped_q4tp().is_some()
21864        && shared.up_proj.mapped_q4tp().is_some()
21865        && shared.down_proj.mapped_q4tp().is_some();
21866    routed_q2 && shared_q4
21867}
21868
21869/// The expert half of `moe_ffn`: run a route's experts on the per-op GPU
21870/// block or the host.
21871pub(crate) fn moe_ffn_experts(
21872    m: &MoeFfn,
21873    x: &[f32],
21874    r: &MoeRoute,
21875    pool: Option<&Pool>,
21876) -> Vec<f32> {
21877    let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
21878    // D5: the whole layer MoE block in one GPU command buffer (experts — the
21879    // same mmap via a no-copy buffer; intermediate activations on the GPU).
21880    // MoE owns a separate probe class from dense FFN: its selected-span pager
21881    // has materially different residency and latency costs.
21882    if crate::gpu::enabled_here() {
21883        #[cfg(target_os = "macos")]
21884        if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
21885            // Do not compare the pager's exact-f32 result with the CPU's
21886            // optional A8W8 activation approximation.  The strict pager is
21887            // the validated Flint path; if a directory/view/device check
21888            // declines, preserve the existing CPU fallback rather than
21889            // leaving the probe in a half-sampled state.
21890            return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
21891                crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
21892            });
21893        }
21894        match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
21895            crate::gpu::ProbeArm::Gpu => {
21896                let t0 = std::time::Instant::now();
21897                if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
21898                    crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
21899                    return out;
21900                }
21901                crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
21902            }
21903            crate::gpu::ProbeArm::CpuTimed => {
21904                let t0 = std::time::Instant::now();
21905                let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
21906                crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
21907                return out;
21908            }
21909            crate::gpu::ProbeArm::Cpu => {
21910                return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
21911            }
21912        }
21913    }
21914    moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
21915}
21916
21917/// One MiniCPM CPU-tail MoE through its opt-in dynamic bank, or through the
21918/// same-route host fallback.  The route is formed exactly once so statistics,
21919/// tracing, and model semantics remain owned by `moe_ffn_route`.
21920fn moe_ffn_minicpm_tail(
21921    slot: &mut crate::minicpm_tail::MiniCpmTail,
21922    li: usize,
21923    total_layers: usize,
21924    m: &MoeFfn,
21925    x: &[f32],
21926    pool: Option<&Pool>,
21927) -> Vec<f32> {
21928    let route = moe_ffn_route(m, x, pool, None);
21929    match slot.forward(li, m, x, &route, pool, total_layers) {
21930        Some(out) => out,
21931        // A cache refusal must be observationally identical to the generic
21932        // CPU-tail path. The surrounding host-tail scope already disables the
21933        // per-op arena, so do not introduce a float-activation policy change
21934        // merely because this explicit opt-in cache declined a model/layout.
21935        None => moe_ffn_experts(m, x, &route, pool),
21936    }
21937}
21938
21939/// One MoE token through the MiMo expert bank (`crate::mimo_moe`), or —
21940/// when the bank does not serve it — through the host path with the SAME
21941/// route, so the routing statistics and `CMF_MOE_TRACE` see it once.
21942fn moe_ffn_banked(
21943    slot: &mut crate::mimo_moe::Slot,
21944    li: usize,
21945    m: &MoeFfn,
21946    x: &[f32],
21947    pool: Option<&Pool>,
21948) -> Vec<f32> {
21949    let t0 = std::time::Instant::now();
21950    let r = moe_ffn_route(m, x, pool, None);
21951    slot.note_route(t0.elapsed().as_nanos() as u64);
21952    match slot.forward(li, m, x, &r, pool) {
21953        Some(out) => out,
21954        None => crate::qtensor::float_activations_scope(|| {
21955            crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
21956        }),
21957    }
21958}
21959
21960/// Verify rows share a bank frame; routing and fallback are decode's.
21961fn moe_ffn_banked_rows(
21962    slot: &mut crate::mimo_moe::Slot,
21963    li: usize,
21964    m: &MoeFfn,
21965    xs: &[f32],
21966    b: usize,
21967    hidden: usize,
21968    pool: Option<&Pool>,
21969) -> Vec<f32> {
21970    let t0 = std::time::Instant::now();
21971    let routes: Vec<_> = xs
21972        .chunks_exact(hidden)
21973        .map(|x| moe_ffn_route(m, x, pool, None))
21974        .collect();
21975    slot.note_route(t0.elapsed().as_nanos() as u64);
21976    if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
21977        return out;
21978    }
21979    let mut out = Vec::with_capacity(b * hidden);
21980    for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
21981        let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
21982            // A failed bank must not stream missing experts into the arena.
21983            crate::qtensor::float_activations_scope(|| {
21984                crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
21985            })
21986        });
21987        out.extend(row);
21988    }
21989    out
21990}
21991
21992/// One-shot report of whether the whole-token wgpu graph actually formed.
21993/// A refusal silently reverts to the per-op path, which is how a model can
21994/// look "GPU-accelerated" while every layer walks the host.  A device prefix
21995/// is tracked separately because it still pays a host boundary for the tail.
21996fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
21997    use std::sync::atomic::{AtomicBool, Ordering};
21998    if built {
21999        GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
22000        if total_layers > 0 && layers_run < total_layers {
22001            GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
22002        } else {
22003            GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
22004        }
22005    } else {
22006        GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
22007    }
22008    static SAID: AtomicBool = AtomicBool::new(false);
22009    if !SAID.swap(true, Ordering::Relaxed) {
22010        if built {
22011            tracing::info!("wgpu whole-token graph: ACTIVE");
22012        } else {
22013            tracing::warn!("wgpu whole-token graph refused — per-op path");
22014        }
22015    }
22016}
22017
22018/// Whole-token graph outcomes, process-wide: a benchmark that claims a
22019/// GPU number while MISS climbs is measuring the CPU — the honest-bench
22020/// contract makes that an error, not a footnote.
22021pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22022pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22023/// Graph calls that returned a hidden after running only a leading device
22024/// prefix.  These are valid hybrid executions but must not be reported as a
22025/// full GPU graph in benchmark evidence.
22026pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22027/// Graph calls that covered the complete requested layer span.
22028pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22029
22030/// Native Metal TokenGraph completion counters. These are incremented only
22031/// after checked command-buffer completion and successful readback, so a
22032/// fused-head NLL report can prove the route rather than infer it from env.
22033pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22034pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22035pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
22036    std::sync::atomic::AtomicU64::new(0);
22037pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22038pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22039/// Ordinary native-Metal rows-prefill admissions and completed rows.  These
22040/// counters are separate from TokenGraph token/head counts so a batch NLL
22041/// receipt cannot accidentally claim serial execution as batched.
22042pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
22043    std::sync::atomic::AtomicU64::new(0);
22044pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22045pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
22046    std::sync::atomic::AtomicU64::new(0);
22047pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
22048    std::sync::atomic::AtomicU64::new(0);
22049
22050/// `CMF_MOE_BATCH=0` restores the per-expert serial loop — the A/B lever
22051/// for the batched kernel, and how its bit-identity is checked.
22052fn moe_batch_enabled() -> bool {
22053    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
22054    *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
22055}
22056
22057/// Two-dispatch CPU MoE: every routed expert (and the shared one) fused
22058/// into one gate/up/SiLU dispatch and one down dispatch, instead of two
22059/// pool barriers per expert. Bit-identical to the serial loop below —
22060/// see `moe_gate_up_many` / `moe_down_many`. `None` = the batched kernel
22061/// does not cover this layer, walk the serial path.
22062fn moe_ffn_cpu_batched(
22063    m: &MoeFfn,
22064    x: &[f32],
22065    idx: &[usize],
22066    p: &[f32],
22067    wsum: f32,
22068    pool: Option<&Pool>,
22069) -> Option<Vec<f32>> {
22070    if idx.is_empty() || !moe_batch_enabled() {
22071        return None;
22072    }
22073    // The bake probe reads per-neuron activation mass out of the
22074    // single-expert path; batching would skip it. Rare and offline —
22075    // hand those runs to the serial loop.
22076    if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
22077        return None;
22078    }
22079    let n = idx.len() + usize::from(m.shared.is_some());
22080    let mut pairs = Vec::with_capacity(n);
22081    let mut downs = Vec::with_capacity(n);
22082    let mut ws = Vec::with_capacity(n);
22083    for &e in idx {
22084        let d = &m.experts[e];
22085        if d.act != Act::Silu {
22086            return None;
22087        }
22088        pairs.push((&d.gate_proj, &d.up_proj));
22089        downs.push(&d.down_proj);
22090        ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
22091    }
22092    // The shared expert goes last, matching the serial loop's order —
22093    // the f32 accumulation order is part of the bit-identity claim.
22094    if let Some((se, gate)) = &m.shared {
22095        if se.act != Act::Silu {
22096            return None;
22097        }
22098        let g = gate.as_ref().map_or(1.0, |gate| {
22099            let mut gl = [0.0f32; 1];
22100            gate.matvec(x, &mut gl, pool);
22101            1.0 / (1.0 + (-gl[0]).exp())
22102        });
22103        pairs.push((&se.gate_proj, &se.up_proj));
22104        downs.push(&se.down_proj);
22105        ws.push(g);
22106    }
22107    let inter = pairs[0].0.rows();
22108    let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
22109    if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
22110        return None;
22111    }
22112    let mut out = attention::take_buf(x.len());
22113    if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
22114        attention::recycle_buf(&mut out);
22115        return None;
22116    }
22117    Some(out)
22118}
22119
22120/// Exact CPU completion for the routed experts a dynamic device cache did
22121/// not contain. The weights are already the router's final normalized mix.
22122/// Keeping this independent of `MoeFfn` makes the job `Sync`: its routing
22123/// statistics live in a `RefCell`, while the immutable expert tensors can be
22124/// evaluated safely in parallel with the GPU's resident subset.
22125/// Pooled-tail stage timers, reported per token by the bench JSON
22126/// (`m3_span_ms_per_tok`, `m3_cold_ms_per_tok`, `m3_arm_ms_per_tok`).
22127pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22128pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22129pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22130pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22131pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22132
22133pub(crate) fn moe_cold_experts_cpu(
22134    experts: &[(&DenseFfn, f32)],
22135    x: &[f32],
22136    pool: Option<&Pool>,
22137) -> Vec<f32> {
22138    let mut out = attention::take_buf(x.len());
22139    if experts.is_empty() {
22140        return out;
22141    }
22142    let pairs: Vec<_> = experts
22143        .iter()
22144        .map(|(e, _)| (&e.gate_proj, &e.up_proj))
22145        .collect();
22146    let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
22147    let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
22148    let inter = experts[0].0.gate_proj.rows();
22149    let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
22150    // The batched gate/up kernels implement PLAIN SiLU only (their legacy
22151    // helper passes limit 0 and no activation code). MiniMax's SwiGLU-OAI
22152    // (alpha 1.702, shifted up) must take the per-expert path that reads
22153    // each tensor's own activation — otherwise every cold expert computes
22154    // silu and the residual walks away from the reference (measured: the
22155    // pooled tail shipped cos 0.047 hidden while the resident path was
22156    // bit-close).
22157    let silu_only = experts
22158        .iter()
22159        .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
22160    if silu_only
22161        && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
22162        && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
22163    {
22164        return out;
22165    }
22166    out.fill(0.0);
22167    for &(expert, weight) in experts {
22168        let mut one = dense_ffn(expert, x, pool);
22169        for (o, v) in out.iter_mut().zip(&one) {
22170            *o += weight * v;
22171        }
22172        attention::recycle_buf(&mut one);
22173    }
22174    out
22175}
22176
22177/// Cold part of a short bank batch. Share each expert's weight stream
22178/// across its tokens, but reduce contributions in each token's route order.
22179/// On an unsupported CPU/layout, retain the single-token cold kernels.
22180pub(crate) fn moe_cold_experts_rows_cpu(
22181    jobs: &[Vec<(&DenseFfn, f32)>],
22182    xs: &[f32],
22183    hidden: usize,
22184    pool: Option<&Pool>,
22185) -> Vec<f32> {
22186    let mut out = vec![0.0; xs.len()];
22187    let mut experts: Vec<&DenseFfn> = Vec::new();
22188    let mut groups: Vec<Vec<usize>> = Vec::new();
22189    let mut terms = vec![Vec::new(); jobs.len()];
22190    for (r, row) in jobs.iter().enumerate() {
22191        for &(e, w) in row {
22192            let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
22193                Some(g) => g,
22194                None => {
22195                    experts.push(e);
22196                    groups.push(Vec::new());
22197                    groups.len() - 1
22198                }
22199            };
22200            terms[r].push((g, groups[g].len(), w));
22201            groups[g].push(r);
22202        }
22203    }
22204    if experts.is_empty() {
22205        return out;
22206    }
22207    let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
22208    let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
22209    let lens: Vec<_> = groups.iter().map(Vec::len).collect();
22210    let count: usize = lens.iter().sum();
22211    let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
22212    let mut ds = vec![vec![0.0; hidden]; count];
22213    if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
22214        && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
22215    {
22216        let mut offset = 0;
22217        let offsets: Vec<_> = lens
22218            .iter()
22219            .map(|&n| {
22220                let start = offset;
22221                offset += n;
22222                start
22223            })
22224            .collect();
22225        for (r, terms) in terms.iter().enumerate() {
22226            for &(g, slot, w) in terms {
22227                for (o, &v) in out[r * hidden..(r + 1) * hidden]
22228                    .iter_mut()
22229                    .zip(&ds[offsets[g] + slot])
22230                {
22231                    *o += w * v;
22232                }
22233            }
22234        }
22235    } else {
22236        for (r, jobs) in jobs.iter().enumerate() {
22237            if !jobs.is_empty() {
22238                let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
22239                out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
22240                attention::recycle_buf(&mut row);
22241            }
22242        }
22243    }
22244    out
22245}
22246
22247/// The pure-CPU MoE expert loop (also the fallback of every GPU refusal).
22248fn moe_ffn_cpu(
22249    m: &MoeFfn,
22250    x: &[f32],
22251    idx: &[usize],
22252    p: &[f32],
22253    wsum: f32,
22254    pool: Option<&Pool>,
22255) -> Vec<f32> {
22256    if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
22257        return out;
22258    }
22259    let mut out = attention::take_buf(x.len());
22260    for &e in idx {
22261        let mut eo = dense_ffn(&m.experts[e], x, pool);
22262        let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
22263        for i in 0..out.len() {
22264            out[i] += w * eo[i];
22265        }
22266        attention::recycle_buf(&mut eo);
22267    }
22268    if let Some((se, gate)) = &m.shared {
22269        let mut so = dense_ffn(se, x, pool);
22270        let g = gate.as_ref().map_or(1.0, |gate| {
22271            let mut gl = [0.0f32; 1];
22272            gate.matvec(x, &mut gl, pool);
22273            1.0 / (1.0 + (-gl[0]).exp())
22274        });
22275        for i in 0..out.len() {
22276            out[i] += g * so[i];
22277        }
22278        attention::recycle_buf(&mut so);
22279    }
22280    out
22281}
22282
22283/// DeepSeek-V2 MLA forward, expand-to-MHA form (see `AttnKind::Mla`):
22284/// per token the latent expands to every head's K/V and the ordinary
22285/// cache + grouped attend do the rest. K head layout is [rope | nope]
22286/// (rotary_dim = qk_rope rotates the shared rope key and each q head's
22287/// prefix); V rows are zero-padded to the K head_dim inside the cache
22288/// and the pad is sliced off before O. Attention importance is not
22289/// accumulated for MLA yet (no eviction interplay).
22290#[allow(clippy::too_many_arguments)]
22291pub(crate) fn mla_attention(
22292    w: &MlaWeights,
22293    normed: &[f32],
22294    cache: &mut crate::kv_cache::LayerKvCache,
22295    position: usize,
22296    inv_freq: &[f32],
22297    rope_scale: f32,
22298    eps: f64,
22299    pool: Option<&Pool>,
22300) -> Vec<f32> {
22301    let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
22302    let hd = dr + dn;
22303    let mut q = vec![0.0f32; nh * hd];
22304    match (&w.q_a, &w.q_a_norm) {
22305        (Some(qa), Some(qn)) => {
22306            let mut t = vec![0.0f32; qa.rows()];
22307            qa.matvec(normed, &mut t, pool);
22308            let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
22309            w.q_proj.matvec(&tn, &mut q, pool);
22310        }
22311        _ => w.q_proj.matvec(normed, &mut q, pool),
22312    }
22313    let mut ca = vec![0.0f32; lora + dr];
22314    w.kv_a.matvec(normed, &mut ca, pool);
22315    let (c_lat, k_rope) = ca.split_at_mut(lora);
22316    let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
22317    let mut kvb = vec![0.0f32; nh * (dn + dv)];
22318    w.kv_b.matvec(&latn, &mut kvb, pool);
22319    if !w.nope {
22320        attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
22321    }
22322    for h in 0..nh {
22323        if !w.nope {
22324            attention::rope_rotate_scaled(
22325                &mut q[h * hd..h * hd + dr],
22326                position,
22327                inv_freq,
22328                rope_scale,
22329            );
22330        }
22331    }
22332    let mut k = vec![0.0f32; nh * hd];
22333    let mut v = vec![0.0f32; nh * hd];
22334    for h in 0..nh {
22335        k[h * hd..h * hd + dr].copy_from_slice(k_rope);
22336        k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
22337        v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
22338    }
22339    cache.append(&k, &v, &vec![true; nh]);
22340    let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
22341    attention::recycle_buf(&mut imp);
22342    let mut ov = vec![0.0f32; nh * dv];
22343    for h in 0..nh {
22344        ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
22345    }
22346    let mut out = vec![0.0f32; w.o_proj.rows()];
22347    w.o_proj.matvec(&ov, &mut out, pool);
22348    out
22349}
22350
22351/// Gemma-4 dual-branch FFN (spec: see `FfnKind::DenseMoe`). The dense
22352/// branch reads the pre-FFN-normed activation; the router and the
22353/// expert branch read the RAW residual — the router through a
22354/// scale-less rms norm (its constant gain is folded into the weights),
22355/// the experts through `pre_norm_2`. CPU path; GPU graphs refuse the
22356/// layer kind honestly.
22357fn dense_moe_ffn(
22358    dm: &DenseMoeFfn,
22359    x_normed: &[f32],
22360    h_raw: &[f32],
22361    eps: f64,
22362    norm_style: NormStyle,
22363    pool: Option<&Pool>,
22364) -> Vec<f32> {
22365    let mut d = dense_ffn(&dm.dense, x_normed, pool);
22366    d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
22367    let m = &dm.moe;
22368    let ne = m.experts.len();
22369    let mut logits = vec![0.0f32; ne];
22370    if m.router_input_norm {
22371        let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
22372        let inv = 1.0 / (ss + eps as f32).sqrt();
22373        let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
22374        m.router.matvec(&xr, &mut logits, pool);
22375    } else {
22376        m.router.matvec(h_raw, &mut logits, pool);
22377    }
22378    let (idx, p, wsum) = moe_route(&logits, m, None);
22379    {
22380        let mut st = m.stats.borrow_mut();
22381        if st.len() < ne {
22382            st.resize(ne, 0);
22383        }
22384        for &e in &idx {
22385            st[e] += 1;
22386        }
22387    }
22388    let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
22389    let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
22390    let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
22391    for (di, mi) in d.iter_mut().zip(&mo) {
22392        *di += mi;
22393    }
22394    d
22395}
22396
22397/// Building the MoE-layer GPU jobs: all selected experts (+shared) must
22398/// be q8_2f-Mapped from the primary mapping; otherwise None → CPU path.
22399/// One-shot report of why the MoE GPU block refused. A silent `?` here
22400/// sends every expert to the CPU with nothing in the logs to say so —
22401/// which is exactly how a q4tp MoE model looked "GPU-accelerated" while
22402/// running entirely on the host.
22403fn moe_gpu_refused(why: &'static str) {
22404    use std::sync::atomic::{AtomicBool, Ordering};
22405    static SAID: AtomicBool = AtomicBool::new(false);
22406    if !SAID.swap(true, Ordering::Relaxed) {
22407        tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
22408    }
22409}
22410
22411fn moe_ffn_gpu(
22412    m: &MoeFfn,
22413    x: &[f32],
22414    idx: &[usize],
22415    p: &[f32],
22416    wsum: f32,
22417    pool: Option<&Pool>,
22418) -> Option<Vec<f32>> {
22419    use crate::gpu::MoeJob;
22420
22421    let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
22422    let mut model_ref = None;
22423    for &e in idx {
22424        if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
22425            moe_gpu_refused("push_job(expert)");
22426            return None;
22427        }
22428    }
22429    if let Some((se, gate)) = &m.shared {
22430        let g = gate.as_ref().map_or(1.0, |gate| {
22431            let mut gl = [0.0f32; 1];
22432            gate.matvec(x, &mut gl, pool);
22433            1.0 / (1.0 + (-gl[0]).exp())
22434        });
22435        if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
22436            moe_gpu_refused("push_job(shared)");
22437            return None;
22438        }
22439    }
22440    let Some(model) = model_ref else {
22441        moe_gpu_refused("no model_ref");
22442        return None;
22443    };
22444    let hidden = jobs[0].down.1;
22445    let mut out = vec![0.0f32; hidden];
22446    if crate::gpu::moe_block(&model, &jobs, &mut out) {
22447        Some(out)
22448    } else {
22449        moe_gpu_refused("gpu::moe_block");
22450        None
22451    }
22452}
22453
22454/// Single-position FFN dispatch.
22455fn ffn_forward(
22456    ffn: &FfnKind,
22457    x: &[f32],
22458    pool: Option<&Pool>,
22459    experts_allowed: Option<&[bool]>,
22460) -> Vec<f32> {
22461    match ffn {
22462        FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
22463        FfnKind::Dense(d) => dense_ffn(d, x, pool),
22464        FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
22465        // Dual-branch layers need the raw residual — their callers
22466        // dispatch dense_moe_ffn directly; the auxiliary paths that land
22467        // here (MTP draft, o1 replay) do not co-occur with gemma-4 MoE.
22468        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22469    }
22470}
22471
22472/// Fused two-position FFN: gate/up/down streamed once (dense). MoE
22473/// falls back to two singles — expert sets differ per position, there
22474/// is nothing to fuse.
22475fn ffn_forward_pair(
22476    ffn: &FfnKind,
22477    x1: &[f32],
22478    x2: &[f32],
22479    pool: Option<&Pool>,
22480    experts_allowed: Option<&[bool]>,
22481) -> (Vec<f32>, Vec<f32>) {
22482    let d = match ffn {
22483        // A tube layer has nothing to fuse across the pair — the tubes
22484        // are separate matrices; two singles are the honest path.
22485        FfnKind::Dense(d) if !d.segs.is_empty() => {
22486            return (
22487                tube_ffn(d, x1, 1, pool, None),
22488                tube_ffn(d, x2, 1, pool, None),
22489            );
22490        }
22491        FfnKind::Dense(d) => d,
22492        FfnKind::Moe(m) => {
22493            return (
22494                moe_ffn(m, x1, pool, experts_allowed),
22495                moe_ffn(m, x2, pool, experts_allowed),
22496            );
22497        }
22498        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22499    };
22500    let inter = d.gate_proj.rows();
22501    FFN_SCRATCH.with(|s| {
22502        let mut s = s.borrow_mut();
22503        let [g1, g2, u1, u2] = &mut *s;
22504        g1.resize(inter, 0.0);
22505        g2.resize(inter, 0.0);
22506        u1.resize(inter, 0.0);
22507        u2.resize(inter, 0.0);
22508        // Multi-matrix pair job: gate+up under one pool dispatch
22509        // (o1s = lane-1 outputs across tensors, o2s = lane-2).
22510        QTensor::matvec2_many(
22511            [&d.gate_proj, &d.up_proj],
22512            x1,
22513            x2,
22514            [g1.as_mut_slice(), u1.as_mut_slice()],
22515            [g2.as_mut_slice(), u2.as_mut_slice()],
22516            pool,
22517        );
22518        for i in 0..inter {
22519            g1[i] = d.act.combine(g1[i], u1[i]);
22520            g2[i] = d.act.combine(g2[i], u2[i]);
22521        }
22522        let mut o1 = attention::take_buf(d.down_proj.rows());
22523        let mut o2 = attention::take_buf(d.down_proj.rows());
22524        d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
22525        (o1, o2)
22526    })
22527}
22528
22529#[cfg(test)]
22530mod tests {
22531    use std::ffi::OsString;
22532    use std::sync::Mutex;
22533
22534    // Environment switches are process-global. Keep the speculative-path
22535    // tests serialized and restore every prior value even during unwinding.
22536    static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
22537
22538    struct ScopedTestEnv {
22539        previous: Vec<(&'static str, Option<OsString>)>,
22540    }
22541
22542    impl ScopedTestEnv {
22543        fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
22544            let previous = vars
22545                .iter()
22546                .map(|(name, _)| (*name, std::env::var_os(name)))
22547                .collect();
22548            for &(name, value) in vars {
22549                unsafe {
22550                    match value {
22551                        Some(value) => std::env::set_var(name, value),
22552                        None => std::env::remove_var(name),
22553                    }
22554                }
22555            }
22556            Self { previous }
22557        }
22558    }
22559
22560    impl Drop for ScopedTestEnv {
22561        fn drop(&mut self) {
22562            for (name, value) in self.previous.drain(..).rev() {
22563                unsafe {
22564                    match value {
22565                        Some(value) => std::env::set_var(name, value),
22566                        None => std::env::remove_var(name),
22567                    }
22568                }
22569            }
22570        }
22571    }
22572
22573    #[test]
22574    fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
22575        use super::pad_mtp_draft_logits;
22576
22577        let mut logits = vec![-3.0, -1.0, -2.0];
22578        pad_mtp_draft_logits(&mut logits, 6);
22579        assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
22580        assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
22581        assert_eq!(
22582            logits
22583                .iter()
22584                .enumerate()
22585                .max_by(|(_, a), (_, b)| a.total_cmp(b))
22586                .map(|(i, _)| i),
22587            Some(1),
22588            "an unevaluated row cannot win an all-negative shortlist"
22589        );
22590    }
22591
22592    #[test]
22593    fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
22594        use super::{Pipeline, SpecMon, SpecTrial};
22595
22596        let mut mon = SpecMon::default();
22597        let start = std::time::Instant::now();
22598        mon.round(1.0, 1);
22599        let first = Pipeline::spec_trial_round(
22600            SpecTrial::Spec {
22601                t0: start,
22602                gen0: 0,
22603                rounds: 0,
22604            },
22605            &mut mon,
22606            1,
22607        );
22608        assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
22609
22610        mon.round(1.0, 1);
22611        let second = Pipeline::spec_trial_round(first, &mut mon, 2);
22612        assert!(
22613            matches!(second, SpecTrial::Plain { gen0: 2, .. }),
22614            "two verifier-tip-only rounds must stop the losing MTP trial"
22615        );
22616
22617        // One successful draft resets the streak, so a viable head retains
22618        // the normal five-round measurement budget.
22619        let mut viable = SpecMon::default();
22620        viable.round(1.0, 1);
22621        viable.round(1.0, 2);
22622        let continued = Pipeline::spec_trial_round(
22623            SpecTrial::Spec {
22624                t0: start,
22625                gen0: 0,
22626                rounds: 1,
22627            },
22628            &mut viable,
22629            3,
22630        );
22631        assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
22632    }
22633
22634    #[test]
22635    fn fresh_mtp_request_does_not_inherit_full_head_streak() {
22636        let mut p = spark_test_pipeline(None);
22637        let head_rows = 248_070;
22638        p.draft_full_streak = 7;
22639        assert_eq!(p.draft_head_rows(head_rows), head_rows);
22640
22641        p.reset_fresh_mtp_draft_state();
22642        assert_eq!(
22643            p.draft_head_rows(head_rows),
22644            super::Pipeline::draft_vocab_rows(head_rows),
22645            "a fresh request returns to its configured shortlist"
22646        );
22647
22648        // A high-id first token of the NEW request still restores the
22649        // full-head protection immediately.
22650        p.note_draft_id(u32::MAX);
22651        assert_eq!(p.draft_head_rows(head_rows), head_rows);
22652    }
22653
22654    #[test]
22655    fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
22656        use super::graph_spec_wanted_by_policy;
22657
22658        // A capable, otherwise-default-eligible native Metal model stays on
22659        // the serial path until the user deliberately asks for the
22660        // experimental verifier.
22661        assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
22662        assert!(!graph_spec_wanted_by_policy(
22663            Some("anything-but-1"),
22664            true,
22665            false,
22666            false,
22667            true,
22668        ));
22669        assert!(graph_spec_wanted_by_policy(
22670            Some("1"),
22671            false,
22672            true,
22673            false,
22674            true,
22675        ));
22676        assert!(!graph_spec_wanted_by_policy(
22677            Some("0"),
22678            true,
22679            false,
22680            false,
22681            true,
22682        ));
22683
22684        // Do not regress the established discrete/wgpu policy while closing
22685        // the native-Metal quality gate.
22686        assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
22687        assert!(graph_spec_wanted_by_policy(
22688            Some("legacy-force"),
22689            false,
22690            true,
22691            true,
22692            false,
22693        ));
22694    }
22695
22696    #[test]
22697    fn encoder_hidden_states_are_normalized_and_reusable() {
22698        let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
22699        let ids = [1, 7, 11, 19];
22700
22701        let first = p.encode_hidden_states(&ids).expect("encoder forward");
22702        let second = p
22703            .encode_hidden_states(&ids)
22704            .expect("reusable encoder forward");
22705        assert_eq!(first.len(), ids.len() * 8);
22706        assert!(first.iter().all(|v| v.is_finite()));
22707        assert_eq!(
22708            first, second,
22709            "encoder forward must start from a fresh state"
22710        );
22711
22712        // An encoder call must not leave stale KV/GDN state behind for the
22713        // regular generative API sharing the same Pipeline implementation.
22714        assert!(p.forward_ids(&ids, None).is_ok());
22715    }
22716
22717    /// The 0.7.6 prefill-chunk rule: a plain dense stack wholly on a
22718    /// discrete card reads the prompt in wide chunks on x86; every other
22719    /// case keeps the width it had (the GDN-hybrid, MoE and DeepSeek paths
22720    /// were tuned on hardware not measured for this change).
22721    #[test]
22722    fn prefill_chunk_rule_widens_only_dense_on_discrete() {
22723        use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
22724        let dense_card = ChunkStackFacts {
22725            plain_dense: true,
22726            discrete: true,
22727            gpu_on: true,
22728            ..Default::default()
22729        };
22730        assert!(dense_card.dense_on_discrete());
22731        // The bug: a dense Llama on a Vulkan RTX 3090 got 48.
22732        assert_eq!(
22733            prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
22734            DISCRETE_DENSE_PREFILL_CHUNK
22735        );
22736        assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
22737        for (label, facts) in [
22738            (
22739                "GDN hybrid / MoE / DeepSeek stack",
22740                ChunkStackFacts {
22741                    plain_dense: false,
22742                    ..dense_card
22743                },
22744            ),
22745            (
22746                "integrated GPU",
22747                ChunkStackFacts {
22748                    discrete: false,
22749                    ..dense_card
22750                },
22751            ),
22752            (
22753                "CPU only",
22754                ChunkStackFacts {
22755                    gpu_on: false,
22756                    discrete: false,
22757                    ..dense_card
22758                },
22759            ),
22760            (
22761                "capacity split",
22762                ChunkStackFacts {
22763                    capacity_split: true,
22764                    ..dense_card
22765                },
22766            ),
22767            (
22768                "multi-GPU plan",
22769                ChunkStackFacts {
22770                    multi_gpu: true,
22771                    ..dense_card
22772                },
22773            ),
22774            (
22775                "O(1) layers",
22776                ChunkStackFacts {
22777                    o1: true,
22778                    ..dense_card
22779                },
22780            ),
22781        ] {
22782            assert!(!facts.dense_on_discrete(), "{label}");
22783            assert_eq!(
22784                prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
22785                48,
22786                "{label} keeps the historical x86 chunk"
22787            );
22788        }
22789        // Other hosts are untouched whatever the model.
22790        for dense in [false, true] {
22791            assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
22792            assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
22793        }
22794        // CMF_PREFILL_CHUNK still wins everywhere (and is clamped to ≥ 1).
22795        for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
22796            for dense in [false, true] {
22797                assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
22798                assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
22799            }
22800        }
22801    }
22802
22803    #[test]
22804    fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
22805        use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
22806        let full = |host_rows, device_rows| ReuseLayer {
22807            full: true,
22808            host_rows,
22809            device_rows,
22810            device_state: false,
22811        };
22812        // Turn 1: 300-token prompt prefilled on the host, 40 tokens decoded
22813        // by the wgpu graph into the device mirror only. Turn 2 reuses 339.
22814        assert_eq!(
22815            kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
22816            ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
22817        );
22818        // CPU / Metal: the host owner already holds every forwarded row.
22819        assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
22820        // A mirror past the prefix is fine for the host (it gets rewound).
22821        assert_eq!(
22822            kv_reuse_plan(339, &[full(339, Some(345))]),
22823            ReusePlan::Ready
22824        );
22825        // GPU prefix / CPU tail: only the device layers lag.
22826        assert_eq!(
22827            kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
22828            ReusePlan::Pull(vec![(0, 300, 339)])
22829        );
22830        // The device cannot supply the missing rows: never continue.
22831        assert_eq!(
22832            kv_reuse_plan(339, &[full(300, Some(320))]),
22833            ReusePlan::Fresh
22834        );
22835        assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
22836        assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
22837        // A recurrent state advanced on the device cannot be handed to a
22838        // host prefill (it is not rewindable and the host copy is stale).
22839        let conv = |device_state| ReuseLayer {
22840            full: false,
22841            host_rows: 0,
22842            device_rows: None,
22843            device_state,
22844        };
22845        assert_eq!(
22846            kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
22847            ReusePlan::Fresh
22848        );
22849        assert_eq!(
22850            kv_reuse_plan(339, &[conv(false), full(339, None)]),
22851            ReusePlan::Ready
22852        );
22853    }
22854
22855    #[test]
22856    fn nll_graph_policy_scopes_only_the_fused_head() {
22857        for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
22858            // A Vulkan/Wgpu hidden-only graph remains the quality route.
22859            ("vulkan graph", true, true, false, true, false),
22860            // Native Metal adds the strict fused graph-head contract.
22861            ("native Metal graph", true, true, true, true, true),
22862            // Masked NLL and the explicit non-graph fallback remain unchanged.
22863            ("masked", false, true, false, false, false),
22864            ("graph disabled", true, false, true, false, false),
22865        ] {
22866            let (graph_quality, graph_head_required) =
22867                super::nll_graph_policy(unmasked, prefer_graph, native_metal);
22868            assert_eq!(graph_quality, want_graph, "{label}: graph quality");
22869            assert_eq!(graph_head_required, want_head, "{label}: fused head");
22870        }
22871    }
22872
22873    #[test]
22874    fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
22875        assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
22876        assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
22877        assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
22878        assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
22879        assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
22880    }
22881
22882    #[test]
22883    fn cancel_flag_stops_generation() {
22884        let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
22885        // Set before the call: the prefill loops honour it, the run
22886        // returns immediately with the cancelled reason and no tokens.
22887        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22888        let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
22889        assert_eq!(r.finish_reason, "cancelled");
22890        assert!(
22891            r.token_ids.is_empty(),
22892            "no tokens after cancel: {:?}",
22893            r.token_ids
22894        );
22895        assert_eq!(p.kv_cache.seq_len(), 0);
22896        assert!(p.kv_history.is_empty());
22897        assert!(!p.graph_want_logits);
22898        assert!(p.graph_logits.is_none());
22899        // Flag auto-cleared: the next call generates normally.
22900        let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
22901        assert_ne!(r2.finish_reason, "cancelled");
22902    }
22903    use super::*;
22904
22905    #[test]
22906    fn minimax_m3_arch_selects_checked_oai_activation_only() {
22907        // Keep this fixture minimal: `Act::from_arch_full` needs only the
22908        // architecture id and activation name, while serde supplies stable
22909        // defaults for unrelated CMF fields.
22910        let arch = |arch_name: &str, hidden_act: &str| {
22911            serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
22912                "arch_name": arch_name,
22913                "hidden_size": 8,
22914                "intermediate_size": 16,
22915                "num_layers": 1,
22916                "num_attention_heads": 1,
22917                "num_kv_heads": 1,
22918                "head_dim": 8,
22919                "vocab_size": 16,
22920                "layer_types": ["FullAttention"],
22921                "rms_norm_eps": 1e-6,
22922                "max_position_embeddings": 16,
22923                "hidden_act": hidden_act
22924            }))
22925            .expect("minimal architecture")
22926        };
22927        let minimax = arch("minimax_m3_vl", "swigluoai");
22928        assert_eq!(
22929            Act::from_arch_full(&minimax),
22930            Act::SwiGluOai {
22931                alpha: 1.702,
22932                limit: 7.0,
22933            }
22934        );
22935
22936        // The same spelling must not turn into MiniMax math on a generic
22937        // model.  Only the source-validated M3 architecture owns it.
22938        let unrelated = arch("qwen2", "swigluoai");
22939        assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
22940
22941        // Conversely, an M3-tagged header with a normal activation keeps the
22942        // normal path; this prevents the architecture id from overriding the
22943        // explicit serialized contract.
22944        let non_oai = arch("minimax_m3_vl", "gelu");
22945        assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
22946    }
22947
22948    #[test]
22949    fn minimax_swiglu_oai_act_matches_scalar_contract() {
22950        let act = Act::swiglu_oai(1.702, 7.0);
22951        let got = act.combine(10.0, -10.0);
22952        let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
22953        assert!((got - expected).abs() < 1e-6);
22954        assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
22955        assert_eq!(
22956            act.wgpu_graph_act(),
22957            Some(crate::gpu::GraphAct::SwiGluOaiM3),
22958            "the WGPU-only arm accepts only the frozen M3 constants"
22959        );
22960        assert_eq!(
22961            Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
22962            None,
22963            "nearby OAI constants must not borrow MiniMax's kernel"
22964        );
22965        assert_eq!(
22966            act.moe_job_graph_contract(),
22967            Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
22968            "the direct MoE path carries the frozen clamp with the marker"
22969        );
22970        assert_eq!(
22971            Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
22972            None,
22973            "a nearby clamp must not borrow MiniMax's device contract"
22974        );
22975
22976        // `apply` is the gate half only; `combine` owns the up clamp and
22977        // +1 offset. The two must not accidentally collapse to ordinary SiLU.
22978        let gate_only = act.apply(-10.0);
22979        let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
22980        assert!((gate_only - expected_gate).abs() < 1e-10);
22981    }
22982
22983    #[test]
22984    fn explicit_sliding_schedule_selects_local_rope_profile() {
22985        // Mellum alternates three local 1,024-token layers with one global
22986        // YaRN layer.  An explicit schedule deliberately overrides the
22987        // cadence sentinel in `swa`, so test both RoPE table and scale
22988        // dispatch rather than merely checking that the metadata loaded.
22989        let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
22990        p.swa = Some((1024, usize::MAX));
22991        p.sliding_layers = Some(vec![true, true, true, false]);
22992        p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
22993        p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
22994        p.rope_scale = 1.277_258_9;
22995        p.rope_scale_local = 1.0;
22996
22997        for li in 0..3 {
22998            assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
22999            assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
23000            assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
23001        }
23002        assert_eq!(p.layer_window(3), None);
23003        assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
23004        assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
23005    }
23006
23007    /// sparse_ffn_quant must equal a dense FFN where inactive neurons are
23008    /// zeroed (mask × mmap correctness). On F32 tensors this is EXACT —
23009    /// it validates the row_dot / add_col_scaled / scatter indexing, the
23010    /// bug-prone part. The q8 branches reuse the golden-tested linear
23011    /// The per-token sparse path reads a transposed `down`; it must
23012    /// agree with the arm that computes everything and zeroes the
23013    /// losers, or the speed measurement is measuring a different model.
23014    #[test]
23015    fn dynamic_ffn_equals_the_zeroing_arm() {
23016        let (hidden, inter) = (8usize, 32usize);
23017        let synth = |n: usize, salt: usize| -> Vec<f32> {
23018            (0..n)
23019                .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
23020                .collect()
23021        };
23022        let down = synth(hidden * inter, 3);
23023        let mut down_t = vec![0.0f32; inter * hidden];
23024        for r in 0..hidden {
23025            for c in 0..inter {
23026                down_t[c * hidden + r] = down[r * inter + c];
23027            }
23028        }
23029        let d = DenseFfn {
23030            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23031            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23032            down_proj: QTensor::from_f32(down.clone(), hidden, inter),
23033            act: Act::Silu,
23034            down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
23035            segs: Vec::new(),
23036        };
23037        let x = synth(hidden, 11);
23038        let k = 12usize;
23039        let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
23040        // Reference: full compute, keep the k loudest |silu(gate)|.
23041        let mut g = vec![0.0f32; inter];
23042        d.gate_proj.matvec(&x, &mut g, None);
23043        let mut u = vec![0.0f32; inter];
23044        d.up_proj.matvec(&x, &mut u, None);
23045        for v in g.iter_mut() {
23046            *v = inference::silu(*v);
23047        }
23048        keep_top_k(&mut g, k);
23049        for i in 0..inter {
23050            g[i] *= u[i];
23051        }
23052        let mut want = vec![0.0f32; hidden];
23053        d.down_proj.matvec(&g, &mut want, None);
23054        for (a, b) in want.iter().zip(&got) {
23055            assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
23056        }
23057    }
23058
23059    /// A tube layer is the same layer, re-cut. With every tube open the
23060    /// answer must equal the dense FFN over the concatenated neurons
23061    /// (the permutation is an identity on the layer's function); with a
23062    /// tube closed it must equal the dense FFN with those neurons
23063    /// zeroed — the mask semantics, now paid for in bytes not read.
23064    #[test]
23065    fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
23066        let (hidden, core, tube) = (8usize, 12usize, 8usize);
23067        let inter = core + tube;
23068        let synth = |n: usize, salt: usize| -> Vec<f32> {
23069            (0..n)
23070                .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
23071                .collect()
23072        };
23073        let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
23074        let d_all = synth(hidden * inter, 3);
23075        // The dense layer, and the same weights cut into core + tube.
23076        let dense = DenseFfn {
23077            gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
23078            up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
23079            down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
23080            act: Act::Silu,
23081            down_t: None,
23082            segs: Vec::new(),
23083        };
23084        let rows =
23085            |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
23086        let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
23087            let mut o = Vec::with_capacity(hidden * (b - a));
23088            for r in 0..hidden {
23089                o.extend_from_slice(&v[r * inter + a..r * inter + b]);
23090            }
23091            o
23092        };
23093        let tubed = DenseFfn {
23094            down_t: None,
23095            gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
23096            up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
23097            down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
23098            act: Act::Silu,
23099            segs: vec![FfnSeg {
23100                gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
23101                up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
23102                down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
23103                start: core,
23104                width: tube,
23105            }],
23106        };
23107        let x = synth(hidden, 7);
23108        let want = dense_ffn(&dense, &x, None);
23109        let got = tube_ffn(&tubed, &x, 1, None, None);
23110        for (a, b) in want.iter().zip(&got) {
23111            assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
23112        }
23113        // Closed tube: bits on for the core, off for the tube.
23114        let mut bits = vec![0u8; inter.div_ceil(8)];
23115        for n in 0..core {
23116            bits[n / 8] |= 1 << (n % 8);
23117        }
23118        let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23119        let masked = dense_ffn_masked(&dense, &x, None, &bits);
23120        for (a, b) in masked.iter().zip(&closed) {
23121            assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
23122        }
23123        // The batched arm must agree with the single-position one.
23124        let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23125        for (a, b) in closed.iter().zip(&batch) {
23126            assert_eq!(a, b, "batch arm disagrees with decode arm");
23127        }
23128    }
23129
23130    /// scale, structurally identical to the matvec kernels.
23131    #[test]
23132    fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
23133        let (hidden, inter) = (16usize, 40usize);
23134        let synth = |n: usize, salt: usize| -> Vec<f32> {
23135            (0..n)
23136                .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
23137                .collect()
23138        };
23139        let d = DenseFfn {
23140            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23141            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23142            down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
23143            act: Act::Silu,
23144            down_t: None,
23145            segs: Vec::new(),
23146        };
23147        let x = synth(hidden, 9);
23148        // Active = every 3rd neuron.
23149        let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
23150
23151        let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
23152
23153        // Reference: full dense FFN but g[i]=0 for inactive neurons.
23154        let mut g = vec![0.0f32; inter];
23155        d.gate_proj.matvec(&x, &mut g, None);
23156        let mut u = vec![0.0f32; inter];
23157        d.up_proj.matvec(&x, &mut u, None);
23158        let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
23159        for i in 0..inter {
23160            g[i] = if act_set.contains(&(i as u16)) {
23161                inference::silu(g[i]) * u[i]
23162            } else {
23163                0.0
23164            };
23165        }
23166        let mut reference = vec![0.0f32; hidden];
23167        d.down_proj.matvec(&g, &mut reference, None);
23168
23169        let max_d = sparse
23170            .iter()
23171            .zip(&reference)
23172            .map(|(a, b)| (a - b).abs())
23173            .fold(0.0f32, f32::max);
23174        assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
23175    }
23176
23177    /// Attach a synthetic MTP head. `inter` deliberately stays independent
23178    /// from the trunk width so calibration tests can catch a trunk-proxy bug.
23179    fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
23180        let (h, inter, heads, kv, hd) = (
23181            p.hidden_size,
23182            inter,
23183            p.num_heads,
23184            p.num_kv_heads,
23185            p.head_dim,
23186        );
23187        let synth = |n: usize, salt: usize| -> Vec<f32> {
23188            (0..n)
23189                .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
23190                .collect()
23191        };
23192        let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
23193            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23194        };
23195        p.mtp = Some(MtpModule {
23196            enorm: vec![1.0; h],
23197            hnorm: vec![1.0; h],
23198            eh_proj: qt(h, 2 * h, 301),
23199            layer: LayerWeights {
23200                input_norm: vec![1.0; h],
23201                post_norm: vec![1.0; h],
23202                attn_out_norm: None,
23203                ffn_out_norm: None,
23204                layer_scale: None,
23205                ffn: FfnKind::Dense(DenseFfn {
23206                    gate_proj: qt(inter, h, 315),
23207                    up_proj: qt(inter, h, 316),
23208                    down_proj: qt(h, inter, 317),
23209                    act: Act::Silu,
23210                    down_t: None,
23211                    segs: Vec::new(),
23212                }),
23213                attn: AttnKind::Full {
23214                    bias: None,
23215                    wq: qt(heads * hd, h, 311),
23216                    wk: qt(kv * hd, h, 312),
23217                    wv: qt(kv * hd, h, 313),
23218                    wo: qt(h, heads * hd, 314),
23219                    q_norm: None,
23220                    k_norm: None,
23221                    output_gate: false,
23222                    softplus_gate: None,
23223                },
23224            },
23225            final_norm: vec![1.0; h],
23226            kv: crate::kv_cache::LayerKvCache::new(kv, hd),
23227        });
23228    }
23229
23230    /// Attach a synthetic MTP head with the trunk's conventional width.
23231    fn attach_test_mtp(p: &mut Pipeline) {
23232        attach_test_mtp_width(p, p.intermediate_size);
23233    }
23234
23235    #[test]
23236    fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
23237        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
23238        // This differs from the trunk FFN width (16). A result shaped like
23239        // the trunk probe would be a silent, dangerous compaction bug.
23240        attach_test_mtp_width(&mut p, 11);
23241        let ids = [3, 9, 4, 12, 7, 2];
23242        let trunk = p.probe_ffn_mass(&ids);
23243        assert_eq!(trunk[0].len(), p.intermediate_size);
23244
23245        let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
23246        assert_eq!(got.len(), 1);
23247        assert_eq!(got[0].mass.len(), 11);
23248        assert_eq!(got[0].squared_mass.len(), 11);
23249        assert_eq!(got[0].observed_positions, ids.len() - 1);
23250        assert!(got[0].mass.iter().any(|&v| v > 0.0));
23251        assert!(got[0]
23252            .mass
23253            .iter()
23254            .zip(&got[0].squared_mass)
23255            .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23256        assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
23257        assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
23258
23259        // Force an error *after* the MTP attention has appended a draft KV
23260        // row. This proves the error path clears detached draft state, not
23261        // merely the easy invalid-token preflight branch.
23262        let h = p.hidden_size;
23263        let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
23264            panic!("synthetic MTP must be dense")
23265        };
23266        dense.segs.push(FfnSeg {
23267            gate: QTensor::from_f32(vec![0.0; h], 1, h),
23268            up: QTensor::from_f32(vec![0.0; h], 1, h),
23269            down: QTensor::from_f32(vec![0.0; h], h, 1),
23270            start: dense.gate_proj.rows(),
23271            width: 1,
23272        });
23273        let err = p.probe_mtp_ffn(&ids);
23274        assert!(err
23275            .unwrap_err()
23276            .contains("tube-segmented draft FFNs are not supported"));
23277        assert_eq!(p.kv_cache.seq_len(), 0);
23278        assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
23279    }
23280
23281    #[test]
23282    fn speculative_equals_vanilla_greedy() {
23283        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23284        // Exercise the experimental host pair deterministically; the native
23285        // and wgpu batch graphs have independent coverage.
23286        let _env = ScopedTestEnv::set(&[
23287            ("CMF_GPU", Some("0")),
23288            ("CMF_GPU_WGPU_GRAPH", Some("0")),
23289            ("CMF_MTP_PAIR", Some("1")),
23290        ]);
23291        let run = |spec: bool| {
23292            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23293            p.sampler_config.temperature = 0.0;
23294            attach_test_mtp(&mut p);
23295            p.speculative = spec;
23296            let r = p.generate("abcdef", 12, None, None).unwrap();
23297            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23298        };
23299        let (vanilla, d0, _) = run(false);
23300        let (spec, d1, a1) = run(true);
23301        assert_eq!(d0, 0, "vanilla path must not draft");
23302        assert!(d1 > 0, "speculative path must draft");
23303        assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
23304        assert_eq!(
23305            vanilla, spec,
23306            "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
23307        );
23308    }
23309
23310    #[test]
23311    fn speculative_accepts_constant_oracle() {
23312        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23313        let _env = ScopedTestEnv::set(&[
23314            ("CMF_GPU", Some("0")),
23315            ("CMF_GPU_WGPU_GRAPH", Some("0")),
23316            ("CMF_MTP_PAIR", Some("1")),
23317        ]);
23318        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23319        p.sampler_config.temperature = 0.0;
23320        p.sampler_config.repetition_penalty = 1.0;
23321        // Constant lm_head → every logit equal → both the main model and
23322        // the draft head argmax to token 0: acceptance must be 100%.
23323        p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
23324        attach_test_mtp(&mut p);
23325        p.speculative = true;
23326        let r = p.generate("abcd", 10, None, None).unwrap();
23327        assert!(r.mtp_drafted > 0);
23328        assert_eq!(
23329            r.mtp_accepted, r.mtp_drafted,
23330            "constant logits → every draft accepted"
23331        );
23332        // Ties resolve to the same token in both the main and draft
23333        // heads — the sequence is one repeated token.
23334        assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
23335    }
23336
23337    #[test]
23338    fn host_fused_mtp_pair_is_opt_in() {
23339        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23340        let _env = ScopedTestEnv::set(&[
23341            ("CMF_GPU", Some("0")),
23342            ("CMF_GPU_WGPU_GRAPH", Some("0")),
23343            ("CMF_MTP_PAIR", None),
23344        ]);
23345        let run = |spec: bool| {
23346            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23347            p.sampler_config.temperature = 0.0;
23348            attach_test_mtp(&mut p);
23349            p.speculative = spec;
23350            let r = p.generate("abcdef", 12, None, None).unwrap();
23351            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23352        };
23353        let (plain, d0, _) = run(false);
23354        let (guarded, d1, a1) = run(true);
23355        assert_eq!(d0, 0);
23356        assert_eq!(d1, 0, "host MTP pair must remain opt-in");
23357        assert_eq!(a1, 0);
23358        assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
23359    }
23360
23361    #[test]
23362    fn empty_prompt_is_an_error_not_a_panic() {
23363        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23364        let r = p.generate("", 4, None, None);
23365        assert!(r.is_err(), "empty prompt must be a clean error");
23366    }
23367
23368    #[test]
23369    fn every_token_enters_kv_exactly_once() {
23370        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23371        // Greedy so no RNG variance; byte tokenizer → 3 prompt tokens.
23372        p.sampler_config.temperature = 0.0;
23373        let r = p.generate("abc", 2, None, None).unwrap();
23374        assert_eq!(r.prompt_tokens, 3);
23375        // prompt(3) + first sampled token forwarded before second logits:
23376        // step0 samples from prefill hidden (no extra forward), then
23377        // forwards t1 → cache 4; step1 samples, loop ends (max_tokens).
23378        assert_eq!(
23379            p.kv_cache.seq_len(),
23380            3 + r.tokens_generated - 1,
23381            "each token must be cached exactly once (v1 cached the last prompt token twice)"
23382        );
23383    }
23384
23385    #[test]
23386    fn generation_is_reproducible_with_seed() {
23387        let run = || {
23388            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23389            p.generate("hello", 8, None, None).unwrap().token_ids
23390        };
23391        assert_eq!(run(), run());
23392    }
23393
23394    #[test]
23395    fn resetting_sampler_restarts_the_seeded_stream() {
23396        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23397        let config = SamplerConfig {
23398            seed: Some(1234),
23399            ..SamplerConfig::default()
23400        };
23401        p.set_sampler_config(config.clone());
23402        let first = p.generate("hello", 8, None, None).unwrap().token_ids;
23403        p.set_sampler_config(config);
23404        let second = p.generate("hello", 8, None, None).unwrap().token_ids;
23405        assert_eq!(first, second);
23406    }
23407
23408    #[test]
23409    fn eviction_bounds_the_cache() {
23410        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23411        p.kv_cache.max_seq_len = 6;
23412        p.sampler_config.temperature = 0.0;
23413        let _ = p.generate("abcd", 12, None, None).unwrap();
23414        assert!(
23415            p.kv_cache.seq_len() <= 6 + 1,
23416            "cache must stay bounded by max_seq_len (got {})",
23417            p.kv_cache.seq_len()
23418        );
23419    }
23420
23421    #[test]
23422    fn confidence_matches_tokens_and_is_a_probability() {
23423        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23424        p.sampler_config.temperature = 0.0;
23425        p.sampler_config.repetition_penalty = 1.0;
23426        let r = p.generate("abcd", 10, None, None).unwrap();
23427        assert_eq!(
23428            r.token_confidence.len(),
23429            r.token_ids.len(),
23430            "one confidence per emitted token"
23431        );
23432        for &c in &r.token_confidence {
23433            assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
23434        }
23435        // top1_prob is a valid softmax probability.
23436        let logits = [1.0f32, 3.0, 0.5, 3.0];
23437        let p0 = top1_prob_t(&logits, 1, 1.0);
23438        let p1 = top1_prob_t(&logits, 3, 1.0);
23439        assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
23440        assert!(p0 > 0.0 && p0 < 1.0);
23441        // Calibration temperature > 1 softens an over-confident peak.
23442        let sharp = top1_prob_t(&logits, 1, 1.0);
23443        let soft = top1_prob_t(&logits, 1, 2.0);
23444        assert!(soft < sharp, "higher temperature lowers peak confidence");
23445    }
23446
23447    #[test]
23448    fn trace_is_opt_in_and_parallels_the_output() {
23449        // Off by default: the runtime is silent unless observation asked.
23450        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23451        p.sampler_config.temperature = 0.0;
23452        p.sampler_config.repetition_penalty = 1.0;
23453        let r = p.generate("abcd", 10, None, None).unwrap();
23454        assert!(r.traces.is_empty(), "trace must be empty unless enabled");
23455
23456        // On: exactly one row per emitted token, aligned with the output.
23457        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23458        p.sampler_config.temperature = 0.0;
23459        p.sampler_config.repetition_penalty = 1.0;
23460        p.set_trace(true);
23461        let r = p.generate("abcd", 10, None, None).unwrap();
23462        assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
23463        for (i, tr) in r.traces.iter().enumerate() {
23464            assert_eq!(tr.t, i, "trace index is sequential");
23465            assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
23466            assert_eq!(
23467                tr.confidence, r.token_confidence[i],
23468                "trace confidence matches the confidence channel"
23469            );
23470            // No dynamic router in this pipeline → no skill, no coherence.
23471            assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
23472        }
23473    }
23474
23475    #[test]
23476    fn explain_prefill_logits_match_greedy_first_token() {
23477        // `cortiq explain` shows the next-token distribution from
23478        // prefill_next_logits; its argmax must equal what greedy generate
23479        // actually emits first — otherwise explain would lie.
23480        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23481        p.sampler_config.temperature = 0.0;
23482        p.sampler_config.repetition_penalty = 1.0;
23483        let ids = p.tokenizer.encode("abcd");
23484        let logits = p.prefill_next_logits(&ids, None);
23485        let argmax = logits
23486            .iter()
23487            .enumerate()
23488            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
23489            .unwrap()
23490            .0 as u32;
23491        let r = p.generate("abcd", 1, None, None).unwrap();
23492        assert_eq!(
23493            argmax, r.token_ids[0],
23494            "explain preview must match greedy emit"
23495        );
23496    }
23497
23498    #[test]
23499    fn laguna_shared_expert_is_unconditionally_added() {
23500        let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
23501        let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
23502        let zero_dense = || DenseFfn {
23503            gate_proj: matrix(vec![0.0; 4]),
23504            up_proj: matrix(vec![0.0; 4]),
23505            down_proj: matrix(vec![0.0; 4]),
23506            act: Act::Silu,
23507            down_t: None,
23508            segs: Vec::new(),
23509        };
23510        let shared = DenseFfn {
23511            gate_proj: identity(),
23512            up_proj: identity(),
23513            down_proj: identity(),
23514            act: Act::Silu,
23515            down_t: None,
23516            segs: Vec::new(),
23517        };
23518        let x = [1.0, 2.0];
23519        let expected = dense_ffn(&shared, &x, None);
23520        let moe = MoeFfn {
23521            router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
23522            experts: vec![zero_dense()],
23523            top_k: 1,
23524            norm_topk_prob: true,
23525            router_sigmoid: true,
23526            sigmoid_denom_eps: 1e-6,
23527            expert_bias: None,
23528            routed_scaling: 1.0,
23529            route_tau: None,
23530            shared: Some((shared, None)),
23531            stats: std::cell::RefCell::new(Vec::new()),
23532            act_sq: std::cell::RefCell::new(Vec::new()),
23533            act_rows: std::cell::RefCell::new(Vec::new()),
23534            mask: None,
23535            per_expert_scale: None,
23536            router_input_norm: false,
23537            resonance: None,
23538            grown: Vec::new(),
23539        };
23540        let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
23541        for (actual, expected) in actual.iter().zip(expected) {
23542            assert!((actual - expected).abs() < 1e-6);
23543        }
23544    }
23545
23546    /// A tiny MiMo-V2-shaped stack (the M3 fixture): layers [full, sliding,
23547    /// sliding, full]; 4 Q heads over 1 (full) / 2 (sliding) KV heads;
23548    /// head_dim 8 with 4-wide V heads; partial rotary 4 at θ 1e7 (full) /
23549    /// 1e4 (sliding); window 3; learned sinks on the sliding layers; layer
23550    /// 0 a dense FFN, layers 1..3 sigmoid-routed MoE with a selection bias
23551    /// (4 experts, top-2, renormalized, no shared expert). Geometry and
23552    /// sinks go through the same `set_attn_geometry` / `set_layer_sinks`
23553    /// the loader calls.
23554    fn mimo_test_pipeline() -> Pipeline {
23555        let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
23556        let kvh = [1usize, 2, 2, 1];
23557        let synth = |n: usize, salt: usize| -> Vec<f32> {
23558            (0..n)
23559                .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23560                .collect()
23561        };
23562        let qt = |rows: usize, cols: usize, salt: usize| {
23563            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23564        };
23565        let dense = |inter: usize, salt: usize| DenseFfn {
23566            gate_proj: qt(inter, hs, salt),
23567            up_proj: qt(inter, hs, salt + 1),
23568            down_proj: qt(hs, inter, salt + 2),
23569            act: Act::Silu,
23570            down_t: None,
23571            segs: Vec::new(),
23572        };
23573        let layers: Vec<LayerWeights> = (0..4)
23574            .map(|li| LayerWeights {
23575                input_norm: vec![1.0; hs],
23576                post_norm: vec![1.0; hs],
23577                attn_out_norm: None,
23578                ffn_out_norm: None,
23579                layer_scale: None,
23580                ffn: if li == 0 {
23581                    FfnKind::Dense(dense(inter, 50))
23582                } else {
23583                    FfnKind::Moe(MoeFfn {
23584                        router: qt(4, hs, 60 + li),
23585                        experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
23586                        top_k: 2,
23587                        norm_topk_prob: true,
23588                        router_sigmoid: true,
23589                        sigmoid_denom_eps: 1e-6,
23590                        expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
23591                        routed_scaling: 1.0,
23592                        route_tau: None,
23593                        shared: None,
23594                        stats: std::cell::RefCell::new(Vec::new()),
23595                        act_sq: std::cell::RefCell::new(Vec::new()),
23596                        act_rows: std::cell::RefCell::new(Vec::new()),
23597                        mask: None,
23598                        per_expert_scale: None,
23599                        router_input_norm: false,
23600                        resonance: None,
23601                        grown: Vec::new(),
23602                    })
23603                },
23604                attn: AttnKind::Full {
23605                    wq: qt(nh * hd, hs, li * 10 + 1),
23606                    wk: qt(kvh[li] * hd, hs, li * 10 + 2),
23607                    wv: qt(kvh[li] * vd, hs, li * 10 + 3),
23608                    wo: qt(hs, nh * vd, li * 10 + 4),
23609                    q_norm: None,
23610                    k_norm: None,
23611                    output_gate: false,
23612                    softplus_gate: None,
23613                    bias: None,
23614                },
23615            })
23616            .collect();
23617        let mut p = Pipeline::new(
23618            Tokenizer::byte_level(),
23619            PipelineWeights {
23620                embed_tokens: qt(vocab, hs, 100),
23621                layers,
23622                lm_head: qt(vocab, hs, 200),
23623                final_norm: vec![1.0; hs],
23624            },
23625            hs,
23626            inter,
23627            nh,
23628            1, // header num_kv_heads (the full layers')
23629            hd,
23630            4,
23631            4,
23632            false,
23633            vocab,
23634            1e-6,
23635            1e7,
23636            NormStyle::Qwen,
23637            4096,
23638            SamplerConfig {
23639                seed: Some(7),
23640                ..Default::default()
23641            },
23642        );
23643        // Diagnostics stay off whatever the test environment exports.
23644        p.layer_dump = None;
23645        p.set_rotary(4, 1e7);
23646        p.sliding_layers = Some(vec![false, true, true, false]);
23647        p.swa = Some((3, usize::MAX));
23648        p.rotary_dim_local = Some(4);
23649        p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
23650        p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
23651        p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
23652        p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
23653        p
23654    }
23655
23656    #[test]
23657    fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
23658        let mut p = mimo_test_pipeline();
23659        p.speculative = false;
23660        p.ignore_eos = true;
23661        p.sampler_config.temperature = 0.0;
23662        p.sampler_config.repetition_penalty = 1.0;
23663        let a = vec![3, 5, 7, 9, 11, 13];
23664        let b = vec![4, 8, 12, 16, 20, 24];
23665        let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
23666        let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
23667        // Same placeholder IDs as an earlier request are not a cache key
23668        // for different media. The actual rows, not a re-embedding of a,
23669        // must determine the continuation.
23670        let actual = p
23671            .generate_from_embeds(&a, &rows, 8, None, None)
23672            .unwrap()
23673            .token_ids;
23674        assert_eq!(actual, expected);
23675        assert!(p.kv_history.is_empty());
23676        let mut extended = a.clone();
23677        extended.push(17);
23678        let after_media = p
23679            .generate_from_ids(&extended, 8, None, None)
23680            .unwrap()
23681            .token_ids;
23682        p.reset_session();
23683        let fresh = p
23684            .generate_from_ids(&extended, 8, None, None)
23685            .unwrap()
23686            .token_ids;
23687        assert_eq!(after_media, fresh);
23688        assert!(
23689            p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
23690                .is_err()
23691        );
23692        // Force a real token-prefix reuse opportunity into the media call.
23693        // Those labels are unchanged, but their embeddings now describe a
23694        // different source sequence and every KV row must be rebuilt.
23695        p.reset_session();
23696        p.generate_from_ids(&a, 1, None, None).unwrap();
23697        let mut media_ids = p.kv_history.clone();
23698        assert!(!media_ids.is_empty());
23699        media_ids.extend_from_slice(&[19, 21, 23]);
23700        let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
23701        let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
23702        let mut oracle = mimo_test_pipeline();
23703        oracle.speculative = false;
23704        oracle.ignore_eos = true;
23705        oracle.sampler_config.temperature = 0.0;
23706        oracle.sampler_config.repetition_penalty = 1.0;
23707        let expected = oracle
23708            .generate_from_ids(&source_ids, 8, None, None)
23709            .unwrap()
23710            .token_ids;
23711        assert_eq!(
23712            p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
23713                .unwrap()
23714                .token_ids,
23715            expected
23716        );
23717        assert!(p.kv_history.is_empty());
23718        let mut bad = rows;
23719        bad[0] = f32::NAN;
23720        assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
23721    }
23722
23723    fn f32_bits(v: &[f32]) -> Vec<u32> {
23724        v.iter().map(|x| x.to_bits()).collect()
23725    }
23726
23727    /// M3 acceptance: on the MiMo-shaped stack the decode walk (one
23728    /// position at a time through `forward_layers`) and the batched
23729    /// prefill (`prefill_batch_span`, whole prompt and split in two
23730    /// chunks) give bit-identical logits at all 12 positions — per-layer
23731    /// KV heads, narrow V, sinks, the window and the biased sigmoid MoE all
23732    /// agree across the two walks.
23733    #[test]
23734    fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
23735        let mut p = mimo_test_pipeline();
23736        let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
23737        assert_eq!(kv, vec![1, 2, 2, 1]);
23738        assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
23739        assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
23740        let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
23741        let hs = p.hidden_size;
23742        let mut decode = Vec::new();
23743        for (pos, &id) in ids.iter().enumerate() {
23744            let e = p.embed_single(id);
23745            let h = p.forward_layers(&e, pos, None);
23746            decode.push(p.logits_from_hidden(&h));
23747        }
23748        for l in &p.kv_cache.layers {
23749            assert_eq!(l.seq_len, 12);
23750            // V rows are padded to head_dim inside the cache.
23751            assert_eq!(l.head_values(0).len(), 12 * 8);
23752        }
23753        assert!(decode.iter().flatten().all(|v| v.is_finite()));
23754
23755        p.clear_sequence_state();
23756        let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23757        for pos in 0..ids.len() {
23758            let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
23759            assert_eq!(
23760                f32_bits(&decode[pos]),
23761                f32_bits(&lg),
23762                "whole prompt, pos {pos}"
23763            );
23764        }
23765
23766        p.clear_sequence_state();
23767        let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
23768        let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
23769        for pos in 0..ids.len() {
23770            let row = if pos < 5 {
23771                &a[pos * hs..(pos + 1) * hs]
23772            } else {
23773                &b[(pos - 5) * hs..(pos - 4) * hs]
23774            };
23775            let lg = p.logits_from_hidden(row);
23776            assert_eq!(
23777                f32_bits(&decode[pos]),
23778                f32_bits(&lg),
23779                "two chunks, pos {pos}"
23780            );
23781        }
23782
23783        // The fixture is not degenerate: the sinks and the window each
23784        // change the answer.
23785        let last = |p: &mut Pipeline| {
23786            p.clear_sequence_state();
23787            let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23788            p.logits_from_hidden(&hb[11 * hs..12 * hs])
23789        };
23790        let base = last(&mut p);
23791        let mut no_sinks = mimo_test_pipeline();
23792        for l in &mut no_sinks.kv_cache.layers {
23793            l.sinks = None;
23794        }
23795        assert_ne!(
23796            f32_bits(&last(&mut no_sinks)),
23797            f32_bits(&base),
23798            "sinks are live"
23799        );
23800        let mut wide = mimo_test_pipeline();
23801        wide.swa = Some((64, usize::MAX));
23802        assert_ne!(
23803            f32_bits(&last(&mut wide)),
23804            f32_bits(&base),
23805            "window is live"
23806        );
23807
23808        // Generation runs end to end on the same stack.
23809        p.clear_sequence_state();
23810        p.ignore_eos = true;
23811        let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
23812        assert_eq!(r.token_ids.len(), 4);
23813    }
23814
23815    /// A Spark-X2.5-shaped stack: layers 0..3 slide (window 6), layer 3
23816    /// is global, every attention carries the head-wise sigmoid g_proj gate.
23817    fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
23818        let (hs, nh) = (16usize, 4usize);
23819        let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
23820        p.layer_dump = None;
23821        p.swa = Some((6, 4));
23822        p.proj_gate_sigmoid = true;
23823        for (li, lw) in p.weights.layers.iter_mut().enumerate() {
23824            if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
23825                let g: Vec<f32> = (0..nh * hs)
23826                    .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
23827                    .collect();
23828                *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
23829            }
23830        }
23831        p.swa_trim = trim;
23832        p
23833    }
23834
23835    /// Trimming the sliding tails changes nothing but memory: the same
23836    /// prompt in uneven chunks, a decode walk, and fused pairs with a
23837    /// 2-row rollback give bit-identical hiddens with and without it,
23838    /// while the trimmed sliding layers stay bounded and the global layer
23839    /// keeps every row.
23840    #[test]
23841    fn swa_trim_pipeline_matches_untrimmed_bitwise() {
23842        let mut a = spark_test_pipeline(None);
23843        let mut b = spark_test_pipeline(Some((2, 4)));
23844        assert_eq!(
23845            (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
23846            vec![Some(6), Some(6), Some(6), None]
23847        );
23848        let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
23849        let mut pos = 0usize;
23850        for &n in [5usize, 13, 7, 16].iter().cycle() {
23851            if pos >= ids.len() {
23852                break;
23853            }
23854            let end = (pos + n).min(ids.len());
23855            let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
23856            let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
23857            assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
23858            pos = end;
23859        }
23860        for _ in 0..23 {
23861            let e = a.embed_single(((pos * 7) % 64) as u32);
23862            let ha = a.forward_layers(&e, pos, None);
23863            let hb = b.forward_layers(&e, pos, None);
23864            assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
23865            for li in 0..3 {
23866                assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
23867            }
23868            pos += 1;
23869        }
23870        for round in 0..9 {
23871            let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
23872            let (a1, a2) = a.forward_pair(&e1, &e2, pos);
23873            let (b1, b2) = b.forward_pair(&e1, &e2, pos);
23874            assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
23875            assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
23876            if round % 2 == 1 {
23877                // A rejected draft: both lanes roll back.
23878                for p in [&mut a, &mut b] {
23879                    for l in &mut p.kv_cache.layers {
23880                        l.truncate_last(2);
23881                    }
23882                }
23883            } else {
23884                pos += 2;
23885            }
23886        }
23887        let e = a.embed_single(9);
23888        let (ha, hb) = (
23889            a.forward_layers(&e, pos, None),
23890            b.forward_layers(&e, pos, None),
23891        );
23892        assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
23893        for li in 0..3 {
23894            let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
23895            assert!(lb.base() > 0, "layer {li} trimmed");
23896            assert_eq!(la.base(), 0);
23897            assert_eq!(lb.pos_len(), la.seq_len);
23898            assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
23899        }
23900        let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
23901        assert_eq!(
23902            (gb.base(), gb.seq_len),
23903            (0, ga.seq_len),
23904            "the global layer keeps all"
23905        );
23906        assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
23907        assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
23908    }
23909
23910    /// The network split's KvFetch hand-off on a trimmed stack: every
23911    /// layer shipped over the wire mid-sequence (the sliding ones as
23912    /// `FullTail`) into a fresh pipeline that never ran a position, which
23913    /// then decodes on — bit for bit what the untrimmed pipeline decodes.
23914    #[test]
23915    fn swa_trim_wire_handoff_continues_bitwise() {
23916        let mut a = spark_test_pipeline(None);
23917        let mut b = spark_test_pipeline(Some((2, 4)));
23918        let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
23919        let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
23920        let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
23921        assert_eq!(f32_bits(&ha), f32_bits(&hb));
23922        let mut pos = ids.len();
23923        for _ in 0..5 {
23924            let e = a.embed_single(((pos * 5) % 64) as u32);
23925            assert_eq!(
23926                f32_bits(&a.forward_layers(&e, pos, None)),
23927                f32_bits(&b.forward_layers(&e, pos, None))
23928            );
23929            pos += 1;
23930        }
23931        let mut c = spark_test_pipeline(Some((2, 4)));
23932        for li in 0..4 {
23933            let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
23934            c.kv_cache.layers[li].import_wire(&bytes).unwrap();
23935        }
23936        assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
23937        assert_eq!(c.kv_cache.seq_len(), pos);
23938        for step in 0..17 {
23939            let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
23940            let ha = a.forward_layers(&e, pos, None);
23941            let hc = c.forward_layers(&e, pos, None);
23942            assert_eq!(
23943                f32_bits(&ha),
23944                f32_bits(&hc),
23945                "after the hand-off, step {step}"
23946            );
23947            pos += 1;
23948        }
23949        assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
23950    }
23951
23952    /// A synthetic MiMo draft stack for `mimo_test_pipeline` (the SWA
23953    /// geometry of its sliding layers: 2 KV heads, head 8 / V 4).  Widths
23954    /// are per-draft-layer so the MTP collector cannot accidentally borrow
23955    /// the trunk's `intermediate_size`.
23956    fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
23957        let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
23958        let synth = |len: usize, salt: usize| -> Vec<f32> {
23959            (0..len)
23960                .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
23961                .collect()
23962        };
23963        let qt = |rows: usize, cols: usize, salt: usize| {
23964            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23965        };
23966        let layers = widths
23967            .iter()
23968            .copied()
23969            .enumerate()
23970            .map(|(k, inter)| {
23971                let s = 500 + k * 40;
23972                let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
23973                kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
23974                MtpModule {
23975                    enorm: vec![1.0; hs],
23976                    hnorm: vec![1.0; hs],
23977                    eh_proj: qt(hs, 2 * hs, s),
23978                    layer: LayerWeights {
23979                        input_norm: vec![1.0; hs],
23980                        post_norm: vec![1.0; hs],
23981                        attn_out_norm: None,
23982                        ffn_out_norm: None,
23983                        layer_scale: None,
23984                        attn: AttnKind::Full {
23985                            wq: qt(nh * hd, hs, s + 1),
23986                            wk: qt(nkv * hd, hs, s + 2),
23987                            wv: qt(nkv * vd, hs, s + 3),
23988                            wo: qt(hs, nh * vd, s + 4),
23989                            q_norm: None,
23990                            k_norm: None,
23991                            output_gate: false,
23992                            softplus_gate: None,
23993                            bias: None,
23994                        },
23995                        ffn: FfnKind::Dense(DenseFfn {
23996                            gate_proj: qt(inter, hs, s + 5),
23997                            up_proj: qt(inter, hs, s + 6),
23998                            down_proj: qt(hs, inter, s + 7),
23999                            act: Act::Silu,
24000                            down_t: None,
24001                            segs: Vec::new(),
24002                        }),
24003                    },
24004                    final_norm: vec![1.0; hs],
24005                    kv,
24006                }
24007            })
24008            .collect();
24009        mimo_mtp::MimoMtp::from_layers(layers)
24010    }
24011
24012    fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
24013        mimo_test_mtp_widths(&vec![24; n], gain)
24014    }
24015
24016    fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
24017        p.clear_sequence_state();
24018        p.speculative = spec;
24019        p.ignore_eos = true;
24020        p.sampler_config.temperature = 0.0;
24021        p.generate_from_ids(ids, n, None, None).unwrap()
24022    }
24023
24024    #[test]
24025    fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
24026        let mut p = mimo_test_pipeline();
24027        let widths = [7usize, 13, 29];
24028        assert_ne!(widths[0], p.intermediate_size);
24029        assert_ne!(widths[1], p.intermediate_size);
24030        assert_ne!(widths[2], p.intermediate_size);
24031        p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
24032        let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
24033
24034        let trunk = p.probe_ffn_mass(&ids);
24035        assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
24036        let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
24037        assert_eq!(got.len(), widths.len());
24038        for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
24039            assert_eq!(stat.mass.len(), width, "draft layer {k}");
24040            assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
24041            assert_eq!(stat.observed_positions, ids.len() - k - 1);
24042            assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
24043            assert!(stat
24044                .mass
24045                .iter()
24046                .zip(&stat.squared_mass)
24047                .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
24048        }
24049        assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
24050        let st = p.mimo_mtp.as_ref().unwrap();
24051        assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
24052
24053        // As above, reject only after a MiMo draft KV has been appended.
24054        // The detached multi-layer owner must still be reset on that error.
24055        let h = p.hidden_size;
24056        let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
24057            panic!("synthetic MiMo MTP must be dense")
24058        };
24059        dense.segs.push(FfnSeg {
24060            gate: QTensor::from_f32(vec![0.0; h], 1, h),
24061            up: QTensor::from_f32(vec![0.0; h], 1, h),
24062            down: QTensor::from_f32(vec![0.0; h], h, 1),
24063            start: dense.gate_proj.rows(),
24064            width: 1,
24065        });
24066        assert!(p
24067            .probe_mtp_ffn(&ids)
24068            .unwrap_err()
24069            .contains("tube-segmented draft FFNs are not supported"));
24070        assert_eq!(p.kv_cache.seq_len(), 0);
24071        assert!(p
24072            .mimo_mtp
24073            .as_ref()
24074            .unwrap()
24075            .layers
24076            .iter()
24077            .all(|m| m.kv.seq_len == 0));
24078    }
24079
24080    #[test]
24081    fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
24082        fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
24083            p.clear_sequence_state();
24084            let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
24085            let _exact = ExactMtpCalibrationGuard::enter();
24086            let got = crate::gpu::cpu_scope(|| {
24087                let trunk = p.probe_trunk_rows(ids)?;
24088                p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
24089            })
24090            .expect("teacher-forced MiMo probe");
24091            st.reset();
24092            p.mimo_mtp = Some(st);
24093            p.reset_mtp_probe_state();
24094            got
24095        }
24096
24097        for post_norm_hidden in [false, true] {
24098            let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
24099            let make = || {
24100                let mut p = mimo_test_pipeline();
24101                let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
24102                st.post_norm_hidden = post_norm_hidden;
24103                p.mimo_mtp = Some(st);
24104                p
24105            };
24106            let mut full = make();
24107            let expected = collect(&mut full, &ids, usize::MAX);
24108            for rows in [1usize, 7, 128] {
24109                let mut chunked = make();
24110                let got = collect(&mut chunked, &ids, rows);
24111                assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
24112                for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
24113                    assert_eq!(
24114                        got.observed_positions, expected.observed_positions,
24115                        "post={post_norm_hidden}, rows={rows}, layer={layer}"
24116                    );
24117                    assert_eq!(
24118                        got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24119                        expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24120                        "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
24121                    );
24122                    assert_eq!(
24123                        got.squared_mass
24124                            .iter()
24125                            .map(|v| v.to_bits())
24126                            .collect::<Vec<_>>(),
24127                        expected
24128                            .squared_mass
24129                            .iter()
24130                            .map(|v| v.to_bits())
24131                            .collect::<Vec<_>>(),
24132                        "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
24133                    );
24134                }
24135            }
24136        }
24137    }
24138
24139    /// The draft stack's incremental rounds (a few rows per layer, last
24140    /// round's provisional rows dropped) give exactly the teacher-forced
24141    /// table of one causal pass per layer over the whole sequence — the
24142    /// table `tools/mimo_ref.py mtp` computes for variant A: layer k, row
24143    /// j reads (x[j+k+1], norm(h_j)) at RoPE position j.
24144    #[test]
24145    fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
24146        // Both readings of the backbone hidden: pre-final-norm (default)
24147        // and post-final-norm (`CMF_MIMO_MTP_HIDDEN=post`).
24148        for post in [false, true] {
24149            let mut p = mimo_test_pipeline();
24150            // A non-trivial final norm, so the two readings differ.
24151            p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
24152            let mut st0 = mimo_test_mtp(3, 1.0);
24153            st0.post_norm_hidden = post;
24154            p.mimo_mtp = Some(st0);
24155            let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
24156            let hs = p.hidden_size;
24157            let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24158            p.mimo_note_rows(&hb, 0);
24159            let mut st = p.mimo_mtp.take().unwrap();
24160            // Incremental: one round per t through the decode path (later
24161            // tokens from `ids`, the probe's teacher forcing).
24162            let k = 3;
24163            let mut inc = Vec::new();
24164            for t in 0..ids.len() - k - 1 {
24165                inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
24166            }
24167            // Reference: per layer, ONE batched causal pass over all rows
24168            // with fresh caches.
24169            let s = ids.len();
24170            let mut reference = vec![vec![0u32; k]; s - k - 1];
24171            let mut fresh = mimo_test_mtp(3, 1.0);
24172            for (layer, m) in fresh.layers.iter_mut().enumerate() {
24173                let n = s - layer - 1;
24174                let mut cats = vec![0.0f32; n * 2 * hs];
24175                for j in 0..n {
24176                    let e = p.embed_single(ids[j + layer + 1]);
24177                    let raw = &hb[j * hs..(j + 1) * hs];
24178                    let g = if post {
24179                        inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
24180                    } else {
24181                        raw.to_vec()
24182                    };
24183                    let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
24184                    inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
24185                    inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
24186                }
24187                let mut x = vec![0.0f32; n * hs];
24188                m.eh_proj.matmat(&cats, n, &mut x, None);
24189                p.mimo_mtp_block(m, &mut x, n, 0);
24190                for (t, row) in reference.iter_mut().enumerate() {
24191                    let y = inference::rms_norm(
24192                        &x[t * hs..(t + 1) * hs],
24193                        &m.final_norm,
24194                        p.rms_eps,
24195                        p.norm_style,
24196                    );
24197                    row[layer] = sampler::argmax(&p.lm_head_forward(&y));
24198                }
24199            }
24200            assert_eq!(inc, reference, "post_norm_hidden = {post}");
24201            // Not a degenerate table: the drafts vary.
24202            let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
24203            assert!(distinct.len() > 3, "{inc:?}");
24204            // Each layer's cache ends holding rows up to the last round start.
24205            let last_t = ids.len() - k - 2;
24206            for m in &st.layers {
24207                assert_eq!(m.kv.seq_len, last_t + 1);
24208            }
24209        }
24210    }
24211
24212    /// Greedy with the MiMo draft stack is the plain greedy stream, token
24213    /// for token — with the real draft layers (low acceptance) and with a
24214    /// drafter that is right most of the time (exercises accepted prefixes
24215    /// of every length, the KV truncation of the rejected rows and the
24216    /// logits hand-off to the loop top), under the default repetition
24217    /// penalty.
24218    #[test]
24219    fn mimo_speculative_greedy_equals_plain_greedy() {
24220        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
24221        let _env = ScopedTestEnv::set(&[
24222            ("CMF_GPU", Some("0")),
24223            ("CMF_GPU_WGPU_GRAPH", Some("0")),
24224        ]);
24225        let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
24226        let n = 24;
24227        let mut p = mimo_test_pipeline();
24228        let plain = mimo_greedy(&mut p, &ids, n, false);
24229        assert_eq!(plain.mtp_drafted, 0);
24230        assert_eq!(plain.token_ids.len(), n);
24231        let plain_kv = p.kv_cache.layers[0].seq_len;
24232
24233        // Real draft layers.
24234        p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
24235        let spec = mimo_greedy(&mut p, &ids, n, true);
24236        assert!(spec.mtp_drafted > 0, "the round must draft");
24237        assert_eq!(spec.token_ids, plain.token_ids);
24238        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24239
24240        // A drafter reading the true continuation with every fifth token
24241        // wrong: accepted prefixes of 0..=3 all occur.
24242        let mut truth: Vec<u32> = ids.clone();
24243        truth.extend(&plain.token_ids);
24244        let mut noisy = truth.clone();
24245        for (i, t) in noisy.iter_mut().enumerate() {
24246            if i % 5 == 0 {
24247                *t = (*t + 1) % 64;
24248            }
24249        }
24250        let mut st = mimo_test_mtp(3, 1.0);
24251        st.draft_override = Some(noisy);
24252        p.mimo_mtp = Some(st);
24253        let spec = mimo_greedy(&mut p, &ids, n, true);
24254        assert_eq!(spec.token_ids, plain.token_ids);
24255        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24256        let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
24257        assert_eq!(stats.accepted as usize, spec.mtp_accepted);
24258        assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
24259        assert!(
24260            stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
24261            "{:?}",
24262            stats.accept_hist
24263        );
24264        assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
24265
24266        // A perfect drafter: every draft accepted, rounds of K+1 tokens,
24267        // and the budget is never overrun.
24268        let mut st = mimo_test_mtp(3, 1.0);
24269        st.draft_override = Some(truth);
24270        p.mimo_mtp = Some(st);
24271        let spec = mimo_greedy(&mut p, &ids, n, true);
24272        assert_eq!(spec.token_ids, plain.token_ids);
24273        assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
24274        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24275
24276        // CMF_MTP=0 path: the stack is attached but idle.
24277        let off = mimo_greedy(&mut p, &ids, n, false);
24278        assert_eq!(off.token_ids, plain.token_ids);
24279        assert_eq!(off.mtp_drafted, 0);
24280    }
24281
24282    /// The wgpu graphs carry MiMo-V2's attention per layer (KV heads,
24283    /// narrow V, sinks, windows, two RoPE tables): no attention-level
24284    /// decline for it any more, and the geometry each layer hands the
24285    /// graph is exactly what the CPU attention reads for that layer. The
24286    /// descriptive reasons stay (the Metal graphs and the q1 dropin still
24287    /// decline on them), and what the per-layer geometry cannot express
24288    /// keeps a named wgpu decline.
24289
24290    #[test]
24291    fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
24292        let p = mimo_test_pipeline();
24293        assert_eq!(
24294            p.graph_attn_decline_reason(),
24295            Some("per-layer KV head counts")
24296        );
24297        assert_eq!(p.wgpu_graph_attn_decline(), None);
24298        let g0 = p.graph_attn_geom(0).expect("full layer geometry");
24299        assert_eq!(
24300            (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
24301            (1, 4, 4, None, false)
24302        );
24303        assert_eq!(g0.invf, p.inv_freq.as_slice());
24304        let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
24305        assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
24306        assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
24307        assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
24308        assert_ne!(g0.invf, g1.invf, "two RoPE tables");
24309        let g3 = p.graph_attn_geom(3).expect("full layer geometry");
24310        assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
24311
24312        // No wgpu device in this process: the builders run and decline on
24313        // the (f32, unmapped) experts — never with an attention line.
24314        let emb = p.embed_single(3);
24315        let mut lg = Vec::new();
24316        assert!(
24317            p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
24318                .is_none()
24319        );
24320        let mut hid = emb.clone();
24321        assert_eq!(
24322            p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
24323            crate::gpu::BatchGraphOutcome::Declined
24324        );
24325        assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
24326        assert!(p.try_multi_burst(3, 0, 4).is_none());
24327        assert!(
24328            p.graph_declines().is_empty(),
24329            "no attention decline logged: {:?}",
24330            p.graph_declines()
24331        );
24332        // (No assertion on graph_prefill_preferred: with no attention
24333        // decline it follows the device — a test process that brought a
24334        // wgpu adapter up routes this resident MoE through the graph.)
24335
24336        let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
24337        assert_eq!(plain().graph_attn_decline_reason(), None);
24338        assert_eq!(plain().wgpu_graph_attn_decline(), None);
24339        assert!(
24340            plain().graph_attn_geom(0).is_none(),
24341            "uniform models keep the historical arms"
24342        );
24343        let mut q = plain();
24344        q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
24345        assert_eq!(
24346            q.graph_attn_decline_reason(),
24347            Some("learned attention sinks")
24348        );
24349        assert_eq!(
24350            q.graph_attn_geom(1).unwrap().sink,
24351            Some(&[0.25f32, -0.25][..])
24352        );
24353        let mut q = plain();
24354        q.set_attn_geometry(None, Some(2)).unwrap();
24355        assert_eq!(
24356            q.graph_attn_decline_reason(),
24357            Some("V heads narrower than Q/K heads")
24358        );
24359        assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
24360        let mut q = plain();
24361        q.sliding_layers = Some(vec![true, false]);
24362        q.swa = Some((4, usize::MAX));
24363        assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
24364        assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
24365        assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
24366
24367        // YaRN's post-rotation amplitude factor rides the per-layer graph
24368        // geometry. It is intentionally not an angular scale folded into
24369        // `invf`: the WGPU RoPE kernel receives it separately.
24370        let mut q = mimo_test_pipeline();
24371        q.rope_scale = 2.0;
24372        q.rope_scale_local = 1.25;
24373        assert_eq!(q.wgpu_graph_attn_decline(), None);
24374        assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
24375        assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
24376
24377        // A uniform YaRN model cannot yet pass the factor through the fused
24378        // RKV arm; decline safely instead of producing scale-1 output.
24379        let mut q = plain();
24380        q.rope_scale = 2.0;
24381        assert_eq!(
24382            q.wgpu_graph_attn_decline(),
24383            Some("scaled RoPE without per-layer geometry")
24384        );
24385    }
24386
24387    /// The native q1 attention path has a narrower contract than the WGPU
24388    /// per-layer geometry graph, but it must not discard the exact Flint
24389    /// combination of uniform GQA + sliding windows + learned sinks.  The
24390    /// actual Metal kernel is covered independently with a direct sink
24391    /// softmax test; this is the pure admission test, with no model run.
24392    #[cfg(target_os = "macos")]
24393    #[test]
24394    fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
24395        // rd=64 is in the native rope kernel's lane-pair contract; the
24396        // synthetic stack otherwise has the same uniform K/V geometry that
24397        // Flint needs for q1 (unlike the deliberately ragged MiMo fixture).
24398        let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
24399        p.swa = Some((16, usize::MAX));
24400        p.sliding_layers = Some(vec![true, true]);
24401        p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
24402        p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
24403
24404        assert_eq!(
24405            p.graph_attn_decline_reason(),
24406            Some("learned attention sinks")
24407        );
24408        let swa_graph = p.metal_graph_swa();
24409        assert!(swa_graph, "uniform sliding sink geometry must reach q1");
24410        let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
24411            && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
24412        assert!(
24413            native_sink_attend,
24414            "the q1 sink admission must follow the SWA door"
24415        );
24416        // The batched chunk softmax has no value-less sink column, so its
24417        // separate admission gate must keep this prompt on the host path.
24418        assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
24419    }
24420
24421    #[test]
24422    fn mimo_verify_rewind_preserves_lagging_host_caches() {
24423        let mut p = mimo_test_pipeline();
24424        for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
24425            let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
24426            for _ in 0..if li == 0 { 2 } else { 12 } {
24427                layer.append(&row, &row, &[]);
24428            }
24429        }
24430        p.mimo_verify_rewind(9).unwrap();
24431        assert_eq!(p.kv_cache.layers[0].seq_len, 2);
24432        for layer in &p.kv_cache.layers[1..] {
24433            assert_eq!(layer.seq_len, 9);
24434        }
24435    }
24436
24437    /// CMF_LAYER_DUMP: the decode walk and the batched prefill both write
24438    /// every (position, layer) hidden, the two sets agree byte for byte,
24439    /// and the last layer's file is the stack output.
24440    #[test]
24441    fn layer_dump_covers_every_position_and_layer_on_both_walks() {
24442        let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
24443        let _ = std::fs::remove_dir_all(&dir);
24444        let mut p = mimo_test_pipeline();
24445        let hs = p.hidden_size;
24446        let ids = [5u32, 9, 11, 2, 40];
24447        p.layer_dump = Some(dir.join("decode"));
24448        for (pos, &id) in ids.iter().enumerate() {
24449            let e = p.embed_single(id);
24450            let _ = p.forward_layers(&e, pos, None);
24451        }
24452        p.clear_sequence_state();
24453        p.layer_dump = Some(dir.join("prefill"));
24454        let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24455        for pos in 0..ids.len() {
24456            for li in 0..p.num_layers {
24457                let name = format!("p{pos:06}_l{li:02}.f32");
24458                let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
24459                let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
24460                assert_eq!(a.len(), hs * 4, "{name}");
24461                assert_eq!(a, b, "{name}");
24462            }
24463        }
24464        let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
24465        let vals: Vec<f32> = last
24466            .chunks(4)
24467            .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
24468            .collect();
24469        assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
24470        let _ = std::fs::remove_dir_all(&dir);
24471    }
24472
24473    #[test]
24474    fn attn_geometry_and_sinks_are_validated() {
24475        let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
24476        assert!(
24477            p.set_attn_geometry(Some(vec![2]), None).is_err(),
24478            "one entry per layer"
24479        );
24480        assert!(
24481            p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
24482            "3 does not divide 4"
24483        );
24484        assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
24485        assert!(p.set_attn_geometry(None, Some(0)).is_err());
24486        assert!(
24487            p.set_attn_geometry(None, Some(5)).is_err(),
24488            "V wider than the head"
24489        );
24490        p.set_attn_geometry(None, Some(4)).unwrap();
24491        assert_eq!(
24492            p.v_head_dim, None,
24493            "v_head_dim == head_dim is the uniform case"
24494        );
24495        p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
24496        p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
24497        assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
24498        assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
24499        assert!(
24500            p.kv_cache.layers[1].sinks.is_some(),
24501            "a reshape keeps the layer's sinks"
24502        );
24503        assert_eq!(p.layer_geom(1).0, 4);
24504        assert!(
24505            p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
24506            "one sink per Q head"
24507        );
24508        assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
24509        assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
24510    }
24511
24512    /// The O(1) Nyström state replaces a plain full-context softmax; it
24513    /// must never be armed on a sliding, sink or narrow-V layer.
24514    #[test]
24515    fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
24516        let cfg = || {
24517            Some(crate::nystrom::O1Cfg {
24518                layers: crate::nystrom::O1Layers::All,
24519                m: 4,
24520                w: 8,
24521                sink: 2,
24522                rect: crate::nystrom::O1Rect::Aggregate,
24523            })
24524        };
24525        let mut p = mimo_test_pipeline();
24526        p.set_o1(cfg());
24527        assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
24528        let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
24529        q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
24530        q.sliding_layers = Some(vec![false, false, true]);
24531        q.swa = Some((4, usize::MAX));
24532        q.set_o1(cfg());
24533        assert_eq!(q.o1_flags, vec![true, false, false]);
24534    }
24535
24536    #[test]
24537    fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
24538        const B: usize = 19;
24539        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24540        p.set_o1(Some(crate::nystrom::O1Cfg {
24541            layers: crate::nystrom::O1Layers::All,
24542            m: 4,
24543            w: 8,
24544            sink: 2,
24545            rect: crate::nystrom::O1Rect::Aggregate,
24546        }));
24547        p.o1_begin_with_prefix(Some(B));
24548        let ids: Vec<u32> = (0..B as u32).collect();
24549        let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24550
24551        assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
24552        assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
24553        let next = p.embed_single(B as u32);
24554        let _ = p.forward_layers(&next, B, None);
24555        assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
24556    }
24557
24558    #[test]
24559    fn o1_pair_transition_commits_scratch_before_epoch_publication() {
24560        const B: usize = 19;
24561        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24562        // Keep a real recurrent layer ahead of the Full O(1) layer so the
24563        // pair test observes the GDN lane-2 scratch swap at the same
24564        // boundary, rather than only exercising an artificial scratch vec.
24565        let gdn_cfg = crate::linear_core::GdnCfg {
24566            num_v_heads: 2,
24567            num_k_heads: 1,
24568            key_head_dim: 2,
24569            value_head_dim: 4,
24570            conv_kernel: 3,
24571            hidden_size: 8,
24572            rms_eps: 1e-6,
24573            output_gate_sigmoid: false,
24574        };
24575        let synth = |n: usize, salt: usize| -> Vec<f32> {
24576            (0..n)
24577                .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
24578                .collect()
24579        };
24580        let qt = |rows: usize, cols: usize, salt: usize| {
24581            crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
24582        };
24583        let c_dim = gdn_cfg.conv_dim();
24584        let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
24585        p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
24586            in_proj_qkv: qt(c_dim, 8, 1),
24587            in_proj_z: qt(vd, 8, 2),
24588            in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
24589            in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
24590            conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
24591            a_log: vec![0.2, 0.5],
24592            dt_bias: synth(gdn_cfg.num_v_heads, 6),
24593            norm: vec![1.0; gdn_cfg.value_head_dim],
24594            out_proj: qt(8, vd, 7),
24595        });
24596        p.gdn_cfg = Some(gdn_cfg);
24597        p.set_o1(Some(crate::nystrom::O1Cfg {
24598            layers: crate::nystrom::O1Layers::All,
24599            m: 4,
24600            w: 8,
24601            sink: 2,
24602            rect: crate::nystrom::O1Rect::Aggregate,
24603        }));
24604        p.o1_begin_with_prefix(Some(B));
24605        for pos in 0..B - 2 {
24606            let emb = p.embed_single(pos as u32);
24607            let _ = p.forward_layers(&emb, pos, None);
24608        }
24609        let lane1_state = p.kv_cache.layers[0].linear_state.clone();
24610
24611        let e1 = p.embed_single((B - 2) as u32);
24612        let e2 = p.embed_single((B - 1) as u32);
24613        let _ = p.forward_pair(&e1, &e2, B - 2);
24614
24615        assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
24616        assert!(
24617            p.kv_cache
24618                .layers
24619                .iter()
24620                .enumerate()
24621                .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
24622        );
24623        assert!(!p.kv_cache.layers[0].linear_state.is_empty());
24624        assert_ne!(
24625            p.kv_cache.layers[0].linear_state, lane1_state,
24626            "real pair must commit GDN lane 2 before returning"
24627        );
24628        assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
24629        let next = p.embed_single(B as u32);
24630        let _ = p.forward_layers(&next, B, None);
24631        assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
24632    }
24633
24634    #[test]
24635    fn o1_error_observation_stays_terminal_until_reset() {
24636        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24637        p.set_o1(Some(crate::nystrom::O1Cfg {
24638            layers: crate::nystrom::O1Layers::All,
24639            m: 4,
24640            w: 8,
24641            sink: 2,
24642            rect: crate::nystrom::O1Rect::Aggregate,
24643        }));
24644        p.o1_begin();
24645        p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
24646
24647        assert!(p.o1_seal_checked().is_err());
24648        assert!(
24649            p.o1_seal_checked().is_err(),
24650            "retry must see the sticky error"
24651        );
24652        let k = vec![0.2f32; 4];
24653        let v = vec![0.3f32; 4];
24654        p.kv_cache.layers[0].append(&k, &v, &[]);
24655        assert_eq!(p.kv_cache.layers[0].seq_len, 0);
24656
24657        p.reset_session();
24658        p.o1_begin();
24659        p.kv_cache.layers[0].append(&k, &v, &[]);
24660        assert_eq!(p.kv_cache.layers[0].seq_len, 1);
24661    }
24662
24663    #[test]
24664    fn nll_graph_failure_is_terminal_and_request_is_reusable() {
24665        let ids = vec![1u32, 2, 3, 4, 5, 6];
24666        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24667        p.graph_logits = Some(vec![123.0]);
24668        p.graph_want_logits = true;
24669        p.graph_failed
24670            .store(true, std::sync::atomic::Ordering::Relaxed);
24671        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
24672        let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
24673        assert!(err.contains("before NLL"));
24674        assert!(p.graph_logits.is_none());
24675        assert!(!p.graph_want_logits);
24676        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
24677        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
24678
24679        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24680        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
24681        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
24682        assert_eq!(actual.1, expected.1);
24683        assert!((actual.0 - expected.0).abs() < 1e-9);
24684    }
24685
24686    #[test]
24687    fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
24688        let ids = vec![1u32, 2, 3, 4, 5, 6];
24689        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24690        p.nll_test_fail_at = Some(1);
24691        let err = p
24692            .nll_ids_from(&ids, 0)
24693            .expect_err("one-shot forward failure");
24694        assert!(err.contains("forward") || err.contains("score row"));
24695        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
24696        assert!(!p.graph_want_logits);
24697        assert!(p.graph_logits.is_none());
24698        assert!(p.kv_history.is_empty());
24699
24700        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24701        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
24702        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
24703        assert_eq!(actual.1, expected.1);
24704        assert!((actual.0 - expected.0).abs() < 1e-9);
24705    }
24706
24707    #[test]
24708    fn nll_serial_failure_before_first_row_is_reported() {
24709        let ids = vec![1u32, 2, 3, 4];
24710        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24711        p.nll_test_force_serial = true;
24712        p.nll_test_fail_at = Some(0);
24713        let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
24714        assert!(err.contains("serial forward"));
24715        assert!(p.kv_history.is_empty());
24716        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
24717        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
24718    }
24719
24720    #[test]
24721    fn ffn_probe_failure_discards_recorder_and_state() {
24722        let ids = vec![1u32, 2, 3, 4];
24723        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24724        p.nll_test_fail_at = Some(0);
24725        let err = p
24726            .probe_ffn_mass_batch(&ids)
24727            .expect_err("probe forward failure");
24728        assert!(err.contains("NLL"));
24729        assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
24730        assert!(p.kv_history.is_empty());
24731        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
24732    }
24733
24734    #[test]
24735    fn nll_test_controls_are_pipeline_scoped() {
24736        let ids = vec![1u32, 2, 3, 4];
24737        let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24738        let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24739        failing.nll_test_force_serial = true;
24740        failing.nll_test_fail_at = Some(0);
24741
24742        assert!(!failing.can_prefill_batched());
24743        assert!(unaffected.can_prefill_batched());
24744        let expected = unaffected
24745            .nll_ids_from(&ids, 0)
24746            .expect("unaffected pipeline remains usable");
24747        let err = failing
24748            .nll_ids_from(&ids, 0)
24749            .expect_err("failure injection belongs to failing pipeline");
24750        assert!(err.contains("serial forward"));
24751        assert!(failing.nll_test_fail_at.is_none());
24752        assert!(unaffected.can_prefill_batched());
24753        let actual = unaffected
24754            .nll_ids_from(&ids, 0)
24755            .expect("unaffected pipeline remains reusable");
24756        assert_eq!(actual.1, expected.1);
24757        assert!((actual.0 - expected.0).abs() < 1e-9);
24758    }
24759
24760    #[test]
24761    fn forward_ids_failure_channel_is_terminal_and_reusable() {
24762        let ids = vec![1u32, 2, 3, 4, 5, 6];
24763        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24764        p.graph_logits = Some(vec![123.0]);
24765        p.graph_want_logits = true;
24766        p.graph_failed
24767            .store(true, std::sync::atomic::Ordering::Relaxed);
24768        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
24769
24770        let err = p
24771            .forward_ids(&ids, None)
24772            .expect_err("a failed forward must not become a valid head result");
24773        assert!(err.contains("forward_ids setup"));
24774        assert!(p.graph_logits.is_none());
24775        assert!(!p.graph_want_logits);
24776        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
24777        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
24778        assert_eq!(p.kv_cache.seq_len(), 0);
24779
24780        let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
24781            .forward_ids(&ids, None)
24782            .expect("fresh forward_ids");
24783        let actual = p
24784            .forward_ids(&ids, None)
24785            .expect("pipeline remains reusable after a failed forward");
24786        assert_eq!(actual.len(), expected.len());
24787        assert!(
24788            actual
24789                .iter()
24790                .zip(expected)
24791                .all(|(a, b)| (a - b).abs() < 1e-9)
24792        );
24793        assert_eq!(p.kv_cache.seq_len(), ids.len());
24794    }
24795
24796    #[test]
24797    fn sigmoid_router_floor_is_explicit_per_architecture() {
24798        // Flint and MiniMax M3.1 reference runtimes use +1e-20, while the
24799        // generic LFM2-compatible path uses +1e-6. At low (but representable)
24800        // sigmoid scores, silently sharing the latter changes expert weights by
24801        // orders of magnitude and can make a routed layer look coherent while
24802        // discarding its expert contribution.
24803        let zero = || DenseFfn {
24804            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24805            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24806            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24807            act: Act::Silu,
24808            down_t: None,
24809            segs: Vec::new(),
24810        };
24811        let m = MoeFfn {
24812            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24813            experts: vec![zero(), zero()],
24814            top_k: 1,
24815            norm_topk_prob: true,
24816            router_sigmoid: true,
24817            sigmoid_denom_eps: 1e-20,
24818            expert_bias: None,
24819            routed_scaling: 2.5,
24820            route_tau: None,
24821            shared: None,
24822            stats: std::cell::RefCell::new(Vec::new()),
24823            act_sq: std::cell::RefCell::new(Vec::new()),
24824            act_rows: std::cell::RefCell::new(Vec::new()),
24825            mask: None,
24826            per_expert_scale: None,
24827            router_input_norm: false,
24828            resonance: None,
24829            grown: Vec::new(),
24830        };
24831        let logits = [-20.0f32, -20.0];
24832        let (_, p, flint_wsum) = moe_route(&logits, &m, None);
24833        let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24834        let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
24835        let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
24836        assert!((flint_wsum - reference_expected).abs() < 1e-15);
24837        assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
24838        assert!(legacy_wsum > minimax_wsum * 100.0);
24839    }
24840
24841    #[test]
24842    fn resonance_scores_match_formula_and_stable_tie() {
24843        let r = Resonance {
24844            // Three descriptors, hidden=2, one projection row each.
24845            mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
24846            u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
24847            k: 1,
24848            bias: vec![1.5, 0.5, 0.0],
24849            shell: Vec::new(),
24850        };
24851        let x = [1.0f32, 1.0];
24852        let mut got = vec![0.0; 3];
24853        r.scores(&x, &mut got);
24854        // Expert 0 and 1 are an exact score tie; the CPU top-1 contract uses
24855        // the lower index.  The values also check d² - (U·d)², not just tie
24856        // ordering.
24857        assert!((got[0] - 0.5).abs() < 1e-6);
24858        assert!((got[1] - 0.5).abs() < 1e-6);
24859        assert!(got[2].abs() < 1e-6);
24860        let best = got
24861            .iter()
24862            .enumerate()
24863            .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
24864            .map(|(i, _)| i);
24865        assert_eq!(best, Some(0));
24866        assert!(got.iter().all(|v| v.is_finite()));
24867    }
24868
24869    /// The growth shell (spec §2): a grown expert whose reconstruction
24870    /// error lies outside its shell scores −∞, one inside keeps the exact
24871    /// resonance score, trunk rows (`+inf` shell) are bit-identical to the
24872    /// shell-less computation; the process-wide switch disables it.
24873    #[test]
24874    fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
24875        // hidden = 2, rank 1. Experts 0/1 = trunk (shell +inf); 2 and 3 =
24876        // grown, the same descriptor (μ = (0, 1), u = (1, 1)) with shells
24877        // 6.0 and 0.25. At x' = (3, 0): d = (3, −1), d² = 10, proj =
24878        // (3 − 1)² = 4, err = 6 exactly — on the boundary of expert 2's
24879        // shell (kept: the rule is strict `>`), outside expert 3's.
24880        let plain = Resonance {
24881            mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
24882            u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
24883            k: 1,
24884            bias: vec![1.5, 0.5, 0.0, 0.0],
24885            shell: Vec::new(),
24886        };
24887        let shelled = Resonance {
24888            mu: plain.mu.clone(),
24889            u: plain.u.clone(),
24890            k: 1,
24891            bias: plain.bias.clone(),
24892            shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
24893        };
24894        assert!(!plain.has_shell());
24895        assert!(shelled.has_shell());
24896        let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
24897        let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
24898        set_growth_shell(Some(true));
24899        assert!(growth_shell_enabled());
24900        // x = (1, 1): the grown experts reconstruct it exactly (err 0):
24901        // inside both shells, every row the shell-less bits.
24902        let x = [1.0f32, 1.0];
24903        plain.scores(&x, &mut a);
24904        shelled.scores(&x, &mut b);
24905        assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
24906        assert!(a[2] == 0.0 && a[3] == 0.0);
24907        // x' = (3, 0): expert 3 → −∞, expert 2 (err == shell) and the
24908        // trunk rows keep their exact bits.
24909        let xo = [3.0f32, 0.0];
24910        plain.scores(&xo, &mut a);
24911        shelled.scores(&xo, &mut b);
24912        assert_eq!(a[2], -6.0);
24913        assert_eq!(a[3], -6.0);
24914        assert_eq!(
24915            bits(&a[..3]),
24916            bits(&b[..3]),
24917            "trunk rows + the boundary row"
24918        );
24919        assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
24920        assert_eq!(shelled.effective_shell(4), shelled.shell);
24921        // The switch (`CMF_GROWTH_SHELL=off` / `growth-eval --shell off`):
24922        // all +inf, the shell-less bits everywhere.
24923        set_growth_shell(Some(false));
24924        assert!(!growth_shell_enabled());
24925        assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
24926        shelled.scores(&xo, &mut b);
24927        assert_eq!(bits(&a), bits(&b));
24928        set_growth_shell(None);
24929        // A shell vector shorter than the expert count masks nothing
24930        // beyond it (a legacy layer whose tail has no shell).
24931        let short = Resonance {
24932            shell: vec![f32::INFINITY, f32::INFINITY],
24933            ..shelled
24934        };
24935        set_growth_shell(Some(true));
24936        short.scores(&xo, &mut b);
24937        assert_eq!(bits(&a), bits(&b));
24938        set_growth_shell(None);
24939    }
24940
24941    /// `moe_route` with −∞ logits (a grown expert outside its shell):
24942    /// top-1 is the best finite expert with weight exactly 1.0 on both
24943    /// the softmax and the sigmoid path; all −∞ degrades to uniform.
24944    #[test]
24945    fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
24946        let zero = || DenseFfn {
24947            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24948            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24949            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24950            act: Act::Silu,
24951            down_t: None,
24952            segs: Vec::new(),
24953        };
24954        let moe = |sigmoid: bool| MoeFfn {
24955            router: QTensor::from_f32(vec![0.0; 8], 4, 2),
24956            experts: vec![zero(), zero(), zero(), zero()],
24957            top_k: 1,
24958            norm_topk_prob: true,
24959            router_sigmoid: sigmoid,
24960            sigmoid_denom_eps: 1e-6,
24961            expert_bias: None,
24962            routed_scaling: 1.0,
24963            route_tau: None,
24964            shared: None,
24965            stats: std::cell::RefCell::new(Vec::new()),
24966            act_sq: std::cell::RefCell::new(Vec::new()),
24967            act_rows: std::cell::RefCell::new(Vec::new()),
24968            mask: None,
24969            per_expert_scale: None,
24970            router_input_norm: false,
24971            resonance: None,
24972            grown: Vec::new(),
24973        };
24974        let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
24975        for sigmoid in [false, true] {
24976            let m = moe(sigmoid);
24977            let (idx, p, wsum) = moe_route(&logits, &m, None);
24978            assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
24979            assert_eq!(p[1], 0.0);
24980            assert_eq!(p[3], 0.0);
24981            assert!(p[2] > p[0] && p[0] > 0.0);
24982            assert!(p.iter().all(|v| v.is_finite()));
24983            let w = p[2] / wsum;
24984            if sigmoid {
24985                // The sigmoid renorm keeps its reference floor (+1e-6).
24986                assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
24987            } else {
24988                assert_eq!(
24989                    w, 1.0,
24990                    "softmax: the renormalized top-1 weight is exactly 1"
24991                );
24992            }
24993            // Masked experts stay masked even when they are the only ones
24994            // "admitted" by an allow-list that covers everything.
24995            let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
24996            assert_eq!(idx, vec![2]);
24997        }
24998        // A finite expert always beats −∞ whatever the bias / order.
24999        let m = moe(false);
25000        let (idx, _, _) = moe_route(
25001            &[
25002                f32::NEG_INFINITY,
25003                f32::NEG_INFINITY,
25004                f32::NEG_INFINITY,
25005                -9.0,
25006            ],
25007            &m,
25008            None,
25009        );
25010        assert_eq!(idx, vec![3]);
25011        // Every expert at −∞ (cannot happen on a grown file — trunk rows
25012        // have no shell): uniform, finite, lowest index.
25013        let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
25014        assert_eq!(idx, vec![0]);
25015        assert!(p.iter().all(|&v| v == 0.25));
25016        assert!(wsum.is_finite() && wsum > 0.0);
25017    }
25018
25019    /// MiniMax-M3.1's DEVICE route (the pooled token-graph arm's exact
25020    /// flags: sigmoid scoring via n's high bit, +1e-20 floor,
25021    /// routed_scaling 2.0, selection bias on the CHOICE only) must match
25022    /// the host reference in experts, ORDER and weights — the check that
25023    /// caught the flags/slot collision.
25024    #[cfg(feature = "gpu")]
25025    #[test]
25026    fn minimax_device_route_matches_host_sigmoid_contract() {
25027        match crate::gpu_wgpu::selected_and_up() {
25028            None => {
25029                crate::gpu_wgpu::skip_or_fail(module_path!());
25030                return;
25031            }
25032            // Sibling unit tests in THIS process select wgpu by writing
25033            // `CMF_GPU` (the gpu_wgpu test module does it ~70 times), so on a
25034            // headless runner the variable is already set by the time this
25035            // test reads it — "requested" here is test pollution, not an
25036            // operator's ask, and the adapter genuinely does not exist in
25037            // this process. Same treatment as the gpu_wgpu tests themselves:
25038            // a visible skip, never a silent pass.
25039            Some(false) => {
25040                eprintln!(
25041                    "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25042                    module_path!()
25043                );
25044                return;
25045            }
25046            Some(true) => {}
25047        }
25048        let n = 128usize;
25049        let top_k = 4usize;
25050        let logits: Vec<f32> = (0..n)
25051            .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25052            .collect();
25053        let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25054        let m = MoeFfn {
25055            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25056            experts: Vec::new(),
25057            top_k,
25058            norm_topk_prob: true,
25059            router_sigmoid: true,
25060            sigmoid_denom_eps: 1e-20,
25061            expert_bias: Some(bias.clone()),
25062            routed_scaling: 2.0,
25063            route_tau: None,
25064            shared: None,
25065            stats: std::cell::RefCell::new(Vec::new()),
25066            act_sq: std::cell::RefCell::new(Vec::new()),
25067            act_rows: std::cell::RefCell::new(Vec::new()),
25068            mask: None,
25069            per_expert_scale: None,
25070            router_input_norm: false,
25071            resonance: None,
25072            grown: Vec::new(),
25073        };
25074        let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25075        let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
25076
25077        for shared in [Some(n as u32), None] {
25078            let mut gi = Vec::new();
25079            let mut gw = Vec::new();
25080            assert!(
25081                crate::gpu_wgpu::minimax_moe_route_for_test(
25082                    &logits,
25083                    Some(&bias),
25084                    top_k,
25085                    2.0,
25086                    shared,
25087                    &mut gi,
25088                    &mut gw,
25089                ),
25090                "the device did not come up — the test checked nothing"
25091            );
25092            let routed = if shared.is_some() { top_k } else { gi.len() };
25093            assert_eq!(
25094                &gi[..routed],
25095                &ci[..],
25096                "different experts or order (shared={shared:?})"
25097            );
25098            let d = cw
25099                .iter()
25100                .zip(&gw[..routed])
25101                .map(|(a, b)| (a - b).abs())
25102                .fold(0.0f32, f32::max);
25103            println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
25104            assert!(d < 1e-6, "the weights diverged by {d:.3e}");
25105            if let Some(s) = shared {
25106                assert_eq!(gi[top_k], s as usize, "shared pin slot");
25107                assert_eq!(gw[top_k], 1.0, "shared weight");
25108            }
25109        }
25110    }
25111
25112    /// The pooled arm's COLD hand-back: `(expert, weight)` pairs for the
25113    /// picks the bank could not serve must carry the host's EXACT final
25114    /// weights — the device ships its resident sum, the host adds this on
25115    /// top of the same residual, so a weight drift compounds per layer.
25116    #[cfg(feature = "gpu")]
25117    #[test]
25118    fn minimax_device_cold_weights_match_host_contract() {
25119        match crate::gpu_wgpu::selected_and_up() {
25120            None => {
25121                crate::gpu_wgpu::skip_or_fail(module_path!());
25122                return;
25123            }
25124            // Sibling unit tests in THIS process select wgpu by writing
25125            // `CMF_GPU` (the gpu_wgpu test module does it ~70 times), so on a
25126            // headless runner the variable is already set by the time this
25127            // test reads it — "requested" here is test pollution, not an
25128            // operator's ask, and the adapter genuinely does not exist in
25129            // this process. Same treatment as the gpu_wgpu tests themselves:
25130            // a visible skip, never a silent pass.
25131            Some(false) => {
25132                eprintln!(
25133                    "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25134                    module_path!()
25135                );
25136                return;
25137            }
25138            Some(true) => {}
25139        }
25140        let n = 128usize;
25141        let top_k = 4usize;
25142        let logits: Vec<f32> = (0..n)
25143            .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25144            .collect();
25145        let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25146        let m = MoeFfn {
25147            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25148            experts: Vec::new(),
25149            top_k,
25150            norm_topk_prob: true,
25151            router_sigmoid: true,
25152            sigmoid_denom_eps: 1e-20,
25153            expert_bias: Some(bias.clone()),
25154            routed_scaling: 2.0,
25155            route_tau: None,
25156            shared: None,
25157            stats: std::cell::RefCell::new(Vec::new()),
25158            act_sq: std::cell::RefCell::new(Vec::new()),
25159            act_rows: std::cell::RefCell::new(Vec::new()),
25160            mask: None,
25161            per_expert_scale: None,
25162            router_input_norm: false,
25163            resonance: None,
25164            grown: Vec::new(),
25165        };
25166        let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25167        assert_eq!(ci.len(), top_k);
25168
25169        // Every pick resident → no cold pairs at all.
25170        let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
25171        let mut cold = Vec::new();
25172        let mut idx = Vec::new();
25173        let mut dw = Vec::new();
25174        assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25175            &logits,
25176            Some(&bias),
25177            top_k,
25178            2.0,
25179            &resident,
25180            n as u32,
25181            &mut cold,
25182            &mut idx,
25183            &mut dw,
25184        ));
25185        assert!(cold.is_empty(), "fully resident bank: {cold:?}");
25186        // With subset+pin together: routed slots carry the FLAT remap value
25187        // (here identity = the expert id), weights the host contract, and
25188        // the pinned shared keeps weight 1 — this exact combination is what
25189        // the pooled arm runs with.
25190        for (r, &e) in ci.iter().enumerate() {
25191            assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
25192            let host = p[e] / wsum;
25193            let d = (dw[r] - host).abs();
25194            assert!(
25195                d <= 1e-6 * host.abs().max(1.0),
25196                "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
25197                dw[r]
25198            );
25199        }
25200        assert_eq!(idx[top_k] as usize, n, "shared pin slot");
25201        assert_eq!(dw[top_k], 1.0, "shared weight");
25202
25203        // Make exactly two of the CHOSEN picks cold.
25204        let mut remap = resident.clone();
25205        remap[ci[0]] = u32::MAX;
25206        remap[ci[2]] = u32::MAX;
25207        let mut cold = Vec::new();
25208        let mut idx2 = Vec::new();
25209        let mut dw2 = Vec::new();
25210        assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25211            &logits,
25212            Some(&bias),
25213            top_k,
25214            2.0,
25215            &remap,
25216            n as u32,
25217            &mut cold,
25218            &mut idx2,
25219            &mut dw2,
25220        ));
25221        // The resident picks of the partial bank still carry exact weights.
25222        // A COLD rank is a placeholder the device writes as slot 0 / weight
25223        // 0.0 — its real (id, weight) pair rides in `cold` — so weight zero
25224        // is what marks it, not the slot value: reading the placeholder as
25225        // a resident expert compared 0.0 against p[0]/wsum and failed with
25226        // Δ=2.552e-1. The test logits keep every sigmoid score strictly
25227        // positive, so a resident pick can never weigh exactly zero.
25228        let mut resident_weight_checked = 0usize;
25229        for (r, &slot) in idx2.iter().enumerate().take(top_k) {
25230            if dw2[r] == 0.0 {
25231                continue; // cold rank: placeholder, not an expert id
25232            }
25233            let e = slot as usize;
25234            assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
25235            let host = p[e] / wsum;
25236            let d = (dw2[r] - host).abs();
25237            assert!(
25238                d <= 1e-6 * host.abs().max(1.0),
25239                "partial-bank resident weight expert {e}: Δ={d:.3e}"
25240            );
25241            resident_weight_checked += 1;
25242        }
25243        assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
25244        println!("partial bank: checked {resident_weight_checked} resident weights");
25245        assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
25246        let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
25247        ids.sort_unstable();
25248        let mut want = vec![ci[0], ci[2]];
25249        want.sort_unstable();
25250        assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
25251        for &(e, w) in &cold {
25252            let host = p[e] / wsum;
25253            let d = (w - host).abs();
25254            println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
25255            assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
25256        }
25257    }
25258
25259    /// The resonance router (top-1) selects by the raw score as the
25260    /// trainer and the graph do — not by softmax probabilities, where two
25261    /// scores closer than 2^-25 collapse to the same `exp(l − max) = 1.0`
25262    /// and the LOWER index wins a token whose score is strictly smaller.
25263    #[test]
25264    fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
25265        let zero = || DenseFfn {
25266            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25267            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25268            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25269            act: Act::Silu,
25270            down_t: None,
25271            segs: Vec::new(),
25272        };
25273        let moe = |resonant: bool, norm_topk: bool| MoeFfn {
25274            router: QTensor::from_f32(vec![0.0; 6], 3, 2),
25275            experts: vec![zero(), zero(), zero()],
25276            top_k: 1,
25277            norm_topk_prob: norm_topk,
25278            router_sigmoid: false,
25279            sigmoid_denom_eps: 1e-6,
25280            expert_bias: None,
25281            routed_scaling: 1.0,
25282            route_tau: None,
25283            shared: None,
25284            stats: std::cell::RefCell::new(Vec::new()),
25285            act_sq: std::cell::RefCell::new(Vec::new()),
25286            act_rows: std::cell::RefCell::new(Vec::new()),
25287            mask: None,
25288            per_expert_scale: None,
25289            router_input_norm: false,
25290            resonance: resonant.then(|| Resonance {
25291                mu: vec![0.0; 6],
25292                u: Vec::new(),
25293                k: 0,
25294                bias: vec![0.0; 3],
25295                shell: Vec::new(),
25296            }),
25297            grown: Vec::new(),
25298        };
25299        // lo = −0.1, hi = the next f32 towards zero: hi − lo = 2^-27 <
25300        // 2^-25, so exp(lo − hi) rounds to exactly 1.0 — a softmax tie.
25301        let lo = -0.1f32;
25302        let hi = f32::from_bits(lo.to_bits() - 1);
25303        assert!(hi > lo && hi - lo < 2f32.powi(-25));
25304        assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
25305        // The gated MoE (softmax) path: the tie hands the token to index 0.
25306        let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
25307        assert_eq!(
25308            idx,
25309            vec![0],
25310            "softmax tie → lower index (the gated contract)"
25311        );
25312        // The resonance path: the strictly larger raw score wins, weight
25313        // exactly 1.0 with and without norm_topk.
25314        for norm in [true, false] {
25315            let m = moe(true, norm);
25316            let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
25317            assert_eq!(idx, vec![1], "norm_topk {norm}");
25318            assert_eq!(p, vec![0.0, 1.0, 0.0]);
25319            assert_eq!(p[1] / wsum, 1.0);
25320            // An exact tie: the first maximum (as `resonance_winner` and
25321            // `embryo_core_route_pick`).
25322            let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
25323            assert_eq!(idx, vec![0]);
25324            // `−∞` never wins; the admitted set is honoured.
25325            let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
25326            assert_eq!(idx, vec![2]);
25327            let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
25328            assert_eq!(idx, vec![0]);
25329            assert_eq!(p[0] / wsum, 1.0);
25330            // Every admitted expert at −∞: the generic path's uniform
25331            // fallback (lowest index, finite weights).
25332            let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
25333            assert_eq!(idx, vec![0]);
25334            assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
25335        }
25336    }
25337}