Skip to main content

cortiq_engine/
pipeline.rs

1//! Full inference pipeline: tokenize → embed → layers → lm_head → sample → decode.
2//!
3//! Prefill/decode contract: every token is forwarded exactly once and
4//! enters the KV cache exactly once. Logits for the next token are
5//! computed from the hidden state of the LAST forwarded token — the
6//! decode loop forwards the freshly sampled token, never re-embeds the
7//! prompt tail (v1 duplicated the last prompt token in the cache).
8
9use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12/// MiMo-V2 multi-token prediction (draft stack + speculative round). A
13/// child module so it runs on the pipeline's own helpers.
14#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16#[path = "clover.rs"]
17pub mod clover;
18/// d1's non-causal forward over a packed state and question set.
19#[path = "pipeline_masked.rs"]
20pub mod masked;
21#[path = "pipeline_speech.rs"]
22pub mod speech;
23use crate::kv_cache::KvCache;
24use crate::linear_core::{
25    GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
26    gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
27    vmf_phase_pair,
28};
29use crate::pool::Pool;
30use crate::qtensor::QTensor;
31use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
32use crate::tokenizer::Tokenizer;
33use cortiq_core::mask::TaskMask;
34use cortiq_core::types::NormStyle;
35
36pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
37    std::sync::atomic::AtomicBool::new(false);
38
39/// One MoE layer's expert bank as the resident token graph reads it: the
40/// `(gate, up, down)` tensor indices in routing order (the in-bank shared
41/// expert last), the layer's uniform layout flags and activation, and the
42/// file that holds them. A pure function of the layer's weights — walking
43/// 257 experts × 3 matrices per layer per token was ~2 ms of host time a
44/// token on a 40-layer 256-expert stack, so the walk runs once a layer.
45#[derive(Clone)]
46struct GraphMoeBank {
47    experts: Vec<(usize, usize, usize)>,
48    q4tp: Option<bool>,
49    gu_q2: Option<bool>,
50    q82: Option<bool>,
51    act: Option<crate::gpu::GraphAct>,
52    model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
53}
54
55/// Identity of the weights a `GraphMoeBank` was built from: the physical
56/// layer, the expert table's storage and length, the in-bank shared expert,
57/// and the first and last expert tensors' (file, index) — so a table
58/// rebuilt at a reused address (a skill, a grown expert) cannot alias an
59/// old entry.
60type GraphMoeKey = (usize, usize, usize, usize, usize, usize, usize);
61
62// MTP compaction statistics must describe the dense draft that will be
63// serialized, not an ambient serving experiment such as gate/oracle top-k.
64// A thread-local nesting counter keeps the guard local to calibration and
65// avoids mutating process-global environment variables mid-command.
66thread_local! {
67    static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
68}
69
70struct ExactMtpCalibrationGuard;
71
72impl ExactMtpCalibrationGuard {
73    fn enter() -> Self {
74        EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
75        Self
76    }
77}
78
79impl Drop for ExactMtpCalibrationGuard {
80    fn drop(&mut self) {
81        EXACT_MTP_CALIBRATION.with(|depth| {
82            debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
83            depth.set(depth.get().saturating_sub(1));
84        });
85    }
86}
87
88fn exact_mtp_calibration_active() -> bool {
89    EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
90}
91
92/// Extend an MTP draft-head shortlist to the model vocabulary.  Rows outside
93/// the shortlist were deliberately not evaluated, so they must be impossible
94/// draft candidates rather than neutral zero logits.
95fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
96    logits.resize(vocab_size, f32::NEG_INFINITY);
97}
98
99/// Reusable per-pipeline forward scratch: the four norm outputs the
100/// decode paths recompute every layer (single: n1/p1; pair: all four).
101/// Plain buffers, resized once — steady-state decode reuses them.
102struct ForwardScratch {
103    n1: Vec<f32>,
104    n2: Vec<f32>,
105    p1: Vec<f32>,
106    p2: Vec<f32>,
107}
108
109impl ForwardScratch {
110    fn new(hidden: usize) -> Self {
111        Self {
112            n1: vec![0.0; hidden],
113            n2: vec![0.0; hidden],
114            p1: vec![0.0; hidden],
115            p2: vec![0.0; hidden],
116        }
117    }
118}
119
120/// Complete inference pipeline state.
121pub struct Pipeline {
122    /// In-process layer split across local GPUs: (device, first layer,
123    /// last layer) per segment, in execution order. `None` = one device.
124    /// Arc so cloning the plan out of `&mut self` does not fight the
125    /// borrow checker on the hot path.
126    gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
127    /// Arc: the server shares one tokenizer handle across request
128    /// handlers without borrowing a pipeline slot.
129    pub tokenizer: std::sync::Arc<Tokenizer>,
130    pub kv_cache: KvCache,
131    pub sampler_config: SamplerConfig,
132    pub weights: PipelineWeights,
133    pub hidden_size: usize,
134    pub intermediate_size: usize,
135    pub num_heads: usize,
136    pub num_kv_heads: usize,
137    pub head_dim: usize,
138    /// Total virtual layers (num_layers × num_loops for looped models).
139    pub num_layers: usize,
140    /// Physical layers in weights.layers (≤ num_layers for looped models).
141    pub physical_layers: usize,
142    /// Looped Transformer: apply final norm after each loop iteration.
143    pub loop_final_norm: bool,
144    /// SenseNova-Loop: virtual → physical layer order (None: `v %
145    /// physical_layers`, the whole-stack loop of Nanbeige).
146    pub layer_map: Option<Vec<usize>>,
147    /// SenseNova-Loop: the mHC routers (`sensenova::Mhc`).
148    pub mhc: Option<Box<crate::sensenova::Mhc>>,
149    /// The decode token's mHC streams, `[streams · hidden]`.
150    mhc_r: Vec<f32>,
151    /// Interleaved-MRoPE sections and the prompt rows' (t, h, w) positions
152    /// for the prefill of an image prompt (SenseNova-Loop); cleared when
153    /// the prefill ends.
154    pub mrope_sections: Option<[usize; 3]>,
155    pub prompt_mrope: Option<Vec<[i64; 3]>>,
156    pub vocab_size: usize,
157    pub rms_eps: f64,
158    pub rope_base: f32,
159    pub norm_style: NormStyle,
160    /// RoPE dims actually rotated (≤ head_dim; Qwen3.5 uses head_dim/4).
161    pub rotary_dim: usize,
162    /// Optional Q-head count override for each attention layer (Laguna).
163    pub attention_heads_per_layer: Option<Vec<usize>>,
164    /// Optional KV-head count of each PHYSICAL attention layer (MiMo-V2:
165    /// 4 on full-attention layers, 8 on sliding ones). Set only through
166    /// [`Pipeline::set_attn_geometry`], which also reshapes the layer
167    /// caches. None = every layer has `num_kv_heads`.
168    pub kv_heads_per_layer: Option<Vec<usize>>,
169    /// Width of each V head when it is narrower than `head_dim` (MiMo-V2:
170    /// 128 against 192). V is zero-padded to `head_dim` inside the cache
171    /// and the attention output is compacted back to nh·v_head_dim before
172    /// o_proj (see `QwenAttnCfg::v_head_dim`). None = `head_dim`.
173    pub v_head_dim: Option<usize>,
174    /// `CMF_LAYER_DUMP=<dir>` (read once at construction; tests set it
175    /// directly): the hidden state after every layer, for every position,
176    /// as raw little-endian f32 files `p{pos:06}_l{li:02}.f32` of
177    /// `hidden_size` floats each. Written by the CPU layer walks — the
178    /// batched prefill (`prefill_batch_span`) and the single-token forward
179    /// (`forward_layers_span`) — so both prompt ingest and decode can be
180    /// diffed layer by layer against an external oracle (tools/mimo_ref).
181    /// Layers a device graph runs (wgpu token/batch graph, Metal chunk or
182    /// block graphs) and the own-stack families (DeepSeek-V4/V4.1,
183    /// Qwen3.8-Flash-Next, Gemma-3n) are not dumped: run with CMF_GPU=0
184    /// for a complete set. `li` is the virtual layer index. Final logits:
185    /// `CMF_LOGIT_DUMP=<file>` (hidden + logits of the first decode step).
186    pub layer_dump: Option<std::path::PathBuf>,
187    /// GPU-graph declines already logged for this pipeline, as (graph
188    /// site, reason) — one line each, see `graph_attn_decline_reason`.
189    graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
190    /// MiMo-V2 expert placement (prefix / dynamic bank / hybrid), decided
191    /// on the first forward — see `crate::mimo_moe`.
192    pub(crate) mimo_moe: crate::mimo_moe::Slot,
193    /// Opt-in MiniCPM q4tp expert cache for a graph-produced CPU tail.
194    /// It is deliberately separate from MiMo's placement policy because it
195    /// serves only the stock MiniCPM top-8 geometry and otherwise falls back
196    /// to the ordinary exact host MoE path.
197    pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
198    /// Linear-core geometry (present when the model has linear layers).
199    pub vmf_cfg: Option<VmfPhaseCfg>,
200    /// GatedDeltaNet geometry (faithful vendor operator).
201    pub gdn_cfg: Option<GdnCfg>,
202    /// MiniCPM-class logit scale (tied lm_head → cannot fold into weights).
203    pub logit_multiplier: Option<f32>,
204    /// Cooperative cancel: set from any thread (FFI `cortiq_cancel`,
205    /// a dropped server connection); the generate loop checks it at
206    /// every prefill chunk and decode step and finishes with
207    /// `finish_reason: "cancelled"`. Auto-cleared when honoured.
208    pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
209    /// A GPU graph failure is distinct from a user/request cancellation.
210    /// Graph code sets this before raising the cooperative cancel flag so the
211    /// generation API can return an error instead of reporting a successful
212    /// `finish_reason: cancelled` result.
213    graph_failed: std::sync::atomic::AtomicBool,
214    /// Resident-graph MoE expert banks, built once per layer
215    /// (`GraphMoeBank`). `CMF_GRAPH_MOE_BANK=0` rebuilds them every token.
216    graph_moe_banks: std::sync::Mutex<std::collections::HashMap<GraphMoeKey, GraphMoeBank>>,
217    /// Token ids currently materialized in the KV cache (the forwarded
218    /// prompt + all generated tokens except the last, which is sampled
219    /// but not yet forwarded). Lets the next generate call prefill only
220    /// the suffix when a chat app resends the whole history.
221    pub kv_history: Vec<u32>,
222    /// Owner tag of `kv_history`: the resident device graph held that
223    /// sequence (true) or the host did. Reuse continues only on the owner.
224    pub kv_history_device: bool,
225    /// KDA geometry (Kimi Linear / Kimi-K3) — shared by every Kda layer.
226    pub kda_cfg: Option<crate::linear_core::KdaCfg>,
227    /// Gemma-3n stack (AltUp/LAuReL/PLE/KV-sharing): its own forward —
228    /// weights.layers stays empty, the KV caches are the shared ones.
229    pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
230    /// Gemma-4 E-series stack (dual-geometry gemma4 + PLE + shared KV):
231    /// its own per-token forward, same contract as `g3n`.
232    pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
233    /// DeepSeek-V4 runs its own stack too: its hidden state is `hc_mult`
234    /// copies of a vector, so no loop written for a single residual
235    /// stream can carry it.
236    pub dsv4: Option<
237        Box<(
238            crate::dsv4::Dsv4Globals,
239            Vec<crate::dsv4::Dsv4Layer>,
240            crate::dsv4::Dsv4Cfg,
241            crate::dsv4::Dsv4State,
242        )>,
243    >,
244    /// DeepSeek-V4.1 owns the shared CED/CSA2 attention state, raw Engram
245    /// lookup and four-stream mHC handoff. It cannot use the V4 cache
246    /// layout, so it has a dedicated executor and state tuple.
247    pub dsv41: Option<
248        Box<(
249            crate::dsv41::Dsv41Globals,
250            Vec<crate::dsv41::Dsv41Layer>,
251            crate::dsv41::Dsv41Cfg,
252            crate::dsv41::Dsv41State,
253        )>,
254    >,
255    /// Optional V4.1 vision tower. Text-only files leave this unset.
256    pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
257    /// Prepared image rows consumed by the next V4.1 prefill.
258    dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
259    /// Qwen3.8-Flash-Next owns four residual streams plus QSA/PLE state;
260    /// the generic single-residual layer loop cannot represent it.
261    /// Block-diffusion generation (Clover): preview weights and the
262    /// decoding controls; `None` on every causal model.
263    pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
264    clover_stats: clover::CloverStats,
265    /// The causal decode's response penalties (`clover_causal_logits`).
266    clover_pen: clover::Penalties,
267    pub qwen4_exp: Option<
268        Box<(
269            crate::qwen4_exp::Globals,
270            Vec<crate::qwen4_exp::Layer>,
271            crate::qwen4_exp::Cfg,
272            crate::qwen4_exp::State,
273        )>,
274    >,
275    /// DeepSeek-V4's own speculation stack: three draft modules, each a full
276    /// layer, plus a confidence head on the last. Empty when the file has
277    /// none, which is the only signal the decode path needs.
278    pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
279    /// The draft's per-sequence state (KV rings, captured trunk hidden).
280    pub dspark: Option<crate::dsv4::DsparkState>,
281    /// Drafts awaiting their verdict: (position, proposals, still matching,
282    /// accepted so far).
283    pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
284    /// Accepted prefix length of every graded draft.
285    pub dspark_hist: Vec<usize>,
286    /// The real tokens the drafts were graded against — a degenerate,
287    /// repeating output would make any acceptance number meaningless, and
288    /// the cheapest guard against believing one is to count them.
289    pub dspark_real: Vec<u32>,
290    /// The trunk's expert picks for the last few tokens, per layer. The
291    /// union over a window of them is what a batched verify would have to
292    /// read, and the ratio to the pick count is all it could save.
293    pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
294    /// (unique, total) expert picks per draft, trunk side and draft side.
295    pub dspark_exp: Vec<(usize, usize, usize, usize)>,
296    /// Wall time spent in the deliberately out-of-core draft. Kept separate
297    /// from trunk decode so block batching can be judged without conflating
298    /// it with GPU chain variance.
299    pub dspark_draft_ns: u128,
300    /// Per-layer winners of the last pooled route — the admission predictor.
301    m3_winners: Vec<Vec<u32>>,
302    /// Route frequency per (layer, expert) — the bank's prewarm profile.
303    m3_counts: Vec<u32>,
304    m3_n_exp: usize,
305    m3_tokens: u64,
306    m3_prewarmed: bool,
307    /// LFM2 short-convolution geometry (present when the model has
308    /// `ShortConv` mixer layers).
309    pub short_conv_cfg: Option<ShortConvCfg>,
310    /// Multi-token-prediction head (None = absent).
311    pub mtp: Option<MtpModule>,
312    /// MiMo-V2's draft stack (three chained MTP layers from the
313    /// `<stem>.mtp.cmf` sidecar); None = absent. Speculative greedy decode
314    /// uses it unless `CMF_MTP=0` / `CMF_MIMO_MTP=0`.
315    pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
316    /// Set while the MiMo speculative verify runs `prefill_batch`: its MoE
317    /// layers take `moe_ffn_rows_exact` (each row bit-identical to decode).
318    verify_exact_moe: bool,
319    /// Speculative decode via MTP (greedy only; `CMF_MTP=0` disables).
320    pub speculative: bool,
321    /// Keep generating past end-of-sequence ids (the llama-bench contract
322    /// for a timed run). A loop flag, deliberately NOT a sampler
323    /// suppression: suppressed ids count as a penalty and switch the
324    /// speculative round and the greedy burst off, so a benchmark that
325    /// suppressed EOS never measured either.
326    pub ignore_eos: bool,
327    /// Draft-head shortlist guard: tokens left during which the draft
328    /// uses the FULL head because a recently committed id lay past the
329    /// `CMF_DRAFT_VOCAB` cut (Cyrillic and CJK ids sit above 131072 in
330    /// Qwen's table, so a prefix shortlist would draft nothing usable
331    /// there — measured on Russian prose: 2.9 → 1.6 accepted a round).
332    pub draft_full_streak: u32,
333    /// Adaptive draft depth for the speculative round (None until the
334    /// first round): grows while nearly every draft is accepted, shrinks
335    /// when fewer than half are. The verify's cost climbs with the rows on
336    /// a discrete card (RTX PRO 4000: 52 ms at 2 rows, 74 at 5, 80 at 6),
337    /// so prose wants k≈3 and code or the repetitive bench k≈5 — measured
338    /// 33.6 vs 27.6 tok/s on an essay at k=3 vs 5, 45.6 vs 38 on code.
339    /// `CMF_GRAPH_SPEC_K` pins it.
340    pub spec_k_adapt: Option<usize>,
341    /// EWMA of the accepted fraction that drives `spec_k_adapt`.
342    pub spec_acc_ewma: f32,
343    rng: SplitMix64,
344    sampler_scratch: SamplerScratch,
345    /// Speculative SAMPLING state (graph_spec_step, temperature > 0): the
346    /// correction token a rejected draft produced — committed by the loop
347    /// top in place of a fresh draw — and the per-round draft
348    /// distributions / target scratch, reused so a round allocates
349    /// nothing at the vocab size.
350    spec_forced: Option<u32>,
351    spec_q: Vec<Vec<f32>>,
352    spec_p: Vec<f32>,
353    spec_res: Vec<f32>,
354    /// The same three for the sparse chain (top-k configs).
355    spec_qs: Vec<sampler::Sparse>,
356    spec_ps: sampler::Sparse,
357    spec_ress: sampler::Sparse,
358    /// Which arm the MTP draft block runs on this generation: Some(true)
359    /// = the whole-token graph (device attention, one submit a step),
360    /// Some(false) = the per-op path; None = not decided yet. Decided
361    /// on the first draft and held, because the two arms keep the MTP
362    /// KV in different places (device mirror vs the CPU cache) and a
363    /// mid-run switch would read the wrong one.
364    mtp_graph_mode: Option<bool>,
365    /// The Metal verify graph of the round in flight, between its sync
366    /// (logits read) and the commit that replays the accepted prefix.
367    #[cfg(target_os = "macos")]
368    metal_verify: Option<MetalVerifyPending>,
369    /// Precomputed RoPE inverse frequencies [head_dim/2]. Arc: the
370    /// forward path clones a handle to escape the &mut self borrow —
371    /// cloning the table itself was a per-forward allocation.
372    pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
373    /// Reusable norm buffers for the decode hot path (roadmap §3 P0:
374    /// steady-state forward should not heap-allocate). Disjoint field
375    /// from `weights`/`kv_cache`, so split borrows keep working.
376    ws: ForwardScratch,
377    /// Persistent worker pool (None = serial; see CMF_THREADS).
378    pool: Option<std::sync::Arc<Pool>>,
379    // ── Dynamic per-token skill routing (spec §9, claim 14/16) ──
380    /// Source model, retained so a skill switch can re-resolve the
381    /// touched layers' FFN tensors (Mapped = mmap pointers, cheap).
382    pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
383    /// Masks present → weights are dequantized f32 (rebuild path).
384    pub(crate) dyn_force_f32: bool,
385    /// Per-skill FFN layers actually replaced (derived from tensors, not
386    /// the meta `layers` field — ru2 replaces down_proj in 0..23 while
387    /// its meta says [20..23]). None = skill touches non-FFN tensors →
388    /// ineligible for cheap dynamic switching (honest refusal).
389    pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
390    /// Currently overlaid skill (index into model.header.skills); None =
391    /// backbone. Set at load time to the statically-overlaid skill so
392    /// `set_active_skill(None)` correctly reverts it (else a static
393    /// skill would silently persist — the union-diff assumes dyn_active
394    /// always mirrors the live overlay). Switched by `set_active_skill`.
395    pub(crate) dyn_active: Option<usize>,
396    /// Pipeline was loaded with a soft blend (materialized working
397    /// tensors, not a single skill index) → dynamic routing refuses:
398    /// there is no single index to revert the blend from.
399    pub(crate) dyn_blend_loaded: bool,
400    /// Layer whose post-residual hidden feeds the router φ (shared by
401    /// swarm skills). None = φ capture off.
402    pub(crate) dyn_phi_layer: Option<usize>,
403    /// EMA of φ at `dyn_phi_layer` over the decode window (on-policy).
404    dyn_phi_ema: Vec<f32>,
405    dyn_phi_seen: usize,
406    /// Hysteresis router driving per-token skill switches during decode
407    /// (None = static/no dynamic routing). Taken out during generation.
408    pub dyn_router: Option<crate::swarm::DynRouter>,
409    /// O(1) Nyström attention setting (CLI/env/header-hint resolved by
410    /// the caller; None = plain cache attention everywhere).
411    o1_cfg: Option<crate::nystrom::O1Cfg>,
412    /// Bumped once per collecting→sealed transition — the GPU state mirror
413    /// re-uploads when it sees a new epoch (each fresh sealed state).
414    o1_epoch: u64,
415    /// Per-layer o1 flags derived from `o1_cfg` (Full layers only).
416    o1_flags: Vec<bool>,
417    /// Emit a structured per-token trace (B4 telemetry channel). Off by
418    /// default — the runtime is silent unless observation is requested.
419    trace: bool,
420    /// Confidence-calibration temperature (B1): reported probability is
421    /// softmax(logits / calib_temp). 1.0 = raw. Set from header.calibration.
422    calib_temp: f32,
423    /// Process-unique id keying this pipeline's device KV mirrors.
424    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
425    graph_kv_id: u64,
426    /// Decode asks the token graph to also run final-norm + lm_head on
427    /// the device (drops the separate per-op lm_head round trip).
428    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
429    graph_want_logits: bool,
430    /// NLL quality gates require the graph's fused head rather than silently
431    /// accepting a CPU head fallback. Generation keeps the historical
432    /// best-effort `graph_want_logits` behavior.
433    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
434    graph_head_required: bool,
435    /// Logits the graph produced for the token just forwarded (taken by
436    /// the decode loop; None = compute on the CPU path).
437    graph_logits: Option<Vec<f32>>,
438    /// Packed Embryo graph model, built lazily on the first eligible
439    /// resident call so other architectures pay no packing cost.
440    embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
441    /// This pipeline's token graph refused for a STRUCTURAL reason (its
442    /// own weights / layer kinds): not retried. Per pipeline, never
443    /// process-wide: a skill lane whose pack does not build must not flip
444    /// the backbone slot mid-sequence onto the host path (and a new lane
445    /// must not clear the backbone's verdict) — R4/NF-2.
446    graph_refused: std::sync::atomic::AtomicBool,
447    /// Token embeddings are multiplied by this at input (Gemma: √hidden).
448    pub embed_multiplier: f32,
449    /// Attention score scale (1/√head_dim unless the arch overrides —
450    /// Gemma's query_pre_attn_scalar).
451    pub attn_scale: f32,
452    /// Sliding-window attention: (window, every-Nth-layer-is-global
453    /// pattern) — Gemma-3.
454    pub swa: Option<(usize, usize)>,
455    /// Sliding-window tail trimming, `(slack, align)` for
456    /// `LayerKvCache::trim_window`: every layer with a window keeps only
457    /// the rows its window can still read (Spark-X2.5: 576..639 rows per
458    /// 512-window layer instead of the whole context — 27 of the 4B's 36
459    /// layers). Set at load for Spark-X2.5 only (`CMF_SWA_TRIM=0` turns
460    /// it off); None keeps every row, as every other model always has.
461    pub swa_trim: Option<(usize, usize)>,
462    /// Prompt end of the current turn: the tail trim keeps every row a
463    /// rollback to it still reads (`LayerKvCache::trim_window_anchored`),
464    /// so the next chat turn can drop this turn's answer.
465    swa_trim_anchor: usize,
466    /// Fewest prompt rows the Metal chunk prefill graph takes; a shorter
467    /// chunk walks the layers on the host. 32 since the graph landed
468    /// (measured on Qwen2.5-0.5B); Spark-X2.5/X2.6 take 8 (`loader`).
469    pub chunk_min_rows: usize,
470    /// Explicit local/global schedule for architectures that cannot be
471    /// represented by Gemma's every-Nth-global convention.
472    pub sliding_layers: Option<Vec<bool>>,
473    /// Per-layer flag: this layer runs WITHOUT RoPE. Recursive Flint applies
474    /// position encoding only on its sliding-window layers; its 14
475    /// full-attention layers have none. The rotary dim of such a layer is 0,
476    /// which is what the attention kernel reads.
477    pub no_rope_layers: Option<Vec<bool>>,
478    /// Natively bounded anchor record (`arch.anchor_core`): the file's
479    /// operator, installed once at load. `Some` = the model is
480    /// bounded-native — `--o1`/`CMF_O1*` are refused, prefix reuse and
481    /// the penalty window are bounded, and no anchor layer stores
482    /// anything per position.
483    pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
484    /// The `[W][rd/2]` relative-rotation table every bounded layer shares
485    /// (built from `inv_freq` once the RoPE setup is final).
486    bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
487    /// Bounded prefix-reuse key (length + rolling hash + tail) of a
488    /// bounded-native model; `kv_history` stays empty there so nothing
489    /// in the pipeline grows with the dialogue.
490    pub kv_prefix: KvPrefix,
491    /// Prompt positions the last `generate*` call actually forwarded
492    /// (prefix reuse subtracts what the cache already held).
493    pub last_prefill_tokens: usize,
494    /// RoPE table of the sliding (local) layers, when they use their
495    /// own base frequency (Gemma-3: 10k local vs 1M global).
496    pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
497    pub rotary_dim_local: Option<usize>,
498    pub rope_scale: f32,
499    pub rope_scale_local: f32,
500    /// Gemma-4: global layers run their own geometry — (head_dim,
501    /// num_kv_heads); sliding layers keep the base fields.
502    pub global_attn: Option<(usize, usize)>,
503    /// Gemma-4: the global layers' proportional RoPE table (len
504    /// global_head_dim/2, zero-padded tail = identity rotation).
505    pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
506    /// Scale-less RMS normalization of V heads before caching (Gemma-4).
507    pub attn_v_norm: bool,
508    /// HunYuan dense: per-head q/k norm runs after RoPE (see the arch flag).
509    pub qk_norm_after_rope: bool,
510    /// The per-head `self_attn.g_proj` output gate uses sigmoid
511    /// (Spark-X2.5) instead of softplus (Laguna).
512    pub proj_gate_sigmoid: bool,
513    /// Final-logit soft-capping C: logits = C·tanh(logits/C) (Gemma-4).
514    pub final_softcap: Option<f32>,
515    /// Cortiq Embryo hierarchical head: cluster matrix [C, hidden]. The
516    /// flat logits h·Eᵀ are turned into the two-level log-probabilities
517    /// log softmax_c(h·Cᵀ)[c(v)] + log softmax_{s∈c(v)}(h·E_c(v)ᵀ)[v].
518    pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
519    /// Gemma-2 attention-logit soft-capping (0.0 = off).
520    pub attn_softcap: f32,
521    /// Compute per-token confidence (a full-vocab softmax each
522    /// token). On by default; `bench --core` turns it off to match
523    /// llama-bench's core timing.
524    confidence_on: bool,
525    /// Test-only one-shot forward failure, scoped to this pipeline so
526    /// parallel scoring tests cannot consume one another's injection.
527    #[cfg(test)]
528    nll_test_fail_at: Option<usize>,
529    /// Test-only route override; avoids mutating the process-wide
530    /// `CMF_PREFILL` environment variable while forcing the serial path.
531    #[cfg(test)]
532    nll_test_force_serial: bool,
533}
534
535#[cfg(target_os = "macos")]
536impl Drop for Pipeline {
537    fn drop(&mut self) {
538        // the async replay writes into `kv_cache` Vecs about to be freed
539        let _ = crate::gpu_metal::wait_replay();
540        crate::gpu::kv_mirror_drop(self.graph_kv_id);
541    }
542}
543
544#[cfg(not(target_os = "macos"))]
545impl Drop for Pipeline {
546    fn drop(&mut self) {
547        // The wgpu resident Embryo graph owns recurrent/KV buffers keyed by
548        // this pipeline's sequence id.  Release that sequence image when a
549        // pooled pipeline is dropped; model weights stay cached for reuse.
550        crate::gpu::graph_kv_reset(self.graph_kv_id);
551    }
552}
553
554/// Model weights. Matrices are `QTensor` (owned f32 for small models
555/// and tests — bit-identical to the historical paths — or quantized
556/// bytes zero-copy from the CMF mmap for big models). 1-D norms are
557/// always small and stay f32.
558pub struct PipelineWeights {
559    /// Embedding table: [vocab_size, hidden_size]
560    pub embed_tokens: QTensor,
561    /// Per-layer weights
562    pub layers: Vec<LayerWeights>,
563    /// LM head: [vocab_size, hidden_size]
564    pub lm_head: QTensor,
565    /// Final norm: [hidden_size]
566    pub final_norm: Vec<f32>,
567}
568
569/// One transformer layer: shared norms + MLP, attention by kind.
570pub struct LayerWeights {
571    pub input_norm: Vec<f32>,
572    /// The pre-FFN norm (`post_attention_layernorm` classically;
573    /// `pre_feedforward_layernorm` on Gemma-2/3 sandwich layers).
574    pub post_norm: Vec<f32>,
575    /// Gemma-2/3 sandwich: norm applied to the ATTENTION OUTPUT before
576    /// its residual add (`post_attention_layernorm` there).
577    pub attn_out_norm: Option<Vec<f32>>,
578    /// Gemma-4: the whole layer output is multiplied by this scalar.
579    pub layer_scale: Option<f32>,
580    /// Gemma-2/3 sandwich: norm applied to the FFN OUTPUT before its
581    /// residual add (`post_feedforward_layernorm`).
582    pub ffn_out_norm: Option<Vec<f32>>,
583    pub ffn: FfnKind,
584    pub attn: AttnKind,
585}
586
587/// FFN gate activation: SiLU (SwiGLU family), tanh-GELU (Gemma's GeGLU),
588/// or a source-specific gated contract. A property of the model, carried on
589/// every FFN triple.
590#[derive(Clone, Copy, PartialEq, Debug, Default)]
591pub enum Act {
592    #[default]
593    Silu,
594    GeluTanh,
595    /// Exact erf GELU (HF `hidden_act = "gelu"`: Spark-X2.5).
596    Gelu,
597    /// Kimi-K3 SituAndMul: BOTH halves transform —
598    /// a = β·tanh(g/β)·σ(g), up' = linβ·tanh(u/linβ) (linβ>0), out = a·up'.
599    Situ {
600        beta: f32,
601        linear_beta: f32,
602    },
603    /// MiniMax M3's SwiGLU-OAI: upper-clamped gate, symmetrically clamped
604    /// up branch, `sigmoid(alpha * gate)`, and a `+1` up-branch offset.
605    /// It deliberately has no generic GPU graph arm until a matching kernel
606    /// exists; CPU uses the exact scalar reference in `inference`.
607    SwiGluOai {
608        alpha: f32,
609        limit: f32,
610    },
611}
612
613impl Act {
614    pub fn from_arch(name: &str) -> Self {
615        if name == "gelu_tanh" {
616            Self::GeluTanh
617        } else if name == "gelu" {
618            Self::Gelu
619        } else {
620            Self::Silu
621        }
622    }
623
624    /// Arch-driven constructor (activation name + situ betas).
625    pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
626        match arch.hidden_act.as_str() {
627            "situ" => Self::Situ {
628                beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
629                linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
630            },
631            // `swigluoai` alone is deliberately not a generic activation
632            // alias.  Its alpha/limit are part of MiniMax M3's checked
633            // source contract, and accepting that string on an unrelated
634            // architecture would silently impose M3 math on its FFN.
635            "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
636                alpha: 1.702,
637                limit: 7.0,
638            },
639            other => Self::from_arch(other),
640        }
641    }
642
643    /// Construct the MiniMax M3 SwiGLU-OAI activation from a checked model
644    /// contract. This is intentionally explicit rather than recognizing an
645    /// arbitrary `hidden_act` string: callers must validate `alpha` and
646    /// `limit` against their source architecture before enabling it.
647    #[inline]
648    pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
649        Self::SwiGluOai { alpha, limit }
650    }
651
652    #[inline]
653    pub fn apply(self, x: f32) -> f32 {
654        match self {
655            Self::Silu => inference::silu(x),
656            Self::GeluTanh => inference::gelu_tanh(x),
657            Self::Gelu => inference::gelu_erf(x),
658            Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
659            Self::SwiGluOai { alpha, limit } => {
660                let gate = x.min(limit);
661                gate * (1.0 / (1.0 + (-alpha * gate).exp()))
662            }
663        }
664    }
665
666    /// Gated combine — the FFN contract. Situ transforms the UP half
667    /// too, so callers must use this instead of apply(g)·u.
668    #[inline]
669    pub fn combine(self, g: f32, u: f32) -> f32 {
670        match self {
671            Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
672                self.apply(g) * (linear_beta * (u / linear_beta).tanh())
673            }
674            Self::SwiGluOai { alpha, limit } => {
675                inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
676            }
677            _ => self.apply(g) * u,
678        }
679    }
680
681    /// The wgpu graphs' arm for this activation; None = no device kernel
682    /// computes it, and the graph builder must refuse the layer (the dense
683    /// graph FFN once computed SiLU for whatever the model asked for).
684    pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
685        match self {
686            Self::Silu => Some(crate::gpu::GraphAct::Silu),
687            Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
688            Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
689            Self::Situ { .. } | Self::SwiGluOai { .. } => None,
690        }
691    }
692
693    /// The WGPU graph arm.  Keep this deliberately separate from
694    /// `graph_act`: OAI needs its dedicated code rather than a generic SiLU
695    /// fallback.  Only the frozen MiniMax-M3 source constants get a device
696    /// code; a future OAI-like architecture with different constants must add
697    /// and test its own kernel contract.
698    pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
699        self.moe_job_graph_contract().map(|(act, _)| act)
700    }
701
702    /// The exact device contract for a gate/up/down MoE job.  OAI is not a
703    /// generic "clamped SiLU": code 3 commits a backend to MiniMax M3's
704    /// alpha, asymmetric up transform, and frozen clamp.  Keep the clamp
705    /// alongside the marker so a backend cannot accidentally run code 3 with
706    /// a nearby model's limit.
707    pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
708        match self {
709            Self::SwiGluOai { alpha, limit }
710                if alpha.to_bits() == 1.702f32.to_bits()
711                    && limit.to_bits() == 7.0f32.to_bits() =>
712            {
713                Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
714            }
715            _ => self.graph_act().map(|act| (act, 0.0)),
716        }
717    }
718}
719
720/// Dense gated triple — the FFN of a dense layer or of one expert.
721pub struct DenseFfn {
722    pub gate_proj: QTensor,
723    pub up_proj: QTensor,
724    pub down_proj: QTensor,
725    /// Gate activation (SiLU default; Gemma: tanh-GELU).
726    pub act: Act,
727    /// `down_proj` stored transposed (`[inter, hidden]`), when the file
728    /// carries it. Only the per-token sparse path reads it: a neuron's
729    /// down weights are a contiguous ROW there, so the token's chosen
730    /// neurons are the only bytes touched. `None` = the ordinary layout,
731    /// and the sparse path stays off.
732    pub down_t: Option<QTensor>,
733    /// Task tubes (spec: defragged task-conditional width). The three
734    /// matrices above are the CORE — the neurons every task computes;
735    /// each tube is an independently quantized slice of the SAME layer
736    /// holding the neurons only some tasks need. A tube is a normal
737    /// tensor triple, so every kernel runs it unchanged, and the bytes
738    /// of an inactive tube are never read. Empty = ordinary dense FFN.
739    pub segs: Vec<FfnSeg>,
740}
741
742/// One task tube: a contiguous slice of a layer's FFN neurons, stored
743/// as its own `[w, hidden]` / `[hidden, w]` triple. `start` is the
744/// neuron's index in the layer's FULL space (core first, then tubes in
745/// order) — the bit a task mask sets to switch this tube on.
746pub struct FfnSeg {
747    pub gate: QTensor,
748    pub up: QTensor,
749    pub down: QTensor,
750    pub start: usize,
751    pub width: usize,
752}
753
754/// FFN operator of a layer, decided by tensor presence at load time
755/// (router `mlp.gate.weight` in the directory = MoE layer).
756pub enum FfnKind {
757    Dense(DenseFfn),
758    /// Mixture-of-Experts (Qwen2-MoE / Qwen3-MoE): softmax over ALL
759    /// expert logits → top-k, optional renorm; experts stay quantized
760    /// in mmap — only the selected ones are touched per token.
761    Moe(MoeFfn),
762    /// Gemma-4 MoE: a dense MLP branch AND a routed-expert branch in
763    /// the SAME layer, each with its own norm sandwich. The dense
764    /// branch reads the pre-FFN-normed input; the expert branch (and
765    /// the router) read the RAW residual through `pre_norm_2`:
766    ///   d = post_norm_1(dense(x̂));  m = post_norm_2(Σwₑ·FFNₑ(pre_norm_2(h)))
767    ///   ffn_out = d + m   (the caller's ffn_out_norm + residual follow)
768    DenseMoe(Box<DenseMoeFfn>),
769}
770
771/// Gemma-4 dual-branch FFN (see `FfnKind::DenseMoe`).
772pub struct DenseMoeFfn {
773    pub dense: DenseFfn,
774    pub moe: MoeFfn,
775    /// post_feedforward_layernorm_1 — dense-branch output norm.
776    pub post_norm_1: Vec<f32>,
777    /// pre_feedforward_layernorm_2 — expert-branch input norm (applied
778    /// to the RAW residual, not the pre-FFN-normed activation).
779    pub pre_norm_2: Vec<f32>,
780    /// post_feedforward_layernorm_2 — expert-branch output norm.
781    pub post_norm_2: Vec<f32>,
782}
783
784pub struct MoeFfn {
785    /// Router `mlp.gate.weight` [num_experts, hidden].
786    pub router: QTensor,
787    pub experts: Vec<DenseFfn>,
788    pub top_k: usize,
789    pub norm_topk_prob: bool,
790    /// Router scores per-expert with a sigmoid (LFM2-MoE / DeepSeek-V3
791    /// `noaux_tc`) instead of a softmax over all experts (Qwen).
792    pub router_sigmoid: bool,
793    /// Addend in the selected-score denominator of a sigmoid router.
794    /// Most supported checkpoints use the historical `1e-6` floor; Flint's
795    /// reference routing uses `1e-20`; MiniMax M3.1 preserves the same FP32
796    /// `sum + 1e-20` law. This is per-MoE rather than a global family switch
797    /// because the same sigmoid routing law has several contracts.
798    pub sigmoid_denom_eps: f32,
799    /// Per-expert selection bias `mlp.expert_bias` [num_experts]
800    /// (LFM2-MoE): added to the sigmoid scores for the top-k CHOICE only;
801    /// the gathered weights use the unbiased scores. None = no bias.
802    pub expert_bias: Option<Vec<f32>>,
803    /// Top-k weights are multiplied by this after the optional renorm
804    /// (LFM2-MoE `routed_scaling_factor`; 1.0 = off).
805    pub routed_scaling: f32,
806    /// Adaptive routing (CMF_MOE_TAU, opt-in): keep the smallest
807    /// prefix of the top-k whose renormalized mass reaches τ —
808    /// confident tokens touch 1–2 experts, flat ones keep all k.
809    /// MoE decode is memory-bound, so skipped experts are skipped
810    /// weight traffic. None = classic fixed top-k (bit-identical).
811    pub route_tau: Option<f32>,
812    /// Always-on shared expert. Qwen2-MoE carries an additional sigmoid
813    /// gate; Laguna adds the shared expert unconditionally (`None`).
814    pub shared: Option<(DenseFfn, Option<QTensor>)>,
815    /// Expert-selection counters (truncated Fisher B-field of claim 12:
816    /// routing frequency during calibration). Filled by every forward,
817    /// read by the CLI via CMF_MOE_STATS. RefCell: decode is single-threaded.
818    pub stats: std::cell::RefCell<Vec<u64>>,
819    /// Per-CHANNEL sum of squares of this FFN's input, accumulated over a
820    /// calibration run (`CMF_RMS_TRACE`). These are the RMS activation
821    /// traces AWNP needs: raw weight magnitude says every channel matters
822    /// equally, and the question AWNP asks is whether the ACTIVATIONS
823    /// disagree. Off unless the env var is set — an f64 add per channel
824    /// per token is cheap, but not free.
825    pub act_sq: std::cell::RefCell<Vec<f64>>,
826    /// Raw FFN-input rows captured for the layers named by `CMF_ACT_DUMP`
827    /// (`"9,19"`). AWNP is nullspace PROJECTION: after dropping channels the
828    /// survivors are refitted to absorb what was removed, and how much they
829    /// can absorb depends on the activation COVARIANCE, not on per-channel
830    /// RMS. Per-channel numbers can only bound the cost from above.
831    pub act_rows: std::cell::RefCell<Vec<f32>>,
832    /// Task mask over routed experts (DTG-MA over MoE, claim-12 B-field
833    /// applied): `false` experts are excluded from selection, the
834    /// softmax renormalizes over the allowed set. Built by the loader
835    /// from CMF_MOE_MASK=<stats.json> + CMF_MOE_MASK_COVER. None = all.
836    pub mask: Option<Vec<bool>>,
837    /// Gemma-4: per-expert weight scale applied AFTER the top-k renorm
838    /// (`router.per_expert_scale`). None = 1.0 everywhere.
839    pub per_expert_scale: Option<Vec<f32>>,
840    /// Gemma-4: the router reads a SCALE-LESS rms-norm of its input
841    /// (the constant gain router.scale·√hidden is folded into the
842    /// router weights at convert time).
843    pub router_input_norm: bool,
844    /// Cortiq Embryo: resonance routing (P1) — the "logits" are
845    /// bias_e − ‖(x−μ_e) − U_eᵀU_e(x−μ_e)‖², argmax = the expert whose
846    /// descriptor reconstructs the input best. `router` is a placeholder.
847    pub resonance: Option<Resonance>,
848    /// Growth records (`kind = "expert_append"`, spec §9.5.1) mounted
849    /// behind the trunk experts, one entry per grown expert IN THE ORDER
850    /// they sit in `experts` (the tail `experts[experts.len() - grown.len()..]`).
851    /// The trunk keeps `experts.len() - grown.len()` experts. Empty on a
852    /// gated MoE, on a file without records and under `CMF_GROWTH=off`.
853    pub grown: Vec<GrownExpert>,
854}
855
856/// One grown expert of an `expert_append` record as the loader mounted it.
857#[derive(Debug, Clone, PartialEq, Eq)]
858pub struct GrownExpert {
859    /// The record's skill id.
860    pub record: String,
861    /// Position of the record in `header.skills`.
862    pub record_index: usize,
863    pub layer: usize,
864    /// The index the tensor name declares (`experts.{e}` — the chain rule
865    /// of the format); the executed position may be smaller when an
866    /// earlier record of the layer is not mounted.
867    pub expert: usize,
868}
869
870/// Per-expert resonance descriptors of one MoE layer (`mlp.desc.*`).
871pub struct Resonance {
872    /// [E, hidden]
873    pub mu: Vec<f32>,
874    /// [E, k, hidden] orthonormal directions (k may be 0)
875    pub u: Vec<f32>,
876    pub k: usize,
877    /// [E] selection bias (loss-free balancing, trained online)
878    pub bias: Vec<f32>,
879    /// [E] reconstruction-error shell: an expert whose error
880    /// `‖(x−μ)⊥U‖² = d² − proj` exceeds its shell scores `−∞` (it never
881    /// wins). `+inf` = no shell — every trunk expert; a grown expert
882    /// (`expert_append`) carries the finite `desc.shell` its record stores.
883    /// Empty = no shell anywhere (legacy constructors).
884    pub shell: Vec<f32>,
885}
886
887/// `CMF_GROWTH_SHELL` state: 0 = not yet read from the environment, 1 = on,
888/// 2 = off. Process-wide, like the environment it mirrors.
889static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
890
891/// Is the growth shell applied (`Resonance::scores` −∞ rule, the resident
892/// graph's packed shell)? `CMF_GROWTH_SHELL=off` disables it for
893/// measurement; [`set_growth_shell`] overrides the environment in-process
894/// (`growth-eval --shell`). Default: on.
895pub fn growth_shell_enabled() -> bool {
896    use std::sync::atomic::Ordering;
897    match GROWTH_SHELL.load(Ordering::Relaxed) {
898        1 => true,
899        2 => false,
900        _ => {
901            let off = std::env::var("CMF_GROWTH_SHELL")
902                .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
903                .unwrap_or(false);
904            GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
905            !off
906        }
907    }
908}
909
910/// Switch the growth shell on/off for this process (`None` = re-read
911/// `CMF_GROWTH_SHELL` on the next query). A pipeline packed into the
912/// resident graph BEFORE the switch keeps the shell it was packed with —
913/// build a new pipeline after switching.
914pub fn set_growth_shell(on: Option<bool>) {
915    GROWTH_SHELL.store(
916        match on {
917            Some(true) => 1,
918            Some(false) => 2,
919            None => 0,
920        },
921        std::sync::atomic::Ordering::Relaxed,
922    );
923}
924
925impl Resonance {
926    /// Does any expert carry a finite shell (a mounted growth record)?
927    pub fn has_shell(&self) -> bool {
928        self.shell.iter().any(|s| s.is_finite())
929    }
930
931    /// The shell the runtime applies right now: the stored one, or all
932    /// `+inf` when the shell is switched off (`CMF_GROWTH_SHELL=off`).
933    pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
934        let mut out = vec![f32::INFINITY; ne];
935        if growth_shell_enabled() {
936            for (o, s) in out.iter_mut().zip(&self.shell) {
937                *o = *s;
938            }
939        }
940        out
941    }
942
943    /// Routing scores for one input row (higher = better). A grown
944    /// expert whose reconstruction error lies outside its shell gets
945    /// `−∞` (unless the shell is switched off); trunk rows are the exact
946    /// bit pattern they were before growth.
947    pub fn scores(&self, x: &[f32], out: &mut [f32]) {
948        let h = x.len();
949        let ne = out.len();
950        let shell_on = growth_shell_enabled() && !self.shell.is_empty();
951        for e in 0..ne {
952            let mu = &self.mu[e * h..(e + 1) * h];
953            let mut d2 = 0.0f32;
954            for j in 0..h {
955                let d = x[j] - mu[j];
956                d2 += d * d;
957            }
958            let mut proj = 0.0f32;
959            for i in 0..self.k {
960                let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
961                let mut p = 0.0f32;
962                for j in 0..h {
963                    p += (x[j] - mu[j]) * u[j];
964                }
965                proj += p * p;
966            }
967            let err = d2 - proj;
968            out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
969            if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
970                out[e] = f32::NEG_INFINITY;
971            }
972        }
973    }
974}
975
976/// Attention operator of a layer. Extension point: new operators are
977/// new variants here + a forward in their own module.
978pub enum AttnKind {
979    /// GQA softmax attention (+ optional Qwen3.5 qk-norm / output gate).
980    Full {
981        wq: QTensor,
982        wk: QTensor,
983        wv: QTensor,
984        wo: QTensor,
985        q_norm: Option<Vec<f32>>,
986        k_norm: Option<Vec<f32>>,
987        output_gate: bool,
988        /// Laguna: a separate softplus projection applied to the attention
989        /// output before O. The bool means one scalar per head (broadcast
990        /// across head_dim); false means one scalar per element.
991        softplus_gate: Option<(QTensor, bool)>,
992        /// Qwen2-family projection biases (q, k, v).
993        bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
994    },
995    /// Canonical linear core (VMF phase attention).
996    Linear(VmfPhaseWeights),
997    /// Faithful vendor linear operator (Qwen3.5 GatedDeltaNet).
998    LinearGdn(GdnWeights),
999    /// LFM2 gated short-convolution mixer (no KV cache; conv ring state
1000    /// lives in the layer's `linear_state`).
1001    ShortConv(ShortConvWeights),
1002    /// DeepSeek-V2 Multi-head Latent Attention. v1 executes it as
1003    /// expand-to-MHA: the latent is projected per token, K/V expand to
1004    /// every head and live in the ordinary cache (K head layout
1005    /// [rope | nope] so the standard partial rotary covers the shared
1006    /// rope key; V rows are zero-padded to the K head_dim and the pad
1007    /// is sliced off before O). Latent-resident cache is a later
1008    /// optimization, not a semantic change.
1009    Mla(Box<MlaWeights>),
1010    /// Kimi Delta Attention (Kimi Linear / Kimi-K3): per-channel decayed
1011    /// delta rule, separate q/k/v short convs, sigmoid-gated output norm.
1012    /// State lives in the layer's `linear_state` (no KV cache).
1013    Kda(Box<crate::linear_core::KdaWeights>),
1014    /// Natively bounded softmax anchor `swa_sink_v1` (Embryo-O1): ring of
1015    /// the last W raw keys with relative RoPE + trained NoPE sinks, one
1016    /// softmax. State is the fixed-size ring in `LayerKvCache::bounded`;
1017    /// nothing is stored per position (see `crate::bounded`).
1018    Bounded(Box<crate::bounded::BoundedWeights>),
1019}
1020
1021/// DeepSeek-V2 MLA projections (see `AttnKind::Mla`).
1022pub struct MlaWeights {
1023    /// `[nh·(rope+nope), hidden]` (or `[…, q_lora]` when compressed) —
1024    /// the converter permutes each head rope-first so rotary_dim =
1025    /// qk_rope works unchanged.
1026    pub q_proj: QTensor,
1027    /// Compressed q (K3/V3 class): x → q_a `[q_lora, hidden]` →
1028    /// rms(q_a_norm) → q_proj (= q_b). None = direct q (V2-Lite).
1029    pub q_a: Option<QTensor>,
1030    pub q_a_norm: Option<Vec<f32>>,
1031    /// `kv_a_proj_with_mqa` `[lora + rope, hidden]` (latent first).
1032    pub kv_a: QTensor,
1033    /// RMS-norm weights over the latent (`kv_a_layernorm`, [lora]).
1034    pub kv_a_norm: Vec<f32>,
1035    /// `[nh·(nope+v), lora]` — per head [k_nope | v].
1036    pub kv_b: QTensor,
1037    /// `[hidden, nh·v]`.
1038    pub o_proj: QTensor,
1039    pub nh: usize,
1040    pub qk_rope: usize,
1041    pub qk_nope: usize,
1042    pub v_dim: usize,
1043    pub lora: usize,
1044    /// Softmax scale (1/√(rope+nope), YaRN-mscale-corrected at load).
1045    pub scale: f32,
1046    /// Kimi Linear NoPE: skip the rotary entirely (layout unchanged).
1047    pub nope: bool,
1048}
1049
1050/// Multi-token-prediction head (DeepSeek/Qwen style, spec §2.1):
1051/// `x = eh_proj·[enorm(embed(next)); hnorm(hidden)]` → one transformer
1052/// block over its own KV → shared lm_head. Drafts the token after next;
1053/// the main model verifies, so output is exact — MTP only buys speed.
1054pub struct MtpModule {
1055    pub enorm: Vec<f32>,
1056    pub hnorm: Vec<f32>,
1057    /// [hidden, 2·hidden]
1058    pub eh_proj: QTensor,
1059    pub layer: LayerWeights,
1060    pub final_norm: Vec<f32>,
1061    pub kv: crate::kv_cache::LayerKvCache,
1062}
1063
1064/// Teacher-forced activation statistics for one MTP draft FFN.
1065///
1066/// `mass[i]` is `Σ_rows |activation_i|`; `squared_mass[i]` is
1067/// `Σ_rows activation_i²`, where `activation = act(gate) · up` immediately
1068/// before the draft layer's down projection. `observed_positions` is the
1069/// number of teacher-forced rows that contributed to both arrays.
1070///
1071/// Rows of FFN input the canonical draft probe measures in one batch.
1072const MTP_PROBE_FFN_ROWS: usize = 128;
1073
1074/// [`Pipeline::probe_mtp_ffn`] returns one of these for each actual draft
1075/// layer.  It intentionally has a separate type/API from
1076/// [`Pipeline::probe_ffn_mass`] so a compactor cannot accidentally substitute
1077/// trunk statistics for an MTP sidecar whose FFN has a different width.
1078#[derive(Clone, Debug, PartialEq)]
1079pub struct MtpFfnProbe {
1080    pub mass: Vec<f64>,
1081    pub squared_mass: Vec<f64>,
1082    pub observed_positions: usize,
1083}
1084
1085/// A Metal verify graph after its sync: what the commit needs — the
1086/// graph (per-layer replay scratch), the GDN layers in encode order (their
1087/// CPU states receive the replay), and the attention layers with the CPU
1088/// row count they were encoded against (the accepted rows are pulled from
1089/// the mirror from there).
1090/// One item of the Metal rows-graph plan.
1091#[cfg(target_os = "macos")]
1092enum MetalRowsItem<'a> {
1093    Gdn {
1094        run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1095        first: usize,
1096    },
1097    Attn {
1098        l: crate::gpu_metal::AttnGpuLayer<'a>,
1099        li: usize,
1100        q_norm: Option<&'a [f32]>,
1101        k_norm: Option<&'a [f32]>,
1102        output_gate: bool,
1103    },
1104}
1105
1106#[cfg(target_os = "macos")]
1107struct MetalVerifyPending {
1108    graph: crate::gpu_metal::VerifyGraph,
1109    gdn_layers: Vec<usize>,
1110    attn_layers: Vec<(usize, usize)>,
1111}
1112
1113/// A round's batched MTP warm-up, submitted but not yet waited
1114/// (`mtp_warm_batch_submit` → `mtp_warm_batch_finish`): the trunk commit's
1115/// GDN replay is queued between the two.
1116#[cfg(target_os = "macos")]
1117struct MetalWarmPending {
1118    graph: crate::gpu_metal::VerifyGraph,
1119    cpu_stored: usize,
1120    b: usize,
1121}
1122
1123#[cfg(target_os = "macos")]
1124enum MetalRowsRun {
1125    /// Capability/preflight refusal before a command buffer was committed.
1126    Declined,
1127    /// A graph was admitted and then failed; callers must clear the sequence
1128    /// rather than replaying it through CPU/serial state.
1129    Failed,
1130    Completed(MetalVerifyPending),
1131}
1132
1133#[cfg(target_os = "macos")]
1134enum MetalPrefillOutcome {
1135    Declined,
1136    Failed,
1137    Completed(Vec<f32>),
1138}
1139
1140#[cfg(target_os = "macos")]
1141enum MetalBatchNllOutcome {
1142    Declined,
1143    Failed(String),
1144    Completed(f64, usize),
1145}
1146
1147/// The speculation trial's phases (see the decode loop): up to five timed
1148/// speculative rounds, eight timed plain tokens, then the faster arm until a
1149/// re-check. Two consecutive rounds with no accepted draft skip directly to
1150/// the plain measurement: another speculative round cannot pay when it emits
1151/// exactly the one token the plain loop would have produced.
1152#[derive(Clone, Copy)]
1153enum SpecTrial {
1154    Spec {
1155        t0: std::time::Instant,
1156        gen0: usize,
1157        rounds: usize,
1158    },
1159    Plain {
1160        t0: std::time::Instant,
1161        gen0: usize,
1162    },
1163    Decided {
1164        spec: bool,
1165        recheck_at: usize,
1166    },
1167}
1168
1169/// `CMF_GRAPH_SPEC_TIME`: 0 = off, 1 = one line per speculative round
1170/// plus the host stamps of any OUTLIER round (wall > 1.4× the running
1171/// median), 2 = the host stamps of every round.
1172pub(crate) fn spec_time_level() -> u8 {
1173    static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1174    *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1175        Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1176        Err(_) => 0,
1177    })
1178}
1179
1180/// `GraphMoeBank` reuse across tokens (`CMF_GRAPH_MOE_BANK=0` rebuilds
1181/// every layer's bank on every token, the route before the cache).
1182fn graph_moe_bank_on() -> bool {
1183    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1184    *ON.get_or_init(|| std::env::var("CMF_GRAPH_MOE_BANK").as_deref() != Ok("0"))
1185}
1186
1187/// Decide whether the batched speculative verifier is admitted by the
1188/// user/backend policy, before capability checks.  Native Metal's row-batch
1189/// verifier is deliberately fail-closed by default: it has produced a
1190/// different *emitted* greedy token than the serial path on a real model.
1191/// It remains available only through the documented explicit experiment
1192/// spelling, `CMF_GRAPH_SPEC=1`; other backends retain their historical
1193/// nonzero override behaviour.
1194fn graph_spec_wanted_by_policy(
1195    spec_env: Option<&str>,
1196    spec_default_ok: bool,
1197    penalized: bool,
1198    metal_wgpu: bool,
1199    native_metal: bool,
1200) -> bool {
1201    match spec_env {
1202        Some("0") => false,
1203        // Keep the native path genuinely explicit.  In particular, a
1204        // typo/nonzero value must not silently re-enable an unqualified
1205        // verifier just because historical non-Metal parsing treated it as
1206        // force-on.
1207        Some("1") => true,
1208        Some(_) => !native_metal,
1209        // The discrete/wgpu default stays as it was; only native Metal has
1210        // a quality gate here.
1211        None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1212    }
1213}
1214
1215/// The round's host stamps: `spec_stamp(name)` records the time since
1216/// the previous stamp (the section that just ended) — from anywhere on
1217/// the round's call chain (the Metal verify, the draft step, the commit),
1218/// no plumbing. Off (a single atomic load) unless `CMF_GRAPH_SPEC_TIME`
1219/// is set; one decode thread at a time is assumed (diagnostics).
1220struct SpecStampLog {
1221    t_last: std::time::Instant,
1222    items: Vec<(&'static str, f32)>,
1223}
1224
1225static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1226
1227pub(crate) fn spec_stamp(name: &'static str) {
1228    if spec_time_level() == 0 {
1229        return;
1230    }
1231    if let Ok(mut g) = SPEC_STAMPS.lock() {
1232        if let Some(log) = g.as_mut() {
1233            let now = std::time::Instant::now();
1234            log.items
1235                .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1236            log.t_last = now;
1237        }
1238    }
1239}
1240
1241fn spec_stamps_begin() {
1242    if spec_time_level() == 0 {
1243        return;
1244    }
1245    if let Ok(mut g) = SPEC_STAMPS.lock() {
1246        *g = Some(SpecStampLog {
1247            t_last: std::time::Instant::now(),
1248            items: Vec::with_capacity(64),
1249        });
1250    }
1251}
1252
1253fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1254    SPEC_STAMPS
1255        .lock()
1256        .ok()
1257        .and_then(|mut g| g.take())
1258        .map(|l| l.items)
1259        .unwrap_or_default()
1260}
1261
1262/// One line: every stamp name in first-seen order with its total over the
1263/// round and, when it fired more than once (the draft steps), the count.
1264fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1265    let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1266    for &(n, ms) in items {
1267        match agg.iter_mut().find(|e| e.0 == n) {
1268            Some(e) => {
1269                e.1 += ms;
1270                e.2 += 1;
1271            }
1272            None => agg.push((n, ms, 1)),
1273        }
1274    }
1275    let mut s = String::with_capacity(agg.len() * 16);
1276    for (n, ms, k) in agg {
1277        if k > 1 {
1278            s.push_str(&format!("{n} {ms:.1}/{k} "));
1279        } else {
1280            s.push_str(&format!("{n} {ms:.1} "));
1281        }
1282    }
1283    s
1284}
1285
1286/// The speculation monitor: exponential averages of a round's wall time
1287/// and of the tokens it produced, and the plain token's wall time — the
1288/// three numbers the keep/stop rule needs. A round pays when
1289/// `tokens_per_round · plain_ms > round_ms · 1.03`. The one-shot trial
1290/// (four rounds against eight tokens) mis-called prose: the first rounds
1291/// after a prompt are formulaic and accept well, the body does not (an
1292/// essay measured 39 against a plain 44.8 with the trial saying
1293/// "speculate"), so the rule now runs on EVERY round and stops after four
1294/// consecutive losing rounds; a stopped speculation is retried 128 tokens
1295/// later.
1296///
1297/// Native Metal (`metal: true`) does not pay the eight plain tokens up
1298/// front: on the 27B a plain token is ~150 ms, so the trial alone cost
1299/// ~1.2 s of every answer. There the plain phase is (a) skipped while the
1300/// rounds land at least `SPEC_PROXY_TOKENS` tokens each — a k=7 round on
1301/// Metal costs ~1.9 plain tokens (286 against 148 ms measured on the M4),
1302/// so 3.5 tokens/round cannot lose on any Metal round/plain ratio seen —
1303/// and (b) otherwise bounded to the fewest tokens that time it: two, or
1304/// as many as fit in `SPEC_PLAIN_MIN_MS` (a 150-ms token measures itself;
1305/// a 10-ms one needs the eight). The keep/stop rule itself is unchanged:
1306/// the moment a plain rate exists, it decides.
1307#[derive(Default, Clone, Copy)]
1308struct SpecMon {
1309    round_ms: f64,
1310    tokens: f64,
1311    plain_ms: f64,
1312    n: u32,
1313    fails: u32,
1314    /// Consecutive rounds that produced only their verifier-tip token (zero
1315    /// accepted MTP drafts). This is deliberately counted before the warm-up
1316    /// round is excluded from timing: two real zero-acceptance rounds are
1317    /// enough to stop charging an obviously losing speculative arm.
1318    zero_accept_rounds: u8,
1319    metal: bool,
1320}
1321
1322/// Tokens per round at or above which a Metal round pays without a plain
1323/// measurement (see `SpecMon`).
1324const SPEC_PROXY_TOKENS: f64 = 3.5;
1325/// The Metal plain phase: at least two tokens, and more until this much
1326/// wall time has been timed (up to the eight the other backends time).
1327const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1328
1329impl SpecMon {
1330    fn round(&mut self, dt_ms: f64, produced: usize) {
1331        if produced <= 1 {
1332            self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1333        } else {
1334            self.zero_accept_rounds = 0;
1335        }
1336        self.n += 1;
1337        if self.n == 1 {
1338            return; // round 1 pays the batch scratch and the draft mirror
1339        }
1340        let a = if self.n == 2 { 1.0 } else { 0.3 };
1341        self.round_ms += a * (dt_ms - self.round_ms);
1342        self.tokens += a * (produced as f64 - self.tokens);
1343    }
1344    fn pays(&self) -> bool {
1345        if self.plain_ms > 0.0 {
1346            self.tokens * self.plain_ms > self.round_ms * 1.03
1347        } else {
1348            self.metal && self.tokens >= SPEC_PROXY_TOKENS
1349        }
1350    }
1351    fn repeated_zero_acceptance(&self) -> bool {
1352        self.zero_accept_rounds >= 2
1353    }
1354    /// Has the plain phase timed enough tokens to decide?
1355    fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1356        let n = generated.saturating_sub(gen0);
1357        if n >= 8 {
1358            return true;
1359        }
1360        self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1361    }
1362}
1363
1364/// Ids of the consumed prefix the bounded reuse key remembers literally
1365/// (the rest is covered by the rolling hash).
1366pub const KV_PREFIX_TAIL: usize = 128;
1367
1368/// Bounded prefix-reuse key: how many ids the cache holds, a rolling
1369/// hash of ALL of them and the last [`KV_PREFIX_TAIL`] ids literally.
1370/// Answers "does this prompt strictly extend what is cached" exactly
1371/// (hash over the whole consumed prefix + literal tail) without keeping
1372/// the dialogue — the record is a fixed size whatever the session length.
1373#[derive(Debug, Clone, Default)]
1374pub struct KvPrefix {
1375    len: usize,
1376    hash: u64,
1377    tail: Vec<u32>,
1378    /// Owner of the state this key describes: the resident device graph
1379    /// (true) or the host. A turn continues the prefix only on its owner
1380    /// (R4: a host continuation of a device sequence reads an empty host
1381    /// state; a device continuation of a host sequence has no image).
1382    device: bool,
1383}
1384
1385impl KvPrefix {
1386    #[inline]
1387    fn fold(mut h: u64, ids: &[u32]) -> u64 {
1388        for &id in ids {
1389            h ^= id as u64;
1390            h = h.wrapping_mul(0x100000001b3);
1391            h ^= h >> 29;
1392        }
1393        h
1394    }
1395
1396    pub fn clear(&mut self) {
1397        self.len = 0;
1398        self.hash = 0xcbf29ce484222325;
1399        self.tail.clear();
1400        self.device = false;
1401    }
1402
1403    /// Was the prefix built on the resident device graph?
1404    pub fn on_device(&self) -> bool {
1405        self.device
1406    }
1407
1408    /// Tag the owner of the recorded prefix.
1409    pub fn set_on_device(&mut self, device: bool) {
1410        self.device = device;
1411    }
1412
1413    /// Ids the cache holds (the forwarded prefix).
1414    pub fn len(&self) -> usize {
1415        self.len
1416    }
1417
1418    pub fn is_empty(&self) -> bool {
1419        self.len == 0
1420    }
1421
1422    /// Literal tail currently kept (≤ `KV_PREFIX_TAIL`).
1423    pub fn tail_len(&self) -> usize {
1424        self.tail.len()
1425    }
1426
1427    /// Replace the key with `ids` (a fresh sequence).
1428    pub fn set(&mut self, ids: &[u32]) {
1429        self.clear();
1430        self.extend(ids);
1431    }
1432
1433    /// Append `more` to the consumed prefix (an extension-only turn).
1434    pub fn extend(&mut self, more: &[u32]) {
1435        if self.len == 0 && self.hash == 0 {
1436            self.hash = 0xcbf29ce484222325;
1437        }
1438        self.hash = Self::fold(self.hash, more);
1439        self.len += more.len();
1440        if more.len() >= KV_PREFIX_TAIL {
1441            self.tail.clear();
1442            self.tail
1443                .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1444        } else {
1445            let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1446            self.tail.drain(..drop);
1447            self.tail.extend_from_slice(more);
1448        }
1449    }
1450
1451    /// Cached positions when `ids` strictly extends the consumed prefix,
1452    /// 0 otherwise. The tail is compared literally first (cheap), then
1453    /// the hash over the whole prefix must agree.
1454    pub fn extension(&self, ids: &[u32]) -> usize {
1455        if self.len == 0 || ids.len() <= self.len {
1456            return 0;
1457        }
1458        let t = self.tail.len();
1459        if ids[self.len - t..self.len] != self.tail[..] {
1460            return 0;
1461        }
1462        if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1463            return 0;
1464        }
1465        self.len
1466    }
1467}
1468
1469/// Result of a generation call.
1470pub struct GenerateResult {
1471    pub text: String,
1472    pub token_ids: Vec<u32>,
1473    pub prompt_tokens: usize,
1474    pub tokens_generated: usize,
1475    pub finish_reason: String,
1476    /// Speculative-decode stats (0/0 when MTP is absent or inactive).
1477    pub mtp_drafted: usize,
1478    pub mtp_accepted: usize,
1479    /// Per-generated-token confidence = softmax probability of the token
1480    /// that was actually emitted (softmax probability on the chosen state). High =
1481    /// the model was sure; low = it was guessing. Same length as the
1482    /// generated slice of `token_ids`.
1483    pub token_confidence: Vec<f32>,
1484    /// Structured per-token telemetry (B4 channel). Empty unless
1485    /// `set_trace(true)`; otherwise same length as the generated slice.
1486    pub traces: Vec<TokenTrace>,
1487}
1488
1489/// One row of the structured telemetry trace (B4): the model's internal
1490/// routing state at the moment a token was emitted. Every field is a
1491/// quantity the runtime already computes — nothing is inferred or
1492/// estimated (anti-principle: only measured bytes).
1493#[derive(Clone, Debug)]
1494pub struct TokenTrace {
1495    /// 0-based index within the generated slice.
1496    pub t: usize,
1497    /// The emitted token id.
1498    pub token_id: u32,
1499    /// Softmax probability on the emitted token — how sure the model was.
1500    pub confidence: f32,
1501    /// Skill in force while this token was generated (None = backbone).
1502    pub active_skill: Option<String>,
1503    /// Recon error E = ‖r−BBᵀr‖²/‖φ‖² at the last routing eval — coherence
1504    /// with the active skill's subspace (low = coherent). None = no router
1505    /// or not yet evaluated.
1506    pub recon: Option<f32>,
1507    /// The router changed the active skill right after this token (a
1508    /// domain boundary crossed under the hysteresis barrier).
1509    pub switched: bool,
1510}
1511
1512/// Calibrated softmax probability of `id` under `logits` (the confidence on
1513/// the emitted token) — the confidence signal, cheap from logits already
1514/// computed for sampling. `temp` is the calibration temperature (B1):
1515/// softmax(logits / temp); 1.0 = raw.
1516#[cfg_attr(not(test), allow(dead_code))]
1517fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1518    let t = if temp > 1e-3 { temp } else { 1.0 };
1519    let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1520    let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1521    if sum > 0.0 {
1522        (((logits[id as usize] - max) / t).exp()) / sum
1523    } else {
1524        0.0
1525    }
1526}
1527
1528/// prefill-GEMM enabled? (CMF_PREFILL=seq — emergency fallback to the
1529/// sequential path.)
1530fn prefill_batched() -> bool {
1531    std::env::var("CMF_PREFILL")
1532        .map(|v| v != "seq")
1533        .unwrap_or(true)
1534}
1535
1536/// Decide the graph NLL route without conflating graph quality with the
1537/// optional native-Metal fused head. A hidden-state graph remains a valid
1538/// quality route on Vulkan/Wgpu; only native Metal requires graph logits.
1539#[inline]
1540/// ‖a − b‖ / ‖b‖ (infinite when the lengths differ).
1541fn rel_l2(a: &[f32], b: &[f32]) -> f64 {
1542    if a.len() != b.len() {
1543        return f64::INFINITY;
1544    }
1545    let (mut d, mut r) = (0f64, 0f64);
1546    for (&x, &y) in a.iter().zip(b) {
1547        let e = (x - y) as f64;
1548        d += e * e;
1549        r += y as f64 * y as f64;
1550    }
1551    (d / r.max(1e-30)).sqrt()
1552}
1553
1554fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1555    let graph_quality = unmasked && prefer_graph;
1556    let fused_head_quality = graph_quality && native_metal;
1557    (graph_quality, fused_head_quality)
1558}
1559
1560/// Input to the layer-major batched span walk: token ids (embeds itself,
1561/// full-stack and coordinator prefill) or ready boundary hiddens (the
1562/// network worker's side of a split).
1563#[derive(Clone, Copy)]
1564enum PrefillIn<'a> {
1565    Ids(&'a [u32]),
1566    Hidden(&'a [f32]),
1567}
1568
1569/// The batched prefill walks `weights.layers`. Architectures that load
1570/// their own stack (gemma-3n's AltUp replicas, DeepSeek-V4's hyper-
1571/// connections) leave that empty and must go position by position — asking
1572/// otherwise indexes an empty vector, which is a panic rather than a
1573/// fallback. Every call site goes through here so the next such
1574/// architecture is one line, not four.
1575impl Pipeline {
1576    pub(crate) fn can_prefill_batched(&self) -> bool {
1577        #[cfg(test)]
1578        let force_serial = self.nll_test_force_serial;
1579        #[cfg(not(test))]
1580        let force_serial = false;
1581        prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1582    }
1583
1584    /// The backend's automatic capacity split for a mapped transformer.
1585    /// Kept as a method so prefill and decode use the exact same boundary.
1586    fn automatic_gpu_prefix(&self) -> Option<usize> {
1587        let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1588        crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1589    }
1590
1591    /// Positions per batched pass of the layer-stack prefill for THIS
1592    /// model on THIS backend (see [`prefill_chunk_rule`]). Pub: the network
1593    /// split must chunk exactly like the local path to reproduce it.
1594    pub fn prefill_chunk(&self) -> usize {
1595        let env = env_prefill_chunk();
1596        if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1597            return prefill_chunk_rule(env, ChunkHost::here(), false);
1598        }
1599        prefill_chunk_rule(
1600            None,
1601            ChunkHost::Other,
1602            self.chunk_stack_facts().dense_on_discrete(),
1603        )
1604    }
1605
1606    /// Architectures that run a dedicated per-token stack (gemma-3n's
1607    /// AltUp flow, the gemma-4 E-series PLE/shared-KV body): batched
1608    /// prefill and the graphs stay off for them.
1609    fn per_token_stack(&self) -> bool {
1610        self.g3n.is_some() || self.g4e.is_some()
1611    }
1612
1613    fn chunk_stack_facts(&self) -> ChunkStackFacts {
1614        let plain_dense = !self.weights.layers.is_empty()
1615            && !self.per_token_stack()
1616            && self.dsv4.is_none()
1617            && self.dsv41.is_none()
1618            && self.qwen4_exp.is_none()
1619            && self.weights.layers.iter().all(|lw| {
1620                // LFM2/LFM3's short-conv mixer is two projection GEMMs and
1621                // an elementwise conv: the host-driven chunk walk starves
1622                // the card the same way (LFM3-1.7B q8_2f on an A40, 2048
1623                // positions: 137 tok/s at 48, 460-640 at 512).
1624                matches!(lw.attn, AttnKind::Full { .. } | AttnKind::ShortConv(_))
1625                    && matches!(lw.ffn, FfnKind::Dense(_))
1626            });
1627        let gpu_on = crate::gpu::enabled();
1628        ChunkStackFacts {
1629            plain_dense,
1630            discrete: gpu_on && crate::gpu::discrete(),
1631            gpu_on,
1632            // Only asked when the rest already qualifies: it opens the
1633            // backend's capacity plan.
1634            capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1635                || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1636            multi_gpu: self.gpu_plan.is_some(),
1637            o1: self.o1_active(),
1638        }
1639    }
1640}
1641
1642/// Prefill chunk (positions per batched pass), model-agnostic form. On
1643/// macOS the AMX GEMM path wants tall panels — M=48 starves the matrix
1644/// units (ggml uses ubatch 512); elsewhere the historical 48 stays.
1645/// CMF_PREFILL_CHUNK overrides. The architectures with their own stacks
1646/// (DeepSeek-V4/V4.1) chunk with this; the layer-stack prefill asks
1647/// [`Pipeline::prefill_chunk`], which also knows the model and the card.
1648/// A different chunk is a different (equally valid) generation: panel
1649/// width reorders float accumulation.
1650pub fn prefill_chunk() -> usize {
1651    prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1652}
1653
1654fn env_prefill_chunk() -> Option<usize> {
1655    std::env::var("CMF_PREFILL_CHUNK")
1656        .ok()
1657        .and_then(|v| v.parse::<usize>().ok())
1658}
1659
1660/// The host classes the chunk width distinguishes.
1661#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1662enum ChunkHost {
1663    Macos,
1664    /// Linux/Android aarch64 (phones, SBCs).
1665    Aarch64,
1666    /// Everything else: x86-64 Linux/Windows, CPU or Vulkan/DX12.
1667    Other,
1668}
1669
1670impl ChunkHost {
1671    fn here() -> Self {
1672        if cfg!(target_os = "macos") {
1673            ChunkHost::Macos
1674        } else if cfg!(target_arch = "aarch64") {
1675            ChunkHost::Aarch64
1676        } else {
1677            ChunkHost::Other
1678        }
1679    }
1680}
1681
1682/// Chunk for a plain dense stack whose every layer lives on a discrete
1683/// card. On x86 the layer-stack prefill is host-driven: each GEMM and the
1684/// chunk attention (which re-uploads the whole KV prefix per layer) is a
1685/// separate submit + readback, so 48 positions a pass left the card idle
1686/// between them. Measured in-process on an RTX 3090 (Vulkan), 2048-token
1687/// prompt — see CHANGELOG 0.7.6 for the table.
1688const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1689
1690/// The chunk-width rule. `dense_on_discrete` is true only for a plain
1691/// dense transformer (full attention, dense FFN, no special stack) that
1692/// is entirely resident on one discrete card — the one case measured
1693/// here. GDN hybrids, MoE, DeepSeek stacks, capacity-split and CPU-only
1694/// runs keep the width they were tuned with.
1695fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1696    if let Some(n) = env {
1697        return n.max(1);
1698    }
1699    match host {
1700        ChunkHost::Macos => 512,
1701        // Mobile: big enough to feed the batched attend (gate b ≥ 32)
1702        // and the blocked SDOT GEMM without the memory of 512.
1703        ChunkHost::Aarch64 => 256,
1704        ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1705        ChunkHost::Other => 48,
1706    }
1707}
1708
1709/// What the chunk rule needs to know about a loaded stack.
1710#[derive(Clone, Copy, Debug, Default)]
1711struct ChunkStackFacts {
1712    /// Every layer is `AttnKind::Full` or `AttnKind::ShortConv` +
1713    /// `FfnKind::Dense`, and no architecture-owned stack (g3n,
1714    /// DeepSeek-V4/V4.1, qwen4-exp) is set.
1715    plain_dense: bool,
1716    /// The active GPU backend is a discrete card.
1717    discrete: bool,
1718    /// The backend is up and not paused.
1719    gpu_on: bool,
1720    /// A capacity-derived device prefix: some layers run on the host.
1721    capacity_split: bool,
1722    /// An in-process multi-GPU plan is set.
1723    multi_gpu: bool,
1724    /// O(1) layers (their Q trace is recorded by the prefill).
1725    o1: bool,
1726}
1727
1728impl ChunkStackFacts {
1729    fn dense_on_discrete(self) -> bool {
1730        self.plain_dense
1731            && self.discrete
1732            && self.gpu_on
1733            && !self.capacity_split
1734            && !self.multi_gpu
1735            && !self.o1
1736    }
1737}
1738
1739/// Number of prompt rows that have a real teacher-forced next-token pair in a
1740/// prefill span.  The final prompt row has no successor token, so it must not
1741/// be handed to the MTP warm-up.  Keeping this arithmetic in one helper makes
1742/// the full-chunk and tail-chunk boundaries explicit for both the graph and
1743/// CPU implementations.
1744#[inline]
1745fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1746    if end <= start || start >= input_len {
1747        return 0;
1748    }
1749    let rows = (end.min(input_len) - start).min(input_len - start);
1750    if end < input_len {
1751        rows
1752    } else {
1753        rows.saturating_sub(1)
1754    }
1755}
1756
1757/// Callback for streaming tokens. Return `false` to cancel.
1758pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1759
1760/// One layer's cache ownership at a cross-turn KV reuse boundary.
1761#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1762pub(crate) struct ReuseLayer {
1763    /// Exact-attention layer (rows in `LayerKvCache`); otherwise a
1764    /// recurrent / latent mixer whose state cannot be rewound.
1765    pub full: bool,
1766    /// Positions the host owner cache reaches (`pos_len`: a trimmed
1767    /// sliding tail stores only the newest of them).
1768    pub host_rows: usize,
1769    /// Rows the wgpu token graph's device mirror holds (None: no mirror).
1770    pub device_rows: Option<usize>,
1771    /// A recurrent state lives on the device (advanced past the host copy).
1772    pub device_state: bool,
1773}
1774
1775/// What a reused turn must do before its tail prefill runs on the HOST.
1776#[derive(Debug, Clone, PartialEq, Eq)]
1777pub(crate) enum ReusePlan {
1778    /// Host caches already hold exactly the reused prefix.
1779    Ready,
1780    /// Copy device mirror rows `[from..to)` into the host cache of each
1781    /// listed layer (the rows decode wrote on the device only).
1782    Pull(Vec<(usize, usize, usize)>),
1783    /// The prefix cannot be continued on the host exactly: start fresh.
1784    Fresh,
1785}
1786
1787/// The wgpu whole-token graph decodes into a DEVICE K/V mirror and never
1788/// writes those rows back to the host cache, while the chunked prefill of a
1789/// pure-attention model reads (and appends to) the host cache. A reused turn
1790/// therefore found its host cache ending at the previous PROMPT, not at the
1791/// previous answer: the tail prefill attended without the model's own
1792/// answer and appended its rows at the wrong index (MiniCPM5 on Vulkan
1793/// repeated its tool call instead of reading the tool result). Every layer
1794/// must hold exactly `reuse_from` host rows before the host continues; rows
1795/// that exist only on the device are pulled back, anything else is fresh.
1796pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1797    let mut pulls = Vec::new();
1798    for (li, l) in layers.iter().enumerate() {
1799        if !l.full {
1800            if l.device_state {
1801                return ReusePlan::Fresh;
1802            }
1803            continue;
1804        }
1805        if l.host_rows == reuse_from {
1806            continue;
1807        }
1808        if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1809            pulls.push((li, l.host_rows, reuse_from));
1810            continue;
1811        }
1812        return ReusePlan::Fresh;
1813    }
1814    if pulls.is_empty() {
1815        ReusePlan::Ready
1816    } else {
1817        ReusePlan::Pull(pulls)
1818    }
1819}
1820
1821impl Pipeline {
1822    /// Reset state whose meaning is local to a single MTP draft sequence.
1823    /// Unlike ordinary KV reuse, MTP is always rebuilt from the prompt.
1824    fn reset_fresh_mtp_draft_state(&mut self) {
1825        self.draft_full_streak = 0;
1826    }
1827
1828    /// Clear all per-sequence state, including backend device mirrors.
1829    ///
1830    /// The host KV/history buffers are only half of the request lifecycle on
1831    /// wgpu: GDN/O(1) state and cached graph bind groups are keyed by the
1832    /// pipeline id and otherwise survive a pooled request.  Keep every fresh
1833    /// sequence entry point on this one reset path so a new request cannot
1834    /// inherit the prior request's device state.
1835    fn clear_sequence_state(&mut self) {
1836        // a replay still writing the GDN owners must land before they are
1837        // cleared or reallocated (the device holds raw pointers to them)
1838        #[cfg(target_os = "macos")]
1839        let _ = crate::gpu_metal::wait_replay();
1840        self.kv_cache.clear();
1841        // Both reuse keys (the legacy `kv_history` and the bounded
1842        // `kv_prefix`) describe the state being dropped here.
1843        self.clear_history();
1844        self.graph_logits = None;
1845        if let Some(b) = &mut self.dsv41 {
1846            b.3.clear();
1847        }
1848        crate::gpu::graph_kv_reset(self.graph_kv_id);
1849        // MTP is detached from `self` for the duration of generation, so its
1850        // device mirror is not covered by the trunk reset above.  Reset the
1851        // derived id as well: a failed/aborted warm-up must never leave a
1852        // mirror that a later request can mistake for a current MTP cache.
1853        crate::gpu::graph_kv_reset(self.mtp_kv_id());
1854    }
1855
1856    /// Make the host caches own exactly the reused prefix `[0..reuse_from)`
1857    /// before a reused turn's tail prefill runs on the host (see
1858    /// [`kv_reuse_plan`]). Returns false when the prefix cannot be continued
1859    /// exactly — the caller then starts a fresh sequence. A model whose
1860    /// prefill runs through the token graph keeps its device state as the
1861    /// authority and is left untouched.
1862    fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1863        if self.graph_prefill_preferred() {
1864            return true;
1865        }
1866        let kv_id = self.graph_kv_id;
1867        let layers: Vec<ReuseLayer> = (0..self.num_layers)
1868            .map(|li| {
1869                let full = matches!(
1870                    self.weights.layers[self.phys_layer(li)].attn,
1871                    AttnKind::Full { .. }
1872                );
1873                ReuseLayer {
1874                    full,
1875                    // Absolute depth: a trimmed sliding tail stores fewer
1876                    // rows than the positions it has seen.
1877                    host_rows: self.kv_cache.layers[li].pos_len(),
1878                    device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1879                    device_state: crate::gpu::graph_state_resident(kv_id, li),
1880                }
1881            })
1882            .collect();
1883        // No wgpu device state at all (CPU, Metal — whose graph appends every
1884        // decoded row to the owner cache itself): the host is the owner and
1885        // the extension check already proved the prefix.
1886        if layers
1887            .iter()
1888            .all(|l| l.device_rows.is_none() && !l.device_state)
1889        {
1890            return true;
1891        }
1892        let plan = kv_reuse_plan(reuse_from, &layers);
1893        let (what, rows, n) = match &plan {
1894            ReusePlan::Ready => ("host ready", 0, 0),
1895            ReusePlan::Fresh => ("fresh", 0, 0),
1896            ReusePlan::Pull(p) => (
1897                "pull",
1898                p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1899                p.len(),
1900            ),
1901        };
1902        let t0 = std::time::Instant::now();
1903        let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1904        if std::env::var("CMF_PREFILL_PROF").is_ok() {
1905            eprintln!(
1906                "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1907                if ok { "" } else { " (failed → fresh)" },
1908                t0.elapsed().as_secs_f64() * 1e3
1909            );
1910        }
1911        ok
1912    }
1913
1914    fn apply_kv_reuse_plan(
1915        &mut self,
1916        reuse_from: usize,
1917        plan: ReusePlan,
1918        layers: &[ReuseLayer],
1919    ) -> bool {
1920        let kv_id = self.graph_kv_id;
1921        match plan {
1922            ReusePlan::Fresh => return false,
1923            ReusePlan::Ready => {}
1924            ReusePlan::Pull(pulls) => {
1925                // Mirrors of one uniform geometry: one batched read serves
1926                // every layer. Per-layer geometry (MiMo-V2: 4/8 KV heads,
1927                // narrow V, sliding rings) is read layer by layer in the
1928                // host layout instead.
1929                let (nkv, hd) = {
1930                    let c = &self.kv_cache.layers[pulls[0].0];
1931                    (c.num_kv_heads, c.head_dim)
1932                };
1933                let uniform = pulls.iter().all(|&(li, _, _)| {
1934                    let c = &self.kv_cache.layers[li];
1935                    (c.num_kv_heads, c.head_dim) == (nkv, hd)
1936                });
1937                let batched = if uniform {
1938                    crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1939                } else {
1940                    None
1941                };
1942                let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1943                    Some(rows) => rows,
1944                    None => {
1945                        let mut rows = Vec::with_capacity(pulls.len());
1946                        for &(li, from, to) in &pulls {
1947                            let (lnkv, lhd) = {
1948                                let c = &self.kv_cache.layers[li];
1949                                (c.num_kv_heads, c.head_dim)
1950                            };
1951                            let Some((k, v, first_valid)) =
1952                                crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1953                            else {
1954                                return false;
1955                            };
1956                            // The host continues at `to`: a sliding layer
1957                            // reads back only its last window, a full one
1958                            // every row it lacks.
1959                            let need_from = match self.layer_window(li) {
1960                                Some(w) => from.max((to + 1).saturating_sub(w)),
1961                                None => from,
1962                            };
1963                            if first_valid > need_from {
1964                                return false;
1965                            }
1966                            rows.push((k, v));
1967                        }
1968                        rows
1969                    }
1970                };
1971                for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1972                    let cache = &mut self.kv_cache.layers[li];
1973                    let row = cache.num_kv_heads * cache.head_dim;
1974                    for p in 0..to - from {
1975                        cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1976                    }
1977                    if cache.pos_len() != to {
1978                        return false;
1979                    }
1980                }
1981            }
1982        }
1983        // A mirror past the prefix (a greedy burst that ran beyond the stop)
1984        // holds rows of the OLD continuation: rewind it so the next graph
1985        // token re-syncs those positions from the host.
1986        for (li, l) in layers.iter().enumerate() {
1987            if l.full
1988                && l.device_rows.is_some_and(|d| d > reuse_from)
1989                && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1990            {
1991                return false;
1992            }
1993        }
1994        true
1995    }
1996
1997    /// Finish a generation lifecycle after the MTP/router owners were
1998    /// detached.  Every terminal path must put those owners back before the
1999    /// pooled pipeline can serve another request.  Graph side channels and
2000    /// device mirrors are cleared on errors and cancellations; a successful
2001    /// generation keeps its decode-ready host cache for KV reuse.
2002    fn finish_generation(
2003        &mut self,
2004        mtp: &mut Option<MtpModule>,
2005        router: &mut Option<crate::swarm::DynRouter>,
2006        clear_sequence: bool,
2007    ) {
2008        // A dynamic route may have switched the overlay before the terminal
2009        // path. Restore the backbone while the detached router is still
2010        // available, because set_active_skill also owns the overlay reset.
2011        if router.is_some() {
2012            let _ = self.set_active_skill(None);
2013        }
2014        // The last speculative round's replay may still be in flight on
2015        // the second queue: whoever reads the host cache after generate()
2016        // returns (session export, the network split's KV wire, a KV
2017        // reuse) must see the final states.
2018        // A replay that failed leaves the GDN owners half-written: fail
2019        // closed and drop the sequence instead of handing the cache on.
2020        #[cfg(target_os = "macos")]
2021        let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
2022        if clear_sequence {
2023            self.clear_sequence_state();
2024            if let Some(m) = mtp.as_mut() {
2025                // The MTP owner is detached while generation runs, so the
2026                // trunk reset above cannot clear its host cache.  Drop its
2027                // partial rows before reattaching it to the pooled pipeline;
2028                // the next request must start from the same empty anchor on
2029                // CPU and on the device mirror.
2030                m.kv.clear();
2031            }
2032            if let Some(m) = self.mtp.as_mut() {
2033                // A non-speculative request leaves the configured MTP owner
2034                // attached.  Clear that dormant cache too when a shared
2035                // generation failure/cancellation resets the sequence.
2036                m.kv.clear();
2037            }
2038        }
2039        self.graph_want_logits = false;
2040        self.graph_head_required = false;
2041        self.graph_logits = None;
2042        self.graph_failed
2043            .store(false, std::sync::atomic::Ordering::Relaxed);
2044        self.cancel
2045            .store(false, std::sync::atomic::Ordering::Relaxed);
2046        self.dyn_router = router.take().or(self.dyn_router.take());
2047        self.mtp = mtp.take().or(self.mtp.take());
2048        self.mtp_graph_mode = None;
2049        self.spec_forced = None;
2050    }
2051
2052    /// Consume a graph failure reported by a forward that returns only a
2053    /// hidden vector.  `forward_ids` is a public Result API, so it must not
2054    /// turn the graph's zero hidden sentinel into a valid lm_head result.
2055    fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2056        if self
2057            .graph_failed
2058            .swap(false, std::sync::atomic::Ordering::Relaxed)
2059        {
2060            self.cancel
2061                .store(false, std::sync::atomic::Ordering::Relaxed);
2062            self.clear_sequence_state();
2063            self.graph_logits = None;
2064            self.graph_want_logits = false;
2065            self.graph_head_required = false;
2066            return Err(format!("GPU graph failed during {phase} at position {pos}"));
2067        }
2068        Ok(())
2069    }
2070
2071    #[cfg(target_os = "macos")]
2072    fn fail_metal_graph(&mut self, reason: &str) {
2073        crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2074        self.clear_sequence_state();
2075        self.graph_logits = None;
2076        self.graph_failed
2077            .store(true, std::sync::atomic::Ordering::Relaxed);
2078        self.cancel
2079            .store(true, std::sync::atomic::Ordering::Relaxed);
2080        tracing::error!("native Metal TokenGraph failed closed: {reason}");
2081    }
2082
2083    /// Start an NLL/PPL request with all graph side channels in a known
2084    /// state.  A graph failure also raises the cooperative cancel bit; it is
2085    /// consumed here and that graph-induced bit is cleared so an independent
2086    /// request can be reused.  A caller-owned cancellation remains intact.
2087    fn nll_begin(&mut self) -> Result<(), String> {
2088        if self
2089            .graph_failed
2090            .swap(false, std::sync::atomic::Ordering::Relaxed)
2091        {
2092            self.cancel
2093                .store(false, std::sync::atomic::Ordering::Relaxed);
2094            self.clear_sequence_state();
2095            self.graph_logits = None;
2096            self.graph_want_logits = false;
2097            self.graph_head_required = false;
2098            return Err("GPU graph failed before NLL scoring".to_string());
2099        }
2100        self.clear_sequence_state();
2101        self.graph_logits = None;
2102        self.graph_want_logits = false;
2103        self.graph_head_required = false;
2104        Ok(())
2105    }
2106
2107    /// End an NLL/PPL request, including the side channels that are not part
2108    /// of the host KV cache.  This is intentionally explicit instead of
2109    /// relying on a tuple/sentinel return: callers must see every failure.
2110    fn nll_end(&mut self) {
2111        self.clear_sequence_state();
2112        self.graph_logits = None;
2113        self.graph_want_logits = false;
2114        self.graph_head_required = false;
2115        self.graph_failed
2116            .store(false, std::sync::atomic::Ordering::Relaxed);
2117    }
2118
2119    /// Check the graph failure channel at a scoring boundary and leave the
2120    /// pipeline reusable when the device path failed.
2121    fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2122        #[cfg(test)]
2123        if self.nll_test_fail_at == Some(pos) {
2124            self.nll_test_fail_at = None;
2125            self.graph_failed
2126                .store(true, std::sync::atomic::Ordering::Relaxed);
2127            self.cancel
2128                .store(true, std::sync::atomic::Ordering::Relaxed);
2129        }
2130        if self
2131            .graph_failed
2132            .swap(false, std::sync::atomic::Ordering::Relaxed)
2133        {
2134            self.cancel
2135                .store(false, std::sync::atomic::Ordering::Relaxed);
2136            self.clear_sequence_state();
2137            self.graph_logits = None;
2138            self.graph_want_logits = false;
2139            return Err(format!(
2140                "GPU graph failed during NLL {phase} at position {pos}"
2141            ));
2142        }
2143        Ok(())
2144    }
2145
2146    /// Map a virtual layer index to its physical weight index.
2147    /// Looped Transformer (Nanbeige 4.2): 22 physical layers × 2 loops = 44 virtual;
2148    /// virtual layer 23 maps back to physical layer 1 (23 % 22 = 1).
2149    #[inline]
2150    pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2151        match &self.layer_map {
2152            Some(m) => m[virtual_idx],
2153            None => virtual_idx % self.physical_layers,
2154        }
2155    }
2156
2157    /// Mage-VL (`llava_onevision2`) prompt ingest through the batched wgpu
2158    /// graph (`CMF_GRAPH_PREFILL=0` keeps the chunked host prefill).
2159    fn onevision2_prefill(&self) -> bool {
2160        self.model.as_ref().is_some_and(|m| m.header.arch.arch_name == "llava_onevision2")
2161            && std::env::var("CMF_GRAPH_PREFILL").as_deref() != Ok("0")
2162    }
2163
2164    /// SenseNova-Loop: whether virtual layer `li` runs inside the mHC
2165    /// streams (the device graphs decline such layers).
2166    #[inline]
2167    pub fn mhc_layer(&self, li: usize) -> bool {
2168        self.mhc.as_ref().is_some_and(|m| m.on(self.phys_layer(li)))
2169    }
2170
2171    /// mHC before a sub-layer of the decode token: route its streams into
2172    /// `h` (entering the range copies `h` into every stream first).
2173    fn mhc_pre(&mut self, li: usize, ffn: bool, h: &mut [f32]) -> crate::sensenova::MhcCoef {
2174        let p = self.phys_layer(li);
2175        let m = self.mhc.as_deref().expect("mhc layer");
2176        let n = m.s.streams * m.dim;
2177        if self.mhc_r.len() != n {
2178            self.mhc_r.resize(n, 0.0);
2179        }
2180        if !ffn && m.enters(p) {
2181            m.enter(&mut self.mhc_r, h);
2182        }
2183        m.route(p, ffn, &self.mhc_r, h)
2184    }
2185
2186    /// mHC after a sub-layer: fold its output `y` into the streams; after
2187    /// the range's last FFN, `h` becomes the streams' mean.
2188    fn mhc_post(&mut self, li: usize, ffn: bool, y: &[f32], c: &crate::sensenova::MhcCoef, h: &mut [f32]) {
2189        let p = self.phys_layer(li);
2190        let m = self.mhc.as_deref().expect("mhc layer");
2191        m.merge(&mut self.mhc_r, y, c);
2192        if ffn && m.leaves(p) {
2193            m.leave(&self.mhc_r, h);
2194        }
2195    }
2196
2197    /// True when `virtual_idx` is the last layer of a loop iteration
2198    /// (used for loop_final_norm insertion).
2199    #[inline]
2200    pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2201        self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2202    }
2203
2204    /// Build a pipeline from parts (used by the loader and tests).
2205    #[allow(clippy::too_many_arguments)]
2206
2207    /// Whole-block q1 token graph on the GPU (macOS/Metal): the run of
2208    /// consecutive q1 layers — GDN *and* full attention — starting at
2209    /// `start` executes as few command buffers as the CPU truly needs.
2210    /// Hidden stays device-resident across every layer; the only syncs
2211    /// are before each CPU attend (it needs q/k/v and owns the KV
2212    /// cache) and the final hidden readback. Recurrent states
2213    /// round-trip through shared memory (the CPU stays their owner, so
2214    /// every other path remains coherent). Returns the first layer
2215    /// index NOT covered (== `start` → refused, caller falls through
2216    /// to the per-layer CPU path).
2217    /// Should prefill run position-by-position through the GPU token
2218    /// graph instead of the batched CPU chunk-GEMM? True for q1 GDN
2219    /// hybrids on native Metal: their chunk prefill is walled by the
2220    /// sequential scalar recurrence, so the graph's decode rate wins.
2221    /// NOT for Looped Transformers, despite the per-chunk loop_final_norm
2222    /// sync: the chunk-GEMM amortizes each weight over the whole chunk,
2223    /// which the per-position graph cannot (Nanbeige 4.2 on M4, 512-token
2224    /// prompt: 85 tok/s chunked vs 14 through the graph).
2225    #[cfg(target_os = "macos")]
2226    fn graph_prefill_preferred(&self) -> bool {
2227        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2228        if !crate::gpu::enabled_here()
2229            || !graph_force
2230            || std::env::var("CMF_GPU_BLOCK")
2231                .map(|v| v == "0")
2232                .unwrap_or(false)
2233            // CMF_PREFILL_GRAPH=0: the chunked prefill (GEMM projections,
2234            // CPU recurrence) instead of the per-position token graph.
2235            || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2236        {
2237            return false;
2238        }
2239        self.weights
2240            .layers
2241            .iter()
2242            .any(|lw| {
2243                matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2244            })
2245    }
2246
2247    /// Prompt ingest through the batched wgpu graph in device-prefix mode:
2248    /// a MoE stack that does not fit the card runs each chunk's leading
2249    /// layers on the device (experts resident) and the rest on the host's
2250    /// batched walk. On by default for models with per-layer attention
2251    /// geometry (MiMo-V2 — its measured default); `CMF_BATCH_PREFIX=1`
2252    /// opts any other MoE model in, `=0` keeps the chunked host prefill.
2253    #[cfg(not(target_os = "macos"))]
2254    fn batch_prefix_prefill(&self) -> bool {
2255        let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2256            Ok("0") => return false,
2257            Ok("1") => true,
2258            _ => false,
2259        };
2260        crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2261            && crate::gpu::enabled_here()
2262            && !self.graph_refused()
2263            && (forced || self.graph_attn_decline_reason().is_some())
2264            && self.wgpu_graph_attn_decline().is_none()
2265            && self.attn_softcap == 0.0
2266            && self
2267                .weights
2268                .layers
2269                .iter()
2270                .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2271            && self.automatic_gpu_prefix().is_some()
2272    }
2273
2274    /// Gemma-4's prompt through the batched wgpu graph (`gemma_batch`): a
2275    /// dense stack wholly on a discrete card, whose token graph already
2276    /// decodes it. Not `graph_prefill_preferred` — the host stays the KV
2277    /// owner for reuse and scoring; only generation's ingest changes.
2278    /// `CMF_G4_BATCH=0` keeps the chunked host prefill.
2279    fn g4_batch_prefill(&self) -> bool {
2280        #[cfg(target_os = "macos")]
2281        {
2282            false
2283        }
2284        #[cfg(not(target_os = "macos"))]
2285        {
2286            // An EMPTY layer stack (`all()` is vacuously true) plus a
2287            // gemma4 geometry flag is exactly the gemma-4 E-series
2288            // per-token stack — it has no batch graph rows to ingest.
2289            !self.weights.layers.is_empty()
2290                && !self.per_token_stack()
2291                && (self.global_attn.is_some() || self.attn_v_norm)
2292                && crate::gpu::g4_batch_on()
2293                && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2294                && crate::gpu::enabled_here()
2295                && crate::gpu::discrete()
2296                && !self.graph_refused()
2297                && !self.o1_active()
2298                && self.attn_softcap == 0.0
2299                && self.wgpu_graph_attn_decline().is_none()
2300                && self.weights.layers.iter().all(|lw| {
2301                    matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2302                })
2303                && self.automatic_gpu_prefix().is_none()
2304        }
2305    }
2306
2307    /// Gemma-4's batched prefill of `ids` at positions `pos0..`
2308    /// (`g4_batch_prefill`): chunks of `generation_batch_k` rows through the
2309    /// batch graph. Ok((n, rows)): `n` positions ingested; `rows` = the last
2310    /// one's hidden, or every row's (`keep_all`). A refusal stops early
2311    /// (n < ids.len()) after pulling the device rows the host walk needs.
2312    /// Err: a failure after admission — the device state is undefined.
2313    #[cfg(not(target_os = "macos"))]
2314    fn g4_batch_ingest(
2315        &mut self,
2316        ids: &[u32],
2317        pos0: usize,
2318        keep_all: bool,
2319    ) -> Result<(usize, Vec<f32>), String> {
2320        let hs = self.hidden_size;
2321        let chunk = self.generation_batch_k().max(1);
2322        let end_all = pos0 + ids.len();
2323        let mut pos = pos0;
2324        let mut out = Vec::new();
2325        while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2326            let end = (pos + chunk).min(end_all);
2327            let bk = end - pos;
2328            let mut hiddens = vec![0f32; bk * hs];
2329            for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2330                hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2331            }
2332            let positions: Vec<usize> = (pos..end).collect();
2333            let t_chunk = std::time::Instant::now();
2334            let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2335            if std::env::var("CMF_GRAPH_PROF").is_ok() {
2336                let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2337                eprintln!(
2338                    "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2339                    bk as f64 / (ms / 1000.0)
2340                );
2341            }
2342            match outcome {
2343                crate::gpu::BatchGraphOutcome::Completed => {
2344                    if keep_all {
2345                        out.extend_from_slice(&hiddens);
2346                    } else {
2347                        out = hiddens[(bk - 1) * hs..].to_vec();
2348                    }
2349                    pos = end;
2350                }
2351                crate::gpu::BatchGraphOutcome::Failed => {
2352                    return Err("Gemma-4 batched prefill failed after admission".into());
2353                }
2354                crate::gpu::BatchGraphOutcome::Declined => {
2355                    #[cfg(feature = "gpu")]
2356                    if pos > pos0 {
2357                        self.pull_lagging_host_kv(0, self.num_layers, pos);
2358                    }
2359                    break;
2360                }
2361            }
2362        }
2363        Ok((pos - pos0, out))
2364    }
2365
2366    /// Hidden rows of every position of `ids` through Gemma-4's batched
2367    /// prefill from a fresh sequence (the prefill's teacher-forced logits
2368    /// for parity gates: `logits_from_hidden` per row). None when the model
2369    /// or the card does not take that route, or it refused part way.
2370    pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2371        #[cfg(not(target_os = "macos"))]
2372        {
2373            if ids.is_empty() || !self.g4_batch_prefill() {
2374                return None;
2375            }
2376            self.clear_sequence_state();
2377            match self.g4_batch_ingest(ids, 0, true) {
2378                Ok((n, rows)) if n == ids.len() => Some(rows),
2379                _ => {
2380                    self.clear_sequence_state();
2381                    None
2382                }
2383            }
2384        }
2385        #[cfg(target_os = "macos")]
2386        {
2387            let _ = ids;
2388            None
2389        }
2390    }
2391
2392    #[cfg(not(target_os = "macos"))]
2393    fn graph_prefill_preferred(&self) -> bool {
2394        // Discrete-GPU wgpu whole-token graph: GDN layers carry recurrent state
2395        // (conv ring + delta-rule S) resident on the GPU. A batched CPU prefill
2396        // builds that state on the CPU only, leaving the GPU buffers zeroed at
2397        // decode → garbage. Route GDN-hybrid prefill through the graph one
2398        // position at a time so the resident state is seeded exactly as decode
2399        // will read it. Pure-attention models keep the batched CPU prefill (its
2400        // KV mirror re-syncs from the CPU cache, so no seeding gap).
2401        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2402        if !graph_on || !crate::gpu::enabled_here() {
2403            return false;
2404        }
2405        // Embryo's phase state is device-owned by the resident graph during
2406        // prefill; the batched CPU path would leave decode seeing a zeroed
2407        // device recurrence.  Route the prompt position-by-position too.
2408        if self.embryo_resident_eligible() {
2409            // The resident graph owns the phase state and anchor KV on the
2410            // device.  A prefill-only graph would leave decode on the host
2411            // with no way to import that state, so keep the whole sequence
2412            // on one owner (or use the ordinary CPU prefill/decode pair).
2413            return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2414        }
2415        // The descriptor-aware Prism graph now carries both the FWHT/affine
2416        // transforms and resident GDN state, so it is also the exact prefill
2417        // path for this model.  Keeping it here (rather than falling through
2418        // to the CPU chunk walk) is required for a long prompt to seed the
2419        // same device state that decode consumes.
2420        // O(1) needs the CPU prefill: the q-trace that seals the Nyström
2421        // skeleton is recorded there and nowhere else. The GDN half of
2422        // the hybrid loses nothing — the graph's first decode creates
2423        // its (ring, S) entries seeded from `cpu_state`, the same
2424        // handoff every graph run relies on when the entry is fresh.
2425        // Without this line the two designs collide on hybrids and o1
2426        // never becomes graph-portable: prefill through the graph
2427        // records no trace, so views stay None forever.
2428        if self.o1_active() {
2429            return false;
2430        }
2431        // A model the wgpu graphs decline outright would walk its prompt
2432        // one position at a time through a graph that never runs: the
2433        // batched CPU chunk prefill is the right ingest for it.
2434        if self.wgpu_graph_attn_decline().is_some() {
2435            return false;
2436        }
2437        if self
2438            .weights
2439            .layers
2440            .iter()
2441            .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2442            // Mage-VL: its screenshot prompts (~2k rows) go through the
2443            // batched graph, 4× the chunked host ingest on an A40 (2048
2444            // tokens: 806 tok/s at 128 rows, 893 at 256, against 196).
2445            || self.onevision2_prefill()
2446        {
2447            return true;
2448        }
2449        // MoE models too: the chunked CPU prefill runs every expert on the
2450        // host (Hy-MT2-30B-A3B on a Xeon: 8 tok/s of ingest against 53 of
2451        // graph decode), while the token graph — and the batched graph under
2452        // CMF_BATCH_K — keep the experts resident. Full attention in the
2453        // graph writes the KV mirror that decode reads, exactly as it does
2454        // for the hybrids' attention layers. Only when the whole stack is
2455        // resident: with a device prefix the per-position walk finishes
2456        // every token on the host, and the chunked prefill (GEMMs on the
2457        // card, the expert loop batched on the host) is the faster ingest
2458        // (the 8 GB ladder point: 7 tok/s chunked against ~1 walked).
2459        self.weights
2460            .layers
2461            .iter()
2462            .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2463            && self.automatic_gpu_prefix().is_none()
2464    }
2465
2466    #[cfg(target_os = "macos")]
2467    fn q1_graph_gpu(
2468        &mut self,
2469        start: usize,
2470        upto: Option<usize>,
2471        position: usize,
2472        h: &mut [f32],
2473    ) -> usize {
2474        let _mt0 = std::time::Instant::now(); // CMF_METAL_HOSTPROF
2475        use crate::gpu::{
2476            AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, SconvGpuLayer, TokenGraph,
2477        };
2478        const MOE_WORKING_SET_REASON: &str =
2479            "model weights exceed Metal working set for native MoE graph";
2480        // A prior admission check rejected this pipeline. Avoid rebuilding the
2481        // entire 40-layer graph plan at every individual host layer after the
2482        // fallback begins; the per-operation path below stays eligible.
2483        if self
2484            .graph_declines
2485            .borrow()
2486            .iter()
2487            .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2488        {
2489            return start;
2490        }
2491        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2492        if self.attn_softcap > 0.0 // capped scores: no graph kernel — CPU path
2493            || !self.metal_rope_scale_ok()
2494            || !crate::gpu::enabled_here()
2495            || !graph_force
2496            || std::env::var("CMF_GPU_BLOCK")
2497                .map(|v| v == "0")
2498                .unwrap_or(false)
2499        {
2500            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2501                eprintln!(
2502                    "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2503                    self.attn_softcap > 0.0,
2504                    crate::gpu::enabled_here(),
2505                    graph_force,
2506                );
2507            }
2508            if self.graph_head_required {
2509                self.fail_metal_graph("native graph front gate refused");
2510            }
2511            return start;
2512        }
2513        // The graph encodes SiLU or exact-GELU FFNs and attention with an
2514        // explicit model scale. Sliding-window layers with their own RoPE
2515        // table / rotary width ride it too (`metal_graph_swa`): both the
2516        // device attend and the sandwich's CPU attend read each layer's
2517        // window and table. Sandwich norms and other activations fall back
2518        // to the CPU path.
2519        // Gemma-4 (`metal_gemma4`): its sliding layers ride the same door,
2520        // its global layers, V norm, sandwich norms, layer scalar and
2521        // tanh-GELU the `gpu_metal::gemma4` kernels.
2522        let gemma4 = self.metal_gemma4();
2523        let swa_graph = self.metal_graph_swa() || gemma4;
2524        // The normal native GQA kernel carries a learned, value-less
2525        // softmax column.  It also takes the exact per-layer sliding
2526        // window, but its split-K twin has no sink column.  Thus a
2527        // sink-bearing model may use this door only when the ordinary
2528        // sliding geometry has passed `metal_graph_swa` (or when it has no
2529        // sliding layers at all).
2530        let graph_attn_reason = self.graph_attn_decline_reason();
2531        let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2532            && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2533        // Plain-weight (x̂·w) sandwich norms (LFM3) take the generic norm
2534        // kernels, as on the wgpu graph; Gemma-2/3's (1+w) norms and the
2535        // layer scalar stay Gemma-4's.
2536        let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
2537        if (self.swa.is_some() && !swa_graph)
2538            || (self.global_attn.is_some() && !gemma4)
2539            || self.attention_heads_per_layer.is_some()
2540            || (self.attn_v_norm && !gemma4)
2541            // Per-layer KV heads / narrow V stay outside this graph.  A
2542            // learned sink is admitted only through native_sink_attend.
2543            || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2544            || self.weights.layers.iter().any(|lw| {
2545                ((lw.layer_scale.is_some()
2546                    || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some())))
2547                    && !gemma4)
2548                    || matches!(&lw.ffn, FfnKind::Dense(d)
2549                        if !(matches!(d.act, Act::Silu | Act::Gelu)
2550                            || (gemma4 && d.act == Act::GeluTanh)))
2551            })
2552        {
2553            // The Metal graphs' device attend has no per-layer attention
2554            // geometry (the wgpu graphs do): say so once, by name.
2555            if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2556                self.note_graph_decline("metal block graph", reason);
2557            }
2558            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2559                eprintln!(
2560                    "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2561                    self.swa.is_some(),
2562                    self.global_attn.is_some(),
2563                    self.attention_heads_per_layer.is_some(),
2564                    self.attn_v_norm,
2565                    (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2566                );
2567            }
2568            if self.graph_head_required {
2569                self.fail_metal_graph("native graph architecture gate refused");
2570            }
2571            return start;
2572        }
2573        // Looped Transformer: the graph covers ALL loop iterations;
2574        // encode_loop_norm is inserted on-device at each boundary.
2575        let limit = upto
2576            .map(|u| u + 1)
2577            .unwrap_or(self.num_layers)
2578            .min(self.num_layers);
2579
2580        enum Item<'a> {
2581            Gdn {
2582                run: Vec<GdnGpuLayer<'a>>,
2583                first: usize,
2584            },
2585            /// A run of LFM2/LFM3 short-conv layers (conv ring on device).
2586            Sconv {
2587                run: Vec<SconvGpuLayer<'a>>,
2588                first: usize,
2589            },
2590            Attn {
2591                l: AttnGpuLayer<'a>,
2592                li: usize,
2593                q_norm: Option<&'a [f32]>,
2594                k_norm: Option<&'a [f32]>,
2595                output_gate: bool,
2596                /// `self_attn.g_proj` output gate (per-head flag): the
2597                /// sandwich projects the device-normed input on the host
2598                /// and gates the attend's output before O.
2599                proj_gate: Option<(&'a QTensor, bool)>,
2600                /// The same gate's f32 rows `[nh × hidden]` when the device
2601                /// attend can apply it (per-head sigmoid, f32 in RAM).
2602                head_gate_w: Option<&'a [f32]>,
2603                bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2604                /// Attend on the device too (no sync): F32 KV, no
2605                /// o1/bias, dims inside the kernels' contract.
2606                full_gpu: bool,
2607            },
2608        }
2609
2610        // Device-attend KERNEL contract, shared by every Full layer. The
2611        // hd>128 default-off POLICY is applied after the scan: it was
2612        // measured on dense models, and a MoE plan inverts it — with the
2613        // experts on device each CPU-attend sandwich costs a
2614        // commit+wait, ~30 submits/token (W2 on M4: 14.7 tok/s
2615        // sandwiched vs 27.1 device-attend vs 18.8 pure CPU).
2616        let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2617        let attend_contract = attend_mode != "0"
2618            && attend_mode != "off"
2619            && self.head_dim % 4 == 0
2620            && self.head_dim <= 256
2621            // Flint has explicit no-RoPE layers.  The device RoPE kernel
2622            // accepts rd=0 (zero rotation lanes) and binds a sentinel table;
2623            // retain the historical lane-pair requirement for nonzero rd.
2624            && (self.rotary_dim == 0
2625                || (self.rotary_dim >= 2
2626                    && self.rotary_dim <= self.head_dim
2627                    && (self.rotary_dim / 2) % 32 == 0))
2628            && self.num_kv_heads > 0
2629            && self.num_heads % self.num_kv_heads == 0;
2630
2631        let mut plan: Vec<Item> = Vec::new();
2632        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2633        // Break-reason diagnostics ride the same env as the plan summary.
2634        let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2635        let mut scan = start;
2636        while scan < limit {
2637            // SenseNova-Loop: MRoPE prompt rows rotate on the host; an mHC
2638            // layer rides the graph when its kernels compiled (two streams,
2639            // dense FFN, device attend — checked below).
2640            if self.prompt_mrope.is_some()
2641                || (self.mhc_layer(scan)
2642                    && (!crate::gpu_metal::mhc::available()
2643                        || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
2644                        || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
2645                        || !matches!(self.weights.layers[self.phys_layer(scan)].ffn, FfnKind::Dense(_))))
2646            {
2647                break;
2648            }
2649            let lw = &self.weights.layers[self.phys_layer(scan)];
2650            let ffn = match &lw.ffn {
2651                FfnKind::Dense(d) if d.segs.is_empty() => {
2652                    let (Some(g), Some(u), Some(dn)) = (
2653                        d.gate_proj.metal_graph_parts(),
2654                        d.up_proj.metal_graph_parts(),
2655                        d.down_proj.metal_graph_parts(),
2656                    ) else {
2657                        if block_diag {
2658                            eprintln!(
2659                                "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2660                            );
2661                        }
2662                        break;
2663                    };
2664                    MetalFfn::Dense {
2665                        gate: g,
2666                        up: u,
2667                        down: dn,
2668                        // The front gate admitted SiLU, exact GELU and
2669                        // (Gemma-4) tanh-GELU only.
2670                        act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2671                    }
2672                }
2673                FfnKind::Moe(m) => {
2674                    // Flint's detached Q4 shared expert is intentionally
2675                    // tried before the generic uniform-bank graph builder.
2676                    // The latter correctly refuses its ragged Q2/Q4 widths;
2677                    // this narrow descriptor keeps the router and selected
2678                    // routed experts device-resident inside a bounded page.
2679                    let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2680                    {
2681                        MetalFfn::FlintMoe(flint)
2682                    } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2683                        MetalFfn::Moe(moe)
2684                    } else {
2685                        if block_diag {
2686                            eprintln!(
2687                                "block-graph: L{scan} MoE outside the graph contract — run ends"
2688                            );
2689                        }
2690                        break;
2691                    };
2692                    if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2693                        model_ref.get_or_insert_with(|| model.clone());
2694                    }
2695                    ffn
2696                }
2697                _ => {
2698                    if block_diag {
2699                        eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2700                    }
2701                    break;
2702                }
2703            };
2704            match &lw.attn {
2705                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2706                    let parts = (
2707                        w.in_proj_qkv.metal_graph_parts(),
2708                        w.in_proj_z.metal_graph_parts(),
2709                        w.in_proj_a.f32_parts(),
2710                        w.in_proj_b.f32_parts(),
2711                        w.out_proj.metal_graph_parts(),
2712                    );
2713                    let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2714                        if block_diag {
2715                            eprintln!(
2716                                "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2717                                w.in_proj_qkv.metal_graph_parts().is_some(),
2718                                w.in_proj_z.metal_graph_parts().is_some(),
2719                                w.in_proj_a.f32_parts().is_some(),
2720                                w.in_proj_b.f32_parts().is_some(),
2721                                w.out_proj.metal_graph_parts().is_some(),
2722                            );
2723                        }
2724                        break;
2725                    };
2726                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2727                        model_ref.get_or_insert_with(|| model.clone());
2728                    }
2729                    let gl = GdnGpuLayer {
2730                        mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
2731                        attn_norm: &lw.input_norm,
2732                        post_norm: &lw.post_norm,
2733                        qkv,
2734                        z,
2735                        a,
2736                        b,
2737                        out,
2738                        ffn,
2739                        conv1d: &w.conv1d,
2740                        a_log: &w.a_log,
2741                        dt_bias: &w.dt_bias,
2742                        gnorm: &w.norm,
2743                    };
2744                    match plan.last_mut() {
2745                        Some(Item::Gdn { run, .. }) => run.push(gl),
2746                        _ => plan.push(Item::Gdn {
2747                            run: vec![gl],
2748                            first: scan,
2749                        }),
2750                    }
2751                }
2752                AttnKind::ShortConv(w) if self.short_conv_cfg.is_some() => {
2753                    let (Some(inp), Some(out)) =
2754                        (w.in_proj.metal_graph_parts(), w.out_proj.metal_graph_parts())
2755                    else {
2756                        if block_diag {
2757                            eprintln!("block-graph: L{scan} short-conv parts refused");
2758                        }
2759                        break;
2760                    };
2761                    if let QTensor::Mapped { model, .. } = &w.in_proj {
2762                        model_ref.get_or_insert_with(|| model.clone());
2763                    }
2764                    let sl = SconvGpuLayer {
2765                        attn_norm: &lw.input_norm,
2766                        post_norm: &lw.post_norm,
2767                        in_proj: inp,
2768                        taps: &w.conv,
2769                        out,
2770                        attn_out_norm: lw.attn_out_norm.as_deref(),
2771                        ffn_out_norm: lw.ffn_out_norm.as_deref(),
2772                        ffn,
2773                    };
2774                    match plan.last_mut() {
2775                        Some(Item::Sconv { run, .. }) => run.push(sl),
2776                        _ => plan.push(Item::Sconv {
2777                            run: vec![sl],
2778                            first: scan,
2779                        }),
2780                    }
2781                }
2782                AttnKind::Full {
2783                    wq,
2784                    wk,
2785                    wv,
2786                    wo,
2787                    q_norm,
2788                    k_norm,
2789                    output_gate,
2790                    softplus_gate,
2791                    bias,
2792                } if (!self.kv_cache.layers[scan].o1_sealed()
2793                    // Sealed o1 stays plannable when the Metal o1 port
2794                    // is on: full_gpu attends through the device state,
2795                    // and any refusal falls to the sandwich, whose CPU
2796                    // core routes sealed layers through the nystrom step.
2797                    || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2798                    // A projected output gate: Spark-X2.5's per-head
2799                    // sigmoid form only (Laguna's softplus gate is
2800                    // unmeasured on these paths and keeps the CPU walk).
2801                    && softplus_gate
2802                        .as_ref()
2803                        .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2804                {
2805                    let parts = (
2806                        wq.metal_graph_parts(),
2807                        wk.metal_graph_parts(),
2808                        wv.metal_graph_parts(),
2809                        wo.metal_graph_parts(),
2810                    );
2811                    let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2812                        break;
2813                    };
2814                    if let QTensor::Mapped { model, .. } = wq {
2815                        model_ref.get_or_insert_with(|| model.clone());
2816                    }
2817                    let cache = &self.kv_cache.layers[scan];
2818                    // O(1) layer on Metal: the device attends through the
2819                    // sealed Nystrom state (opt-in while the port proves
2820                    // itself). Unsealed -> sandwich path = the CPU o1 step.
2821                    let o1_metal = cache.o1.is_some()
2822                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2823                        && cache.o1_views().is_some();
2824                    // The device attend takes the layer's window and RoPE
2825                    // table, and the per-head gate from f32 rows; a gate
2826                    // held otherwise sandwiches.
2827                    let head_gate_w = softplus_gate
2828                        .as_ref()
2829                        .and_then(|(g, _)| g.f32_parts())
2830                        .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2831                        .map(|(d, _, _)| d);
2832                    // Gemma-4's global layers are 512 wide over one KV head
2833                    // (the `gemma4` 16-slot kernels, `CMF_METAL_GEMMA4_W16`).
2834                    let (nkv_g, hd_g) = if gemma4 {
2835                        let (a, b, _) = self.layer_geom(scan);
2836                        (a, b)
2837                    } else {
2838                        (self.num_kv_heads, self.head_dim)
2839                    };
2840                    let full_gpu = attend_contract
2841                        && softplus_gate.is_none() == head_gate_w.is_none()
2842                        && cache.mode == crate::kv_cache::KvMode::F32
2843                        && (cache.o1.is_none() || o1_metal)
2844                        && bias.is_none()
2845                        && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2846                        && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2847                        && pk.1 == nkv_g * hd_g
2848                        && pv.1 == nkv_g * hd_g
2849                        && po.2 == self.num_heads * hd_g;
2850                    // An mHC layer needs the device attend (the CPU-attend
2851                    // sandwich has no stream hand-off).
2852                    if !full_gpu && self.mhc_layer(scan) {
2853                        break;
2854                    }
2855                    plan.push(Item::Attn {
2856                        l: AttnGpuLayer {
2857                            mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
2858                            attn_norm: &lw.input_norm,
2859                            post_norm: &lw.post_norm,
2860                            wq: pq,
2861                            wk: pk,
2862                            wv: pv,
2863                            wo: po,
2864                            ffn,
2865                            attn_out_norm: lw.attn_out_norm.as_deref(),
2866                            ffn_out_norm: lw.ffn_out_norm.as_deref(),
2867                            layer_scale: lw.layer_scale,
2868                        },
2869                        li: scan,
2870                        q_norm: q_norm.as_deref(),
2871                        k_norm: k_norm.as_deref(),
2872                        output_gate: *output_gate,
2873                        proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2874                        head_gate_w,
2875                        bias: bias
2876                            .as_ref()
2877                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2878                        full_gpu,
2879                    });
2880                }
2881                _ => break,
2882            }
2883            scan += 1;
2884        }
2885        // SenseNova-Loop: the mHC streams live on the device only inside one
2886        // graph, so a graph may neither enter a range midway nor stop
2887        // inside one (the host walks such a stretch with its own streams).
2888        if let Some(m) = self.mhc.as_deref() {
2889            let inside = |li: usize| m.on(self.phys_layer(li));
2890            let starts_mid = inside(start) && !m.enters(self.phys_layer(start));
2891            let stops_mid = scan > start && inside(scan - 1) && !m.leaves(self.phys_layer(scan - 1));
2892            if starts_mid || stops_mid {
2893                if std::env::var("CMF_GRAPH_DBG").is_ok() {
2894                    eprintln!("q1-graph: [{start}, {scan}) would split an mHC range — host path");
2895                }
2896                return start;
2897            }
2898        }
2899        let Some(model) = model_ref else {
2900            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2901                eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2902            }
2903            if self.graph_head_required {
2904                self.fail_metal_graph("native graph has no mapped model reference");
2905            }
2906            return start;
2907        };
2908        if plan.is_empty() {
2909            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2910                eprintln!("q1-graph: empty plan at layer {start}");
2911            }
2912            if self.graph_head_required {
2913                self.fail_metal_graph("native graph plan is empty");
2914            }
2915            return start;
2916        }
2917        let has_moe = plan.iter().any(|it| match it {
2918            Item::Gdn { run, .. } => run
2919                .iter()
2920                .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2921            Item::Sconv { run, .. } => run
2922                .iter()
2923                .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2924            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2925        });
2926        let has_flint_moe = plan.iter().any(|it| match it {
2927            Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2928            Item::Sconv { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2929            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2930        });
2931        let metal_working_set = crate::gpu_metal::working_set_bytes();
2932        let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2933        let model_bytes = model.primary_bytes().len() as u64;
2934        // Flint q2 can sit below recommendedMaxWorkingSetSize on a 24 GB
2935        // M4 yet still exceed maxBufferLength. In that case a normal graph
2936        // would manufacture overlapping whole-file WeightArena windows;
2937        // treat it as the same bounded-page requirement rather than letting
2938        // the generic graph reintroduce broad residency.
2939        let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2940            || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2941        // An oversized MoE mmap normally has to decline: the jobs kernels
2942        // bind all WeightArena windows.  The opt-in pager takes a different,
2943        // bounded route: a *complete sequence of logical layers* is put in
2944        // one transient no-copy page, graph-finished, dropped, then the outer
2945        // layer loop calls us again for the next page.  That preserves exact
2946        // hidden/GDN/KV handoff while replacing ~40 host-routed expert waits
2947        // with a few page-boundary syncs.  Never use it for a fused-head NLL
2948        // request: that contract explicitly requires a single whole graph.
2949        let pager_cap = if !has_moe {
2950            None
2951        } else if has_flint_moe {
2952            // Do not let generic `CMF_METAL_MOE_PAGER` turn the experimental
2953            // full-bank Flint graph on. Its direct selected-view pager stays
2954            // independently automatic; this path needs its own opt-in.
2955            crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2956        } else {
2957            crate::gpu_metal::moe_pager_cap_bytes()
2958        };
2959        let pager_eligible =
2960            pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2961        let mut pager_span: Option<(usize, usize)> = None;
2962        if pager_eligible {
2963            let cap = pager_cap.expect("checked above");
2964            // When this page reaches the true model end, include the fused
2965            // lm_head too.  Otherwise `lm_head_ok` would correctly refuse a
2966            // binding outside the page after the layers had already run.
2967            let tail_span = (self.graph_want_logits
2968                && upto.is_none()
2969                && scan == self.num_layers
2970                && std::env::var("CMF_GPU_LMHEAD")
2971                    .map(|v| v != "0")
2972                    .unwrap_or(true))
2973            .then(|| self.weights.lm_head.metal_graph_parts())
2974            .flatten()
2975            .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2976            let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2977                let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2978                Some(match span {
2979                    Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2980                    None => (lo, hi),
2981                })
2982            };
2983            let page_span = |span: (usize, usize), include_tail: bool| {
2984                let (mut lo, mut hi) = span;
2985                if include_tail {
2986                    if let Some((tail_lo, tail_hi)) = tail_span {
2987                        lo = lo.min(tail_lo);
2988                        hi = hi.max(tail_hi);
2989                    }
2990                }
2991                crate::gpu_metal::page_round_span(lo, hi)
2992            };
2993            let mut selected = 0usize;
2994            let mut selected_span: Option<(usize, usize)> = None;
2995            // `Item::Gdn` is built as a maximal run so the normal resident
2996            // graph can batch its recurrent state work.  A physical CMF can
2997            // nevertheless place that whole run across a page cap while each
2998            // logical layer fits.  Split *only the final admitted run* at a
2999            // layer boundary: the next outer-loop call rebuilds the remaining
3000            // suffix, preserving hidden/state handoff and keeping the normal
3001            // long-run batching within every page.
3002            let mut gdn_prefix: Option<(usize, usize)> = None;
3003            'items: for (i, item) in plan.iter().enumerate() {
3004                match item {
3005                    // Short-conv runs are not paged (no oversized
3006                    // short-conv MoE has been measured): the page ends.
3007                    Item::Sconv { .. } => break,
3008                    Item::Attn { li, .. } => {
3009                        let Some(span) = add_layer_span(selected_span, *li) else {
3010                            break;
3011                        };
3012                        let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
3013                            break;
3014                        };
3015                        if page_hi - page_lo > cap {
3016                            break;
3017                        }
3018                        selected = i + 1;
3019                        selected_span = Some((page_lo, page_hi));
3020                    }
3021                    Item::Gdn { first, run } => {
3022                        // Preserve a complete recurrent run whenever it can
3023                        // occupy a page on its own.  If the preceding items
3024                        // merely used the remaining cap, close that page and
3025                        // let the next q1 invocation retain the full run.
3026                        // Prefix-splitting is only for a run that genuinely
3027                        // cannot fit an empty page.
3028                        let mut whole = None;
3029                        for take in 1..=run.len() {
3030                            whole = add_layer_span(whole, *first + take - 1);
3031                            if whole.is_none() {
3032                                break;
3033                            }
3034                        }
3035                        let whole_fits_empty_page = whole
3036                            .and_then(|span| page_span(span, false))
3037                            .is_some_and(|(lo, hi)| hi - lo <= cap);
3038                        if whole_fits_empty_page {
3039                            let mut span = selected_span;
3040                            for take in 1..=run.len() {
3041                                let Some(next) = add_layer_span(span, *first + take - 1) else {
3042                                    break 'items;
3043                                };
3044                                span = Some(next);
3045                            }
3046                            let Some(span) = span else {
3047                                break;
3048                            };
3049                            let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
3050                            else {
3051                                break;
3052                            };
3053                            if page_hi - page_lo > cap {
3054                                break;
3055                            }
3056                            selected = i + 1;
3057                            selected_span = Some((page_lo, page_hi));
3058                            continue;
3059                        }
3060                        if selected != 0 {
3061                            break;
3062                        }
3063                        let mut span = None;
3064                        let mut best: Option<(usize, (usize, usize))> = None;
3065                        for take in 1..=run.len() {
3066                            let Some(next) = add_layer_span(span, *first + take - 1) else {
3067                                break;
3068                            };
3069                            // A partial final run cannot encode the lm_head;
3070                            // only its complete form above may include it.
3071                            let Some((page_lo, page_hi)) = page_span(next, false) else {
3072                                break;
3073                            };
3074                            if page_hi - page_lo > cap {
3075                                break;
3076                            }
3077                            span = Some((page_lo, page_hi));
3078                            best = Some((take, (page_lo, page_hi)));
3079                        }
3080                        let Some((take, page)) = best else {
3081                            break;
3082                        };
3083                        selected = i + 1;
3084                        selected_span = Some(page);
3085                        if take != run.len() {
3086                            gdn_prefix = Some((i, take));
3087                            break 'items;
3088                        }
3089                    }
3090                }
3091            }
3092            if selected != 0 {
3093                if let Some((item, take)) = gdn_prefix {
3094                    match &mut plan[item] {
3095                        Item::Gdn { run, .. } => run.truncate(take),
3096                        Item::Sconv { .. } | Item::Attn { .. } => {
3097                            unreachable!("only a GDN run is splittable")
3098                        }
3099                    }
3100                }
3101                let (page_lo, page_hi) = selected_span.expect("selected page span");
3102                tracing::info!(
3103                    page_start = page_lo,
3104                    page_end = page_hi,
3105                    page_mb = (page_hi - page_lo) / (1024 * 1024),
3106                    cap_mb = cap / (1024 * 1024),
3107                    graph_items = selected,
3108                    "native Metal MoE graph uses transient bounded weight page"
3109                );
3110                plan.truncate(selected);
3111                pager_span = Some((page_lo, page_hi));
3112            } else {
3113                tracing::warn!(
3114                    cap_mb = cap / (1024 * 1024),
3115                    "native Metal MoE pager declined: one graph item does not fit its safe page cap"
3116                );
3117            }
3118        }
3119        // A native MoE graph binds every WeightArena window to its jobs
3120        // kernels. On UMA, an mmap that already exceeds the driver's
3121        // recommended working set will therefore fail after admission, when
3122        // some recurrent/KV scratch has already been touched. Decline before
3123        // allocating/encoding the graph instead; the ordinary per-operation
3124        // route remains available and only materializes the needed window.
3125        // Do not apply this to dense graphs: they bind one projection at a
3126        // time and do not have the MoE jobs' all-window lifetime.
3127        if has_moe && needs_bounded_pager && pager_span.is_none() {
3128            tracing::info!(
3129                model_bytes,
3130                metal_working_set,
3131                "native Metal MoE graph declined before allocation"
3132            );
3133            self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
3134            if self.graph_head_required {
3135                self.fail_metal_graph(MOE_WORKING_SET_REASON);
3136            }
3137            return start;
3138        }
3139        let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
3140        let dev_attend = attend_contract
3141            && (self.head_dim <= 128
3142                || has_moe
3143                // A GDN hybrid attends on a quarter of its layers: the
3144                // hd>128 caution was measured on pure-dense models where
3145                // gqa_attend dominates, and on Qwen3.8-27B (hd 256, 48
3146                // GDN + 16 attn) the sandwich costs 2x the whole decode
3147                // (1.2 vs 2.21 tok/s measured before the arena fix).
3148                || (self.head_dim <= 256 && has_gdn)
3149                // Sliding-window layers bound most attends by the window:
3150                // Spark-X2.5-1.7B q4tp on the M4 decodes 67 tok/s
3151                // device-attended against 23 sandwiched (28 syncs/token).
3152                || (self.head_dim <= 256 && swa_graph)
3153                || attend_mode == "force"
3154                || attend_mode == "256");
3155        if !dev_attend {
3156            for it in &mut plan {
3157                if let Item::Attn { li, full_gpu, .. } = it {
3158                    // The hd>128 policy is about gqa_attend; an o1 layer
3159                    // attends through its own kernel set.
3160                    let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
3161                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
3162                    if !keep_o1 {
3163                        *full_gpu = false;
3164                    }
3165                }
3166            }
3167        }
3168        if std::env::var("CMF_GRAPH_DBG").is_ok() {
3169            use std::sync::atomic::{AtomicBool, Ordering};
3170            static SAID: AtomicBool = AtomicBool::new(false);
3171            if !SAID.swap(true, Ordering::Relaxed) {
3172                let fg = plan
3173                    .iter()
3174                    .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
3175                    .count();
3176                let att = plan
3177                    .iter()
3178                    .filter(|it| matches!(it, Item::Attn { .. }))
3179                    .count();
3180                eprintln!(
3181                    "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
3182                    plan.len(),
3183                    self.head_dim,
3184                    self.rotary_dim,
3185                    self.num_kv_heads,
3186                    self.num_heads,
3187                );
3188            }
3189        }
3190        let dims = GraphDims {
3191            hidden: self.hidden_size,
3192            eps: self.rms_eps as f32,
3193            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3194        };
3195        // The outer `forward_layers_span` scope is one logical token.  This
3196        // guard starts at q1_graph_gpu entry and ends after all page readback,
3197        // so a multi-page token cannot be reported as several whole tokens.
3198        let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
3199            crate::gpu_metal::MoePagerPageScope::new(
3200                _mt0,
3201                position,
3202                start,
3203                page_lo,
3204                page_hi,
3205                // `pager_span` is selected only after a cap was admitted;
3206                // keep the profiler observational if that invariant is ever
3207                // loosened rather than introducing a new inference panic.
3208                pager_cap.unwrap_or(0),
3209            )
3210        });
3211        let graph_new = match pager_span {
3212            Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3213            None => TokenGraph::new(&model, dims, h),
3214        };
3215        let Some(mut graph) = graph_new else {
3216            if self.graph_head_required {
3217                self.fail_metal_graph("native TokenGraph allocation refused");
3218            }
3219            return start;
3220        };
3221        if swa_graph && self.head_dim > 128 {
3222            // Spark-X2.5 (hd 256, 4 Q heads per KV head): the GQA-shared
3223            // split-K attend reads each K/V row once for the group instead
3224            // of once per head plus the importance re-read — at depth 1000
3225            // on the M4, 59.5 tok/s against 50.0 with the per-head kernel
3226            // up to the default 512 (every sliding layer sits at <= 512).
3227            // From the second position on and in 32-position blocks (more
3228            // threadgroups in flight for a latency-bound walk): Spark-X2.6
3229            // q4tp decode 0.86x the GPU time short and 0.88x at depth 1000,
3230            // q8_2f 0.91x / 0.92x (in-process A/B, M4).
3231            graph.set_attend_blk(1, 32);
3232        }
3233        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3234            nv: cfg.num_v_heads,
3235            nk: cfg.num_k_heads,
3236            dk: cfg.key_head_dim,
3237            dv: cfg.value_head_dim,
3238            kk: cfg.conv_kernel,
3239            hidden: self.hidden_size,
3240            inter: self.intermediate_size,
3241            c_dim: cfg.conv_dim(),
3242            eps: cfg.rms_eps as f32,
3243            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3244        });
3245        // Validate the whole plan BEFORE encoding anything: after the
3246        // first sync a refused layer would leave the token
3247        // half-executed, so truncate to the provably encodable prefix.
3248        let mut valid = 0usize;
3249        let mut end = start;
3250        crate::gpu::stageprof(1, _mt0.elapsed()); // конец планирования
3251        if std::env::var("CMF_PLAN_DUMP").is_ok() {
3252            static ONCE: std::sync::Once = std::sync::Once::new();
3253            ONCE.call_once(|| {
3254                for it in &plan {
3255                    match it {
3256                        Item::Gdn { first, run } => {
3257                            eprintln!("plan: Gdn first={first} len={}", run.len())
3258                        }
3259                        Item::Sconv { first, run } => {
3260                            eprintln!("plan: Sconv first={first} len={}", run.len())
3261                        }
3262                        Item::Attn { li, full_gpu, .. } => {
3263                            eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3264                        }
3265                    }
3266                }
3267            });
3268        }
3269        for item in &plan {
3270            let ok = match item {
3271                Item::Gdn { run, .. } => gcfg
3272                    .as_ref()
3273                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3274                    .unwrap_or(false),
3275                Item::Sconv { run, .. } => self
3276                    .short_conv_cfg
3277                    .is_some_and(|sc| run.iter().all(|l| graph.sconv_ok(l, sc.kernel))),
3278                Item::Attn { l, .. } => graph.attn_ok(l),
3279            };
3280            if !ok {
3281                if block_diag {
3282                    eprintln!(
3283                        "block-graph: plan item {} ({}) failed graph preflight",
3284                        valid,
3285                        match item {
3286                            Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3287                            Item::Sconv { run, first } => {
3288                                format!("short-conv run L{first}+{}", run.len())
3289                            }
3290                            Item::Attn { li, .. } => format!("Attn L{li}"),
3291                        }
3292                    );
3293                }
3294                break;
3295            }
3296            valid += 1;
3297            end += match item {
3298                Item::Gdn { run, .. } => run.len(),
3299                Item::Sconv { run, .. } => run.len(),
3300                Item::Attn { .. } => 1,
3301            };
3302        }
3303        plan.truncate(valid);
3304        if plan.is_empty() {
3305            if self.graph_head_required {
3306                self.fail_metal_graph("native graph preflight produced no valid items");
3307            }
3308            return start;
3309        }
3310
3311        if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3312            self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3313            return start;
3314        }
3315
3316        // Plain dense decode (every item a device-attended full-attention
3317        // layer with a dense FFN, no O(1) state): the only plan shape the
3318        // masked-nibble q4tp matvec and the concurrent layer encoder were
3319        // measured on (MiniCPM5-2B, Qwen3-0.6B on the M4). Hybrids, MoE and
3320        // o1 layers keep the historical serial path bit for bit.
3321        // Every projection must be ONE dispatch (q1t adds an overlay pass,
3322        // Prism q2tp a transform pass — dependent pairs a concurrent
3323        // encoder would race).
3324        let one_pass = |t: (usize, usize, usize)| {
3325            use cortiq_core::TensorDtype as D;
3326            matches!(
3327                model.tensors[t.0].dtype,
3328                D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3329            )
3330        };
3331        let dense_fast = plan.iter().all(|it| match it {
3332            Item::Attn {
3333                l, li, full_gpu, ..
3334            } => {
3335                *full_gpu
3336                    && self.kv_cache.layers[*li].o1.is_none()
3337                    && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3338                    && match l.ffn {
3339                        MetalFfn::Dense { gate, up, down, .. } => {
3340                            one_pass(gate) && one_pass(up) && one_pass(down)
3341                        }
3342                        _ => false,
3343                    }
3344            }
3345            // A short-conv run encodes serially in its own encoder: its
3346            // one-pass projections take the same matvec levers.
3347            Item::Sconv { run, .. } => run.iter().all(|l| {
3348                one_pass(l.in_proj)
3349                    && one_pass(l.out)
3350                    && match l.ffn {
3351                        MetalFfn::Dense { gate, up, down, .. } => {
3352                            one_pass(gate) && one_pass(up) && one_pass(down)
3353                        }
3354                        _ => false,
3355                    }
3356            }),
3357            Item::Gdn { .. } => false,
3358        });
3359        // MoE decode with every layer device-attended (Mellum2.1): the
3360        // routed experts' jobs ladder and the q8_2f attention projections
3361        // take their measured levers (`MOE_*`, `DENSE_Q8R4`).
3362        let moe_plan = !dense_fast
3363            && plan.iter().all(|it| match it {
3364                Item::Attn {
3365                    l, li, full_gpu, ..
3366                } => {
3367                    *full_gpu
3368                        && self.kv_cache.layers[*li].o1.is_none()
3369                        && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3370                }
3371                Item::Gdn { .. } | Item::Sconv { .. } => false,
3372            });
3373        let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3374        if block_diag {
3375            use std::sync::atomic::{AtomicBool, Ordering};
3376            static SAID_AB: AtomicBool = AtomicBool::new(false);
3377            if !SAID_AB.swap(true, Ordering::Relaxed) {
3378                eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3379            }
3380        }
3381        let _mv_fast = match ab {
3382            Some((bits, _)) => {
3383                graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3384                crate::gpu_metal::MvFastGuard::set_raw(bits)
3385            }
3386            None if moe_plan => {
3387                graph.set_dense_concurrent(
3388                    crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3389                );
3390                crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3391            }
3392            None => {
3393                graph.set_dense_concurrent(dense_fast);
3394                // Spark-X2.5 q8_2f: the four-row q8_2f matvec decodes the
3395                // 1.7B at 40.5 tok/s on the M4 against 27.7 one-row.
3396                let q8r4 = if swa_graph {
3397                    crate::gpu_metal::DENSE_Q8R4
3398                } else {
3399                    0
3400                };
3401                crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3402                    crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3403                } else {
3404                    0
3405                })
3406            }
3407        };
3408
3409        // RoPE table and rotary width are per layer (`layer_inv_freq`,
3410        // `layer_geom`) inside the loop below.
3411        let pool = self.pool.clone();
3412        let (nh, nkv, hd, hs, eps) = (
3413            self.num_heads,
3414            self.num_kv_heads,
3415            self.head_dim,
3416            self.hidden_size,
3417            self.rms_eps,
3418        );
3419        let norm_style = self.norm_style;
3420        let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3421        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3422        let kv_id = self.graph_kv_id;
3423        // GDN runs whose states await readback after the next sync
3424        // (device-attended layers add no sync, so several may stack).
3425        let mut pending: Vec<(usize, usize)> = Vec::new();
3426        // Device-attended layers: their K/V/imp are pulled from the
3427        // mirror after the final sync.
3428        let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3429        for item in &plan {
3430            let _xt0 = std::time::Instant::now();
3431            let _xkind: u32 = match item {
3432                Item::Gdn { .. } | Item::Sconv { .. } => 2,
3433                Item::Attn { .. } => 3,
3434            };
3435            // Looped Transformer: insert on-device norm at loop boundaries.
3436            if self.loop_final_norm {
3437                let item_start = match item {
3438                    Item::Gdn { first, .. } | Item::Sconv { first, .. } => *first,
3439                    Item::Attn { li, .. } => *li,
3440                };
3441                if item_start > start && self.is_loop_end(item_start - 1) {
3442                    graph.encode_loop_norm(&self.weights.final_norm);
3443                }
3444            }
3445            match item {
3446                Item::Gdn { run, first } => {
3447                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3448                        if l.linear_state.len() != want {
3449                            l.linear_state = vec![0f32; want];
3450                        }
3451                    }
3452                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3453                        .iter()
3454                        .map(|l| l.linear_state.as_slice())
3455                        .collect();
3456                    let _ig = std::time::Instant::now();
3457                    if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3458                        // Unreachable: the plan was validated above.
3459                        tracing::error!("q1 graph: GDN run refused after validation");
3460                        return start;
3461                    }
3462                    // Early commit: the GPU starts the run while the
3463                    // CPU encodes the next layer (nothing to wait on).
3464                    graph.commit_kind = 2;
3465                    graph.commit();
3466                    crate::gpu::stageprof(0, _ig.elapsed());
3467                    pending.push((*first, run.len()));
3468                }
3469                Item::Sconv { run, first } => {
3470                    let sc = self.short_conv_cfg.expect("validated short-conv run");
3471                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3472                        if l.linear_state.len() != sc.state_len() {
3473                            l.linear_state = vec![0f32; sc.state_len()];
3474                        }
3475                    }
3476                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3477                        .iter()
3478                        .map(|l| l.linear_state.as_slice())
3479                        .collect();
3480                    if !graph.encode_sconv_run(run, &ro, sc.kernel) {
3481                        tracing::error!("q1 graph: short-conv run refused after validation");
3482                        return start;
3483                    }
3484                    graph.commit_kind = 2;
3485                    graph.commit();
3486                    pending.push((*first, run.len()));
3487                }
3488                Item::Attn {
3489                    l,
3490                    li,
3491                    q_norm,
3492                    k_norm,
3493                    output_gate,
3494                    proj_gate,
3495                    head_gate_w,
3496                    bias,
3497                    full_gpu,
3498                } => {
3499                    let _ia = std::time::Instant::now();
3500                    // This layer's attention geometry: its window, RoPE
3501                    // table and rotary width (Spark-X2.5 interleaves
3502                    // 512-window layers rotating all dims at θ 1e4 with
3503                    // full layers rotating a quarter at θ 5e6). Every model
3504                    // without sliding layers reads the global ones here.
3505                    let inv_freq_l = self.layer_inv_freq(*li);
3506                    let rd_l = self.layer_geom(*li).2;
3507                    let window_l = self.layer_window(*li);
3508                    let head_gate_w = *head_gate_w;
3509                    // This layer's KV heads and head width: Gemma-4's global
3510                    // layers (one 512-wide KV head) differ from the base.
3511                    let (nkv, hd) = if gemma4 {
3512                        let (a, b, _) = self.layer_geom(*li);
3513                        (a, b)
3514                    } else {
3515                        (nkv, hd)
3516                    };
3517                    // ── Fully device-resident attention: no sync at all.
3518                    if *full_gpu {
3519                        let cache = &self.kv_cache.layers[*li];
3520                        let o1p = if cache.o1.is_some() {
3521                            match cache.o1_views() {
3522                                Some(views) => Some(crate::gpu::O1AttnParams {
3523                                    views,
3524                                    epoch: self.o1_epoch,
3525                                }),
3526                                // Sealed state gone mid-run: sandwich.
3527                                None => None,
3528                            }
3529                        } else {
3530                            None
3531                        };
3532                        let o1_layer = cache.o1.is_some();
3533                        if o1_layer && o1p.is_none() {
3534                            // fall to the sandwich (CPU o1 step)
3535                        }
3536                        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3537                        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3538                        let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3539                        // A trimmed tail always holds the window the
3540                        // device attend reads (`first = rows + 1 − w`).
3541                        debug_assert!(
3542                            cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3543                        );
3544                        let p = crate::gpu::AttnDeviceParams {
3545                            kv_id,
3546                            layer: *li,
3547                            nh,
3548                            nkv,
3549                            hd,
3550                            rd: rd_l,
3551                            position,
3552                            scale: self.attn_scale,
3553                            eps: eps as f32,
3554                            gemma,
3555                            late_qk_norm: self.qk_norm_after_rope,
3556                            output_gate: *output_gate,
3557                            q_norm: *q_norm,
3558                            k_norm: *k_norm,
3559                            inv_freq: &inv_freq_l,
3560                            rope_scale: self.layer_rope_scale(*li),
3561                            cpu_k,
3562                            cpu_v,
3563                            cpu_stored,
3564                            cpu_gen: cache.generation(),
3565                            o1: o1p,
3566                            window: window_l,
3567                            softmax_sink: cache.sinks.as_deref(),
3568                            head_gate: head_gate_w,
3569                            v_norm: self.attn_v_norm,
3570                        };
3571                        let o1_bad = o1_layer && p.o1.is_none();
3572                        if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3573                        {
3574                            // o1 layers leave no mirror row to pull.
3575                            if p.o1.is_none() {
3576                                dev_attn.push((*li, nkv, hd));
3577                            }
3578                            graph.commit_kind = 3;
3579                            graph.commit();
3580                            // The footer below is skipped by `continue`:
3581                            // account the device-attn item here or its
3582                            // cost hides from the stage profile entirely.
3583                            crate::gpu::stageprof(_xkind, _xt0.elapsed());
3584                            continue;
3585                        }
3586                        // Mirror refused (nothing encoded) → sandwich.
3587                    }
3588                    graph.encode_attn_prefix(l);
3589                    if let Err(err) = graph.sync_checked() {
3590                        self.fail_metal_graph(&err);
3591                        return start;
3592                    }
3593                    if !pending.is_empty() {
3594                        let idxs: Vec<usize> =
3595                            pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3596                        let mut outs: Vec<&mut [f32]> = self
3597                            .kv_cache
3598                            .layers
3599                            .iter_mut()
3600                            .enumerate()
3601                            .filter(|(i, _)| idxs.binary_search(i).is_ok())
3602                            .map(|(_, s)| s.linear_state.as_mut_slice())
3603                            .collect();
3604                        graph.read_states(&mut outs);
3605                    }
3606                    let mut q_raw = attention::take_buf(l.wq.1);
3607                    let mut k = attention::take_buf(l.wk.1);
3608                    let mut v = attention::take_buf(l.wv.1);
3609                    graph.read_qkv(&mut q_raw, &mut k, &mut v);
3610                    // Projected output gate: g = G·norm(h) on the host, off
3611                    // the normed input the prefix left on the device.
3612                    let mut gate_raw = proj_gate.map(|(gp, _)| {
3613                        let mut normed = attention::take_buf(hs);
3614                        graph.read_normed(&mut normed);
3615                        let mut raw = attention::take_buf(gp.rows());
3616                        gp.matvec(&normed, &mut raw, pool.as_deref());
3617                        attention::recycle_buf(&mut normed);
3618                        raw
3619                    });
3620                    let cfg = QwenAttnCfg {
3621                        mrope: None,
3622                        num_heads: nh,
3623                        num_kv_heads: nkv,
3624                        head_dim: hd,
3625                        hidden_size: hs,
3626                        position,
3627                        inv_freq: &inv_freq_l,
3628                        rotary_dim: rd_l,
3629                        scale: self.attn_scale,
3630                        softcap: self.attn_softcap,
3631                        window: window_l,
3632                        // Gemma-4 only (the front gate refuses V norm
3633                        // otherwise).
3634                        v_norm: self.attn_v_norm,
3635                        qk_norm_after_rope: self.qk_norm_after_rope,
3636                        gate_sigmoid: self.proj_gate_sigmoid,
3637                        q_norm: *q_norm,
3638                        k_norm: *k_norm,
3639                        output_gate: *output_gate,
3640                        softplus_gate: None,
3641                        rope_scale: self.layer_rope_scale(*li),
3642                        bias: *bias,
3643                        rms_eps: eps,
3644                        norm_style,
3645                        pool: pool.as_deref(),
3646                        v_head_dim: hd,
3647                    };
3648                    // CMF_ATTN_ORACLE=1: diff the device attend against
3649                    // this CPU attend on identical inputs (bring-up).
3650                    let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3651                        || std::env::var("CMF_ATTN_DUMP").is_ok();
3652                    let _ = full_gpu;
3653                    let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3654                    let mut ao = attention::qwen_attention_core(
3655                        q_raw,
3656                        k,
3657                        v,
3658                        &mut self.kv_cache.layers[*li],
3659                        &cfg,
3660                    );
3661                    // CMF_ATTN_DUMP=<dir>: this token's rope'd Q and the layer's whole
3662                    // K/V cache as raw f32 (offline attention-statistics probes:
3663                    // block bounds, mass concentration). Needs CMF_GPU_ATTEND=0.
3664                    if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3665                        if let Some((qr0, k0, v0)) = oracle_in.clone() {
3666                            let (cq, _cg, _ck, _cv) =
3667                                attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3668                            let cache = &self.kv_cache.layers[*li];
3669                            let n = cache.head_keys(0).len() / hd;
3670                            let mut bytes: Vec<u8> = Vec::new();
3671                            for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3672                                bytes.extend_from_slice(&v.to_le_bytes());
3673                            }
3674                            for v in &cq {
3675                                bytes.extend_from_slice(&v.to_le_bytes());
3676                            }
3677                            for g in 0..nkv {
3678                                for v in cache.head_keys(g) {
3679                                    bytes.extend_from_slice(&v.to_le_bytes());
3680                                }
3681                            }
3682                            for g in 0..nkv {
3683                                for v in cache.head_values(g) {
3684                                    bytes.extend_from_slice(&v.to_le_bytes());
3685                                }
3686                            }
3687                            let _ =
3688                                std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3689                        }
3690                    }
3691                    if let Some((qr0, k0, v0)) =
3692                        oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3693                    {
3694                        let (cq, _cg, ck, cv) =
3695                            attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3696                        let mut h_now = vec![0f32; hs];
3697                        graph.read_h(&mut h_now);
3698                        let cache = &self.kv_cache.layers[*li];
3699                        let n_after = cache.head_keys(0).len() / hd;
3700                        // A sealed O(1) cache may have no dense current-row
3701                        // entry. The oracle is a debug probe, so let it see
3702                        // zero stored exact rows instead of underflowing.
3703                        let stored = n_after.saturating_sub(1);
3704                        let cpu_k: Vec<&[f32]> = (0..nkv)
3705                            .map(|g| &cache.head_keys(g)[..stored * hd])
3706                            .collect();
3707                        let cpu_v: Vec<&[f32]> = (0..nkv)
3708                            .map(|g| &cache.head_values(g)[..stored * hd])
3709                            .collect();
3710                        let p = crate::gpu::AttnDeviceParams {
3711                            kv_id,
3712                            layer: *li,
3713                            nh,
3714                            nkv,
3715                            hd,
3716                            // The layer's own geometry, as the CPU attend
3717                            // above used it (the projected gate is applied
3718                            // after this probe on both sides).
3719                            rd: rd_l,
3720                            position,
3721                            scale: self.attn_scale,
3722                            eps: eps as f32,
3723                            gemma,
3724                            late_qk_norm: self.qk_norm_after_rope,
3725                            output_gate: *output_gate,
3726                            q_norm: *q_norm,
3727                            k_norm: *k_norm,
3728                            inv_freq: &inv_freq_l,
3729                            rope_scale: self.layer_rope_scale(*li),
3730                            cpu_k,
3731                            cpu_v,
3732                            cpu_stored: stored,
3733                            cpu_gen: cache.generation(),
3734                            o1: None,
3735                            window: window_l,
3736                            softmax_sink: cache.sinks.as_deref(),
3737                            head_gate: None,
3738                            v_norm: self.attn_v_norm,
3739                        };
3740                        if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3741                            let md = |a: &[f32], b: &[f32]| {
3742                                a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3743                            };
3744                            let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3745                            eprintln!(
3746                                "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}",
3747                                nn(&cq),
3748                                md(&cq, &dq),
3749                                nn(&ck),
3750                                md(&ck, &dk),
3751                                nn(&cv),
3752                                md(&cv, &dv),
3753                                nn(&ao),
3754                                md(&ao, &dao)
3755                            );
3756                        } else {
3757                            eprintln!("attn-oracle L{li}: device probe declined");
3758                        }
3759                    }
3760                    if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3761                        // V is as wide as the head (the front gate), so ao
3762                        // is nh·hd here, as in `qwen_attention`.
3763                        attention::apply_projected_gate(
3764                            &mut ao,
3765                            raw,
3766                            per_head,
3767                            hd,
3768                            self.proj_gate_sigmoid,
3769                        );
3770                    }
3771                    if let Some(mut raw) = gate_raw.take() {
3772                        attention::recycle_buf(&mut raw);
3773                    }
3774                    graph.encode_attn_suffix(l, &ao);
3775                    // Early commit: the GPU starts O+FFN while the CPU
3776                    // encodes the following GDN run / attention prefix.
3777                    graph.commit();
3778                    attention::recycle_buf(&mut ao);
3779                }
3780            }
3781
3782            crate::gpu::stageprof(_xkind, _xt0.elapsed());
3783        }
3784        // Ride the final norm + lm_head in the same command buffer when
3785        // this run reaches the model's end and the caller wants logits:
3786        // the separate per-op lm_head submit (a full round trip) folds
3787        // into the sync that already happens here.
3788        let mut lm_rows = None;
3789        if self.graph_want_logits
3790            && upto.is_none()
3791            && end == self.num_layers
3792            && std::env::var("CMF_GPU_LMHEAD")
3793                .map(|v| v != "0")
3794                .unwrap_or(true)
3795        {
3796            if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3797                if graph.lm_head_ok(lm) {
3798                    graph.encode_lm_head(&self.weights.final_norm, lm);
3799                    lm_rows = Some(lm.1);
3800                }
3801            }
3802        }
3803        if self.graph_head_required && lm_rows.is_none() {
3804            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3805            self.fail_metal_graph("fused graph head was requested but not encodable");
3806            return start;
3807        }
3808        let _sy0 = std::time::Instant::now();
3809        if let Err(err) = graph.sync_checked() {
3810            if let Some(profile) = pager_profile_page.as_mut() {
3811                profile.finish(start, graph.pager_page_stats(), "sync-error");
3812            }
3813            self.fail_metal_graph(&err);
3814            return start;
3815        }
3816        let _rs0 = std::time::Instant::now();
3817        if !pending.is_empty() {
3818            let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3819            let mut outs: Vec<&mut [f32]> = self
3820                .kv_cache
3821                .layers
3822                .iter_mut()
3823                .enumerate()
3824                .filter(|(i, _)| idxs.binary_search(i).is_ok())
3825                .map(|(_, s)| s.linear_state.as_mut_slice())
3826                .collect();
3827            graph.read_states(&mut outs);
3828        }
3829        if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3830            use std::sync::atomic::{AtomicU64, Ordering};
3831            static SY: AtomicU64 = AtomicU64::new(0);
3832            static RS: AtomicU64 = AtomicU64::new(0);
3833            static N: AtomicU64 = AtomicU64::new(0);
3834            SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3835            RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3836            let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3837            if n % 100 == 0 {
3838                eprintln!(
3839                    "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3840                    SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3841                    RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3842                );
3843            }
3844        }
3845        if let Some(rows) = lm_rows {
3846            crate::gpu::hostprof_encode_done(_mt0);
3847            let mut lg = attention::take_buf(rows.min(self.vocab_size));
3848            graph.read_logits(&mut lg);
3849            crate::gpu::hostprof_total(_mt0);
3850            lg.resize(self.vocab_size, 0.0);
3851            if let Some(c) = self.final_softcap {
3852                softcap_in_place(&mut lg, c, self.pool.as_deref());
3853            }
3854            self.graph_logits = Some(lg);
3855        }
3856        graph.read_h(h);
3857        if self.graph_head_required && self.graph_logits.is_none() {
3858            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3859            self.fail_metal_graph("fused graph head completed without logits readback");
3860            return start;
3861        }
3862        METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3863        METAL_GRAPH_LAYERS.fetch_add(
3864            end.saturating_sub(start) as u64,
3865            std::sync::atomic::Ordering::Relaxed,
3866        );
3867        if self.graph_head_required {
3868            METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3869        }
3870        // Device-attended layers: replay the CPU bookkeeping — append
3871        // the mirror's new K/V row (rope'd on the GPU) into the owner
3872        // cache, then bank this token's attention-importance mass.
3873        for (li, nkv, hd) in dev_attn {
3874            let mut krow = attention::take_buf(nkv * hd);
3875            let mut vrow = attention::take_buf(nkv * hd);
3876            if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3877                let cache = &mut self.kv_cache.layers[li];
3878                cache.append(&krow, &vrow, &[]);
3879                let n = cache.seq_len;
3880                let mut imp = attention::take_buf(n);
3881                crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3882                cache.accumulate_imp(&imp);
3883                attention::recycle_buf(&mut imp);
3884            }
3885            attention::recycle_buf(&mut krow);
3886            attention::recycle_buf(&mut vrow);
3887        }
3888        if let Some(profile) = pager_profile_page.as_mut() {
3889            profile.finish(end, graph.pager_page_stats(), "page-complete");
3890        }
3891        if let Some((_, arm)) = ab {
3892            crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3893        }
3894        end
3895    }
3896
3897    pub fn new(
3898        tokenizer: Tokenizer,
3899        weights: PipelineWeights,
3900        hidden_size: usize,
3901        intermediate_size: usize,
3902        num_heads: usize,
3903        num_kv_heads: usize,
3904        head_dim: usize,
3905        num_layers: usize,
3906        physical_layers: usize,
3907        loop_final_norm: bool,
3908        vocab_size: usize,
3909        rms_eps: f64,
3910        rope_base: f32,
3911        norm_style: NormStyle,
3912        max_seq_len: usize,
3913        sampler_config: SamplerConfig,
3914    ) -> Self {
3915        let rng = match sampler_config.seed {
3916            Some(s) => SplitMix64::new(s),
3917            None => SplitMix64::from_entropy(),
3918        };
3919        let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3920        let pool = Pool::from_env();
3921        if let Some(p) = &pool {
3922            tracing::info!("worker pool: {} threads", p.n_workers());
3923            // Keep the workers on the socket that holds the weights.
3924            if let Some(model) = weights
3925                .lm_head
3926                .model_arc()
3927                .or_else(|| weights.embed_tokens.model_arc())
3928            {
3929                let regions: Vec<&[u8]> =
3930                    model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3931                p.bind_numa(&regions);
3932            }
3933        }
3934        Self {
3935            gpu_plan: None,
3936            tokenizer: std::sync::Arc::new(tokenizer),
3937            kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3938            sampler_config,
3939            weights,
3940            hidden_size,
3941            intermediate_size,
3942            num_heads,
3943            num_kv_heads,
3944            head_dim,
3945            num_layers,
3946            physical_layers,
3947            loop_final_norm,
3948            layer_map: None,
3949            mhc: None,
3950            mhc_r: Vec::new(),
3951            mrope_sections: None,
3952            prompt_mrope: None,
3953            vocab_size,
3954            rms_eps,
3955            rope_base,
3956            norm_style,
3957            rotary_dim: head_dim,
3958            attention_heads_per_layer: None,
3959            kv_heads_per_layer: None,
3960            v_head_dim: None,
3961            layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3962                .filter(|v| !v.is_empty())
3963                .map(std::path::PathBuf::from),
3964            graph_declines: std::cell::RefCell::new(Vec::new()),
3965            mimo_moe: Default::default(),
3966            minicpm_tail: Default::default(),
3967            vmf_cfg: None,
3968            gdn_cfg: None,
3969            kda_cfg: None,
3970            g3n: None,
3971            g4e: None,
3972            dsv4: None,
3973            dsv41: None,
3974            dsv41_vision: None,
3975            dsv41_prefill: None,
3976            qwen4_exp: None,
3977            clover: None,
3978            clover_stats: clover::CloverStats::default(),
3979            clover_pen: Default::default(),
3980            dsv4_mtp: Vec::new(),
3981            dspark: None,
3982            dspark_pending: Vec::new(),
3983            dspark_hist: Vec::new(),
3984            dspark_real: Vec::new(),
3985            dspark_trunk_picks: Vec::new(),
3986            dspark_exp: Vec::new(),
3987            dspark_draft_ns: 0,
3988            m3_winners: Vec::new(),
3989            m3_counts: Vec::new(),
3990            m3_n_exp: 0,
3991            m3_tokens: 0,
3992            m3_prewarmed: false,
3993            logit_multiplier: None,
3994            cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3995            graph_failed: std::sync::atomic::AtomicBool::new(false),
3996            graph_moe_banks: std::sync::Mutex::new(std::collections::HashMap::new()),
3997            kv_history: Vec::new(),
3998            kv_history_device: false,
3999            short_conv_cfg: None,
4000            mtp: None,
4001            mimo_mtp: None,
4002            verify_exact_moe: false,
4003            speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
4004            ignore_eos: false,
4005            draft_full_streak: 0,
4006            spec_k_adapt: None,
4007            spec_acc_ewma: 0.7,
4008            rng,
4009            sampler_scratch: SamplerScratch::default(),
4010            spec_forced: None,
4011            spec_q: Vec::new(),
4012            spec_p: Vec::new(),
4013            spec_res: Vec::new(),
4014            spec_qs: Vec::new(),
4015            spec_ps: Vec::new(),
4016            spec_ress: Vec::new(),
4017            mtp_graph_mode: None,
4018            #[cfg(target_os = "macos")]
4019            metal_verify: None,
4020            inv_freq,
4021            ws: ForwardScratch::new(hidden_size),
4022            pool,
4023            model: None,
4024            dyn_force_f32: false,
4025            dyn_skill_layers: Vec::new(),
4026            dyn_active: None,
4027            dyn_blend_loaded: false,
4028            dyn_phi_layer: None,
4029            dyn_phi_ema: Vec::new(),
4030            dyn_phi_seen: 0,
4031            dyn_router: None,
4032            o1_cfg: None,
4033            o1_epoch: 0,
4034            o1_flags: Vec::new(),
4035            trace: false,
4036            calib_temp: 1.0,
4037            confidence_on: true,
4038            embed_multiplier: 1.0,
4039            attn_scale: 1.0 / (head_dim as f32).sqrt(),
4040            swa: None,
4041            swa_trim: None,
4042            swa_trim_anchor: usize::MAX,
4043            chunk_min_rows: 32,
4044            sliding_layers: None,
4045            no_rope_layers: None,
4046            anchor_core: None,
4047            bounded_rope: None,
4048            kv_prefix: KvPrefix::default(),
4049            last_prefill_tokens: 0,
4050            inv_freq_local: None,
4051            rotary_dim_local: None,
4052            rope_scale: 1.0,
4053            rope_scale_local: 1.0,
4054            global_attn: None,
4055            inv_freq_global: None,
4056            attn_v_norm: false,
4057            qk_norm_after_rope: false,
4058            proj_gate_sigmoid: false,
4059            final_softcap: None,
4060            head_clusters: None,
4061            attn_softcap: 0.0,
4062            graph_want_logits: false,
4063            graph_head_required: false,
4064            graph_logits: None,
4065            embryo_graph: None,
4066            graph_refused: std::sync::atomic::AtomicBool::new(false),
4067            graph_kv_id: {
4068                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
4069                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
4070            },
4071            #[cfg(test)]
4072            nll_test_fail_at: None,
4073            #[cfg(test)]
4074            nll_test_force_serial: false,
4075        }
4076    }
4077
4078    /// Enable/disable per-layer O(1) Nyström attention. Only Full
4079    /// layers are eligible (a linear layer keeps its own operator).
4080    /// Applies to generation (`generate*`/`forward_ids`): the prompt
4081    /// pass stays exact, then the state seals after prefill or at the
4082    /// deferred skeleton-safe boundary for short prompts; decode runs on
4083    /// the O(1) state. Teacher-forced scoring (`ppl_ids`) intentionally
4084    /// stays exact.
4085    pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
4086        if let Err(e) = self.try_set_o1(cfg) {
4087            tracing::error!("{e}");
4088        }
4089    }
4090
4091    /// True when the file carries a native bounded anchor
4092    /// (`arch.anchor_core`): its state is a fixed record the header
4093    /// fixes, and the post-hoc O(1) override is meaningless on it.
4094    pub fn bounded_native(&self) -> bool {
4095        self.anchor_core.is_some()
4096    }
4097
4098    /// Bytes the resident device graph holds for this pipeline's sequence:
4099    /// `(recurrent state, anchor KV/ring)`; None on the host path.
4100    pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
4101        crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
4102    }
4103
4104    /// Why an O(1) override is refused on this pipeline, if it is.
4105    pub fn o1_refusal(&self) -> Option<String> {
4106        self.anchor_core.as_ref().map(|ac| {
4107            format!(
4108                "--o1 / CMF_O1 refused: the anchor is native bounded \
4109                 (anchor_core kind={} window={} sink={}); the file's operator \
4110                 is executed as-is and no post-hoc Nyström overlay applies",
4111                ac.kind, ac.window, ac.sink
4112            )
4113        })
4114    }
4115
4116    /// `set_o1` that reports the refusal instead of logging it.
4117    pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
4118        if let Some(c) = &cfg {
4119            if let Some(why) = self.o1_refusal() {
4120                self.o1_flags = Vec::new();
4121                self.o1_cfg = None;
4122                return Err(why);
4123            }
4124            if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
4125                self.o1_flags.clear();
4126                self.o1_cfg = None;
4127                return Err(format!(
4128                    "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
4129                    c.w, c.sink
4130                ));
4131            }
4132        }
4133        self.o1_flags = match &cfg {
4134            Some(c) => {
4135                let mut flags = c.layer_flags(self.num_layers);
4136                for (li, f) in flags.iter_mut().enumerate() {
4137                    // The Nyström state replaces a full-context plain
4138                    // softmax: a sliding window or a learned sink is not
4139                    // something it can represent, and a V narrower than
4140                    // the head is not what its streaming state stores.
4141                    // Those layers keep exact cache attention.
4142                    if *f
4143                        && (!matches!(
4144                            self.weights.layers[self.phys_layer(li)].attn,
4145                            AttnKind::Full { .. }
4146                        ) || self.layer_window(li).is_some()
4147                            || self.kv_cache.layers[li].sinks.is_some()
4148                            || self.layer_v_dim(li) != self.layer_geom(li).1)
4149                    {
4150                        *f = false;
4151                    }
4152                }
4153                flags
4154            }
4155            None => Vec::new(),
4156        };
4157        if let Some(c) = &cfg {
4158            let n = self.o1_flags.iter().filter(|&&f| f).count();
4159            tracing::info!(
4160                "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
4161                self.num_layers,
4162                c.m,
4163                c.w,
4164                c.sink,
4165                c.rect
4166            );
4167        }
4168        self.o1_cfg = cfg;
4169        Ok(())
4170    }
4171
4172    /// Install the file's bounded anchor: one fixed-size ring per
4173    /// `AttnKind::Bounded` layer (from the header, not per prompt) and
4174    /// the shared relative-rotation table. Must run after the RoPE setup
4175    /// (`set_rotary`, YaRN) so the table is built from the final
4176    /// `inv_freq`.
4177    pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
4178        if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
4179            return Err(format!(
4180                "anchor_core kind '{}' is not executable by this runtime",
4181                cfg.kind
4182            ));
4183        }
4184        if cfg.window == 0 {
4185            return Err("anchor_core.window must be >= 1".into());
4186        }
4187        let mut n = 0usize;
4188        for li in 0..self.num_layers {
4189            let pl = self.phys_layer(li);
4190            if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
4191                if w.window != cfg.window
4192                    || w.sink != cfg.sink
4193                    || w.group_windows != cfg.group_windows
4194                {
4195                    return Err(format!(
4196                        "layer {li}: bounded weights (window {} sink {} groups {:?}) disagree \
4197                         with anchor_core (window {} sink {} groups {:?})",
4198                        w.window, w.sink, w.group_windows, cfg.window, cfg.sink, cfg.group_windows
4199                    ));
4200                }
4201                self.kv_cache.layers[li].install_bounded(cfg.window);
4202                n += 1;
4203            }
4204        }
4205        if n == 0 {
4206            return Err("anchor_core is present but no layer executes it".into());
4207        }
4208        let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
4209        self.bounded_rope = Some(std::sync::Arc::new(rope));
4210        self.anchor_core = Some(cfg.clone());
4211        self.embryo_graph = None;
4212        tracing::info!(
4213            "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
4214            cfg.kind,
4215            cfg.window,
4216            cfg.sink,
4217            self.kv_cache
4218                .layers
4219                .iter()
4220                .map(|l| l.bounded_state_bytes())
4221                .max()
4222                .unwrap_or(0)
4223        );
4224        Ok(())
4225    }
4226
4227    /// Tag every layer's cache with its wire record kind and the model's
4228    /// operator identity (hash64 of `linear_core_identity` JSON) so the
4229    /// versioned state wire refuses a peer holding another operator.
4230    pub fn install_wire_identity(&mut self, identity: u64) {
4231        for li in 0..self.kv_cache.layers.len() {
4232            let pl = self.phys_layer(li);
4233            let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
4234                Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
4235                Some(AttnKind::Linear(_))
4236                | Some(AttnKind::LinearGdn(_))
4237                | Some(AttnKind::ShortConv(_))
4238                | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
4239                _ => crate::kv_cache::WireKind::Full,
4240            };
4241            let l = &mut self.kv_cache.layers[li];
4242            l.wire_kind = kind;
4243            l.wire_identity = identity;
4244            // A per-layer geometry (`set_attn_geometry`, Gemma's global
4245            // heads) rebuilds a layer cache with index 0: re-tag it.
4246            l.wire_layer = li as u32;
4247        }
4248    }
4249
4250    /// Forget the reuse keys (legacy `kv_history` and the bounded prefix).
4251    pub fn clear_history(&mut self) {
4252        self.kv_history.clear();
4253        self.kv_history_device = false;
4254        self.kv_prefix.clear();
4255    }
4256
4257    /// Did THIS pipeline's token graph refuse for a structural reason?
4258    /// (Per pipeline: another lane's refusal, or a new pipeline of the
4259    /// same model, never changes it.)
4260    pub fn graph_refused(&self) -> bool {
4261        self.graph_refused
4262            .load(std::sync::atomic::Ordering::Relaxed)
4263    }
4264
4265    /// Remember a structural refusal of this pipeline's token graph.
4266    pub fn mark_graph_refused(&self) {
4267        if !self
4268            .graph_refused
4269            .swap(true, std::sync::atomic::Ordering::Relaxed)
4270        {
4271            tracing::info!(
4272                "token graph: unsupported for this pipeline (seq {}) — not retrying",
4273                self.graph_kv_id
4274            );
4275        }
4276    }
4277
4278    /// Position the resident Embryo graph holds for this pipeline's
4279    /// sequence (`Some(next position)`), `None` when the device holds no
4280    /// image of it (host-owned sequence, or none started).
4281    pub fn device_sequence_position(&self) -> Option<usize> {
4282        crate::gpu::embryo_device_next_position(self.graph_kv_id)
4283    }
4284
4285    /// Is the sequence this pipeline's reuse key describes owned by the
4286    /// device path it would take now? A prefix recorded on the resident
4287    /// graph continues only there (at exactly `n`), a host prefix only on
4288    /// the host; any mismatch means the next turn re-prefills from zero.
4289    fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4290        let dev = self.device_sequence_position();
4291        if recorded_on_device {
4292            dev == Some(n) && self.embryo_resident_wanted()
4293        } else {
4294            dev.is_none()
4295        }
4296    }
4297
4298    /// The weights under the sequence changed (a real skill switch): every
4299    /// cached state was computed by other weights. Clear the host KV /
4300    /// ring / recurrent state AND the reuse keys — a surviving `kv_prefix`
4301    /// would let the next turn "extend" a prefix the new weights never
4302    /// saw — drop the packed resident graph (it holds the old FFN
4303    /// tensors; the next build packs the live ones under a fresh id) and
4304    /// reset its device sequence.
4305    pub(crate) fn invalidate_for_weight_change(&mut self) {
4306        self.clear_sequence_state();
4307        self.embryo_graph = None;
4308    }
4309
4310    /// Prompt positions the cache already holds when `input_ids`
4311    /// strictly EXTENDS the consumed prefix (0 otherwise). Bounded-native
4312    /// models answer from the fixed-size `kv_prefix` record; everything
4313    /// else from the legacy `kv_history` vector (which the network split
4314    /// also reads and writes).
4315    fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4316        let (n, on_device) = if self.bounded_native() {
4317            (
4318                self.kv_prefix.extension(input_ids),
4319                self.kv_prefix.on_device(),
4320            )
4321        } else {
4322            let h = &self.kv_history;
4323            if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4324                (h.len(), self.kv_history_device)
4325            } else if !h.is_empty() && !self.kv_history_device {
4326                (self.rollback_prefix_len(input_ids), false)
4327            } else {
4328                (0, false)
4329            }
4330        };
4331        // The owner tag: the state the key describes must live where this
4332        // turn will continue it (R4) — else a fresh sequence.
4333        if n > 0 && !self.prefix_owner_matches(n, on_device) {
4334            tracing::warn!(
4335                "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4336                 the device now holds {:?} — re-prefilling from zero",
4337                if on_device { "resident device" } else { "host" },
4338                self.device_sequence_position()
4339            );
4340            return 0;
4341        }
4342        n
4343    }
4344
4345    /// Positions a turn whose ids DIVERGE from the consumed prefix keeps:
4346    /// their longest common prefix, when every layer can drop its rows past
4347    /// it exactly — full attention whose f32 rows the host owns (no wgpu
4348    /// mirror ahead of the host, no recurrent, O(1) or bounded state) and
4349    /// every trimmed sliding tail still holding the window the query at the
4350    /// cut reads. A chat app re-renders the previous answer through the
4351    /// template (a thinking model's history drops its `<think>` block), so a
4352    /// conversation almost never extends token for token: extension-only
4353    /// reuse re-prefilled the whole history every turn. One id at least is
4354    /// left to prefill. `CMF_KV_ROLLBACK=0` keeps extension-only reuse.
4355    fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
4356        if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
4357            || self.device_sequence_position().is_some()
4358        {
4359            return 0;
4360        }
4361        let h = &self.kv_history;
4362        let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
4363        let n = lcp.min(input_ids.len().saturating_sub(1));
4364        if n == 0 || self.kv_cache.layers.len() < self.num_layers {
4365            return 0;
4366        }
4367        let kv_id = self.graph_kv_id;
4368        for li in 0..self.num_layers {
4369            let l = &self.kv_cache.layers[li];
4370            let tail_ok = l.base() == 0
4371                || self
4372                    .layer_window(li)
4373                    .is_some_and(|w| l.base() + w <= n + 1);
4374            if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
4375                || l.o1.is_some()
4376                || l.bounded.is_some()
4377                || !l.linear_state.is_empty()
4378                || l.mode != crate::kv_cache::KvMode::F32
4379                || l.pos_len() != h.len()
4380                || !tail_ok
4381                || crate::gpu::graph_kv_stored(kv_id, li).is_some()
4382                || crate::gpu::graph_state_resident(kv_id, li)
4383            {
4384                return 0;
4385            }
4386        }
4387        n
4388    }
4389
4390    /// Public view of the prefix-reuse decision for `input_ids` (positions
4391    /// the next `generate*` would take from the cache; 0 = fresh sequence).
4392    pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4393        self.cached_prefix_len(input_ids)
4394    }
4395
4396    /// Record the forwarded prefix as the next turn's reuse key. A
4397    /// bounded-native model extends the rolling record (`reused` = the
4398    /// positions this turn found cached); others keep the literal vector.
4399    /// Either way the key carries its OWNER: the resident device graph
4400    /// (it holds an image of this sequence) or the host.
4401    fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4402        let on_device = self.device_sequence_position().is_some();
4403        if self.bounded_native() {
4404            let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4405            let prev_device = self.kv_prefix.on_device();
4406            self.kv_history.clear();
4407            self.kv_history_device = false;
4408            if keep && prev_device == on_device {
4409                self.kv_prefix.extend(&consumed[reused..]);
4410            } else {
4411                self.kv_prefix.set(consumed);
4412            }
4413            self.kv_prefix.set_on_device(on_device);
4414        } else {
4415            self.kv_history = consumed.to_vec();
4416            self.kv_history_device = on_device;
4417        }
4418    }
4419
4420    /// True when at least one layer runs the O(1) kernel.
4421    pub fn o1_active(&self) -> bool {
4422        self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4423    }
4424
4425    /// Whether generation's prompt ingest is routed through the whole-token
4426    /// graph.  The bench uses this to label the measured generation prefill
4427    /// honestly; keep the predicate in Pipeline so CLI labels cannot drift
4428    /// from the production route.
4429    /// Positions per batched-graph submit for the prompt: `CMF_BATCH_K`
4430    /// when set (0 = one position at a time through the token graph),
4431    /// otherwise 128 (32 for a MoE stack) on a discrete card whose prompt
4432    /// takes the graph route.
4433    /// The batched graph read a 2048-token prompt at 53 tok/s against 28.5
4434    /// one position at a time on an RTX PRO 4000 (Qwen3.8-27B q4tp: TTFT
4435    /// 39 s against 72), and its states are the speculative verify's,
4436    /// measured identical to the plain path. macOS keeps its own arm.
4437    /// 128, not 32: the q4tp prefill GEMM tiles 64 batch rows, so a
4438    /// 32-row chunk left half of every tile idle. Qwen3.8-27B q4tp on an
4439    /// RTX 3090, 1000-token prompt: 51 tok/s at 32, 82 at 64, 95 at 128,
4440    /// 97 at 256; greedy output byte-identical to the 32-row chunks
4441    /// (every GEMM row and the k-looped recurrences are per position).
4442    pub fn generation_batch_k(&self) -> usize {
4443        if let Some(k) = std::env::var("CMF_BATCH_K")
4444            .ok()
4445            .and_then(|v| v.parse::<usize>().ok())
4446        {
4447            return k;
4448        }
4449        // Gemma-4 (`g4_batch_prefill`): 128-row chunks, the GEMM tile's
4450        // two 64-row halves full.
4451        if self.g4_batch_prefill() {
4452            return 128;
4453        }
4454        // The sealed O(1) suffix of a bounded prompt (`CMF_O1_PREFILL`) on
4455        // the device route rides the batch graph, whose O(1) kernels take
4456        // the chunk in 64-row sub-chunks (`o1_batch`); 128 as the exact
4457        // prompt below. Without it the suffix went one position at a time.
4458        #[cfg(not(target_os = "macos"))]
4459        if self.o1_active()
4460            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
4461            && crate::gpu::enabled_here()
4462            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
4463        {
4464            return 128;
4465        }
4466        #[cfg(not(target_os = "macos"))]
4467        if self.graph_prefill_preferred() && !self.o1_active() {
4468            // 128 is measured on the dense hybrids; MoE stacks other than
4469            // the two expert codecs below keep the 32 their expert-grouped
4470            // prefill was measured at.
4471            let moe = self
4472                .weights
4473                .layers
4474                .iter()
4475                .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4476            // q8_2f experts (moe_q82's grouped prefill) read each expert
4477            // once per group of up to 16 of its entries, so the chunk that
4478            // fills those groups is the faster ingest: Mellum2.1 q8_2f on an
4479            // RTX 3090, 4000-token prompt, 503 tok/s at 64 rows, 542 at 128
4480            // (and 542 at 256).
4481            let q82 = crate::gpu::moe_q82_graph_on()
4482                && self.weights.layers.iter().any(|lw| {
4483                    matches!(&lw.ffn, FfnKind::Moe(m)
4484                        if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4485                });
4486            // q4tp experts: the grouped GEMM reads every routed expert once
4487            // per chunk, and 32 rows (256 entries over 256 experts, about
4488            // one row each) re-read nearly the whole expert set every 32
4489            // tokens. Apex-35B q4tp on an A40, 4096-token prompt, MTP off:
4490            // 295 tok/s at 32 rows, 357 at 64, 385 at 128, 401 at 256,
4491            // 396 at 512.
4492            let q4tp_moe = self.weights.layers.iter().any(|lw| {
4493                matches!(&lw.ffn, FfnKind::Moe(m)
4494                    if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q4tp().is_some()))
4495            });
4496            return if q82 {
4497                128
4498            } else if q4tp_moe || self.onevision2_prefill() {
4499                256
4500            } else if moe {
4501                32
4502            } else {
4503                128
4504            };
4505        }
4506        0
4507    }
4508
4509    pub fn generation_graph_prefill(&self) -> bool {
4510        let graph = self.graph_prefill_preferred();
4511        // On wgpu, an active MTP head now consumes the trunk's graph batches
4512        // and warms its own block from those returned rows.  The selected
4513        // generation measurement is therefore the batched path, even though
4514        // the underlying GDN model still satisfies the graph-prefill
4515        // predicate.  Keep the CLI label tied to the actual route.  Native
4516        // Metal has a separate prefill-batch arm and retains its historical
4517        // label here.
4518        // A batched prompt (`generation_batch_k` > 0) is the batched graph
4519        // for every model on the graph route, not only those with an MTP
4520        // head — the label follows the route.
4521        #[cfg(not(target_os = "macos"))]
4522        if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4523            return false;
4524        }
4525        graph
4526    }
4527
4528    /// Device-side O(1) mirrors currently uploaded for this pipeline's
4529    /// sequence.  The count/bytes are zero before seal or after a fresh
4530    /// reset; callers use this to distinguish logical host state from the
4531    /// GPU allocation that actually serves decode.
4532    pub fn o1_device_stats(&self) -> (usize, u64) {
4533        crate::gpu::o1_device_stats(self.graph_kv_id)
4534    }
4535
4536    /// Arm query collection on the o1 layers (fresh prompt pass).
4537    /// Reset the o1 layers to Collecting for a fresh sequence. Pub for the
4538    /// network split: each side runs the o1 lifecycle over ITS OWN layers
4539    /// (begin before prefill, seal at the prefill barrier).
4540    pub fn o1_begin(&mut self) {
4541        self.o1_begin_with_prefix(None);
4542    }
4543
4544    /// Arm collection and optionally request a positive calibration prefix.
4545    /// The effective barrier is always at least the skeleton-safe floor, so
4546    /// a short requested prefix cannot create an exact-only runtime state.
4547    pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4548        if let Some(c) = &self.o1_cfg {
4549            let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4550            let boundary = requested_prefix.map(|p| {
4551                p.max(
4552                    crate::nystrom::o1_deferred_boundary(w, sink)
4553                        .expect("o1 config boundary validated in set_o1"),
4554                )
4555            });
4556            for (li, &f) in self.o1_flags.iter().enumerate() {
4557                if f {
4558                    self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4559                }
4560            }
4561        }
4562    }
4563
4564    /// Effective deferred boundary for a positive prefix request.
4565    fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4566        self.o1_cfg.as_ref().and_then(|c| {
4567            crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4568                .map(|floor| requested_prefix.max(floor))
4569        })
4570    }
4571
4572    fn o1_note_transition(&mut self) {
4573        // Drain every layer's one-shot bit before publishing one pipeline
4574        // epoch. `any()` would short-circuit on the first layer and leak the
4575        // remaining bits into later forwards, causing one epoch per layer.
4576        let mut transitioned = false;
4577        for (li, &flagged) in self.o1_flags.iter().enumerate() {
4578            if flagged {
4579                transitioned |= self.kv_cache.layers[li].take_o1_transition();
4580            }
4581        }
4582        if transitioned {
4583            self.o1_epoch = self.o1_epoch.wrapping_add(1);
4584        }
4585    }
4586
4587    fn o1_pending(&self) -> bool {
4588        self.o1_flags.iter().enumerate().any(|(li, &f)| {
4589            f && self.kv_cache.layers[li].seq_len > 0
4590                && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4591        })
4592    }
4593
4594    fn o1_fail(&mut self, err: String) {
4595        tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4596        self.clear_sequence_state();
4597        self.graph_failed
4598            .store(true, std::sync::atomic::Ordering::Relaxed);
4599        self.cancel
4600            .store(true, std::sync::atomic::Ordering::Relaxed);
4601    }
4602
4603    /// Seal participating layers while retaining the exact state when the
4604    /// prompt is below the deferred boundary. A split worker may have
4605    /// collecting layers outside its owned span; zero-depth layers remain
4606    /// armed and are intentionally skipped until their peer runs them.
4607    pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4608        if self.o1_cfg.is_none() {
4609            return Ok(false);
4610        }
4611        let mut participating = false;
4612        for li in 0..self.num_layers {
4613            if !self.o1_flags.get(li).copied().unwrap_or(false) {
4614                continue;
4615            }
4616            if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4617                return Err(err);
4618            }
4619            if self.kv_cache.layers[li].seq_len == 0 {
4620                continue;
4621            }
4622            participating = true;
4623            let num_heads = self.layer_num_heads(li);
4624            self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4625        }
4626        self.o1_note_transition();
4627        for li in 0..self.num_layers {
4628            if self.o1_flags.get(li).copied().unwrap_or(false) {
4629                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4630                    return Err(err);
4631                }
4632            }
4633        }
4634        Ok(participating
4635            && (0..self.num_layers).all(|li| {
4636                !self.o1_flags.get(li).copied().unwrap_or(false)
4637                    || self.kv_cache.layers[li].seq_len == 0
4638                    || self.kv_cache.layers[li].o1_sealed()
4639            }))
4640    }
4641
4642    /// Complete a deferred boundary after a full position/span forward.
4643    /// This is the pipeline owner for epoch publication and failure cleanup.
4644    fn o1_progress(&mut self) {
4645        if !self.o1_active() {
4646            return;
4647        }
4648        for li in 0..self.num_layers {
4649            if self.o1_flags.get(li).copied().unwrap_or(false) {
4650                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4651                    self.o1_fail(err);
4652                    return;
4653                }
4654            }
4655        }
4656        // A qwen_attention row can seal in the middle of a complete layer
4657        // walk. Consume its transition even though the pending boundary has
4658        // already disappeared from the cache.
4659        self.o1_note_transition();
4660        if !self.o1_pending() {
4661            return;
4662        }
4663        if let Err(err) = self.o1_seal_checked() {
4664            self.o1_fail(err);
4665        }
4666    }
4667
4668    /// Turn a deferred O(1) failure raised by a hidden-only forward into the
4669    /// Result error its public batch/span caller must return. The failure
4670    /// path already cleared host/device sequence state; consume only the
4671    /// side-channel marker here and leave the pipeline reusable.
4672    fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4673        if self
4674            .graph_failed
4675            .swap(false, std::sync::atomic::Ordering::Relaxed)
4676        {
4677            self.cancel
4678                .store(false, std::sync::atomic::Ordering::Relaxed);
4679            self.clear_sequence_state();
4680            return Err(format!("{phase}: deferred O(1) transition failed"));
4681        }
4682        Ok(())
4683    }
4684
4685    /// Freeze landmarks + skeleton state after the prompt pass and drop
4686    /// the o1 layers' full KV; decode then runs `step()` per token.
4687    /// Pub for the network split (see `o1_begin`).
4688    pub fn o1_seal(&mut self) {
4689        if let Err(err) = self.o1_seal_checked() {
4690            self.o1_fail(err);
4691        }
4692    }
4693
4694    /// Run a bounded O(1) prefix (`ids`, positions 0..) through the batch
4695    /// graph instead of the host prefill: the same exact attention, with the
4696    /// O(1) layers' rotated queries captured on the device and their K/V rows
4697    /// read back from the mirror, so each layer's collector holds what the
4698    /// host walk would have given its seal. The recurrent and exact layers'
4699    /// state stays on the device for the sealed suffix batches.
4700    /// `Ok(None)`: declined before any device state moved — the caller runs
4701    /// the host prefill. `Ok(Some(h))`: the last prefix row's hidden.
4702    fn o1_prefix_on_graph(
4703        &mut self,
4704        ids: &[u32],
4705        chunk: usize,
4706    ) -> Result<Option<Vec<f32>>, String> {
4707        let o1_layers: Vec<usize> = (0..self.num_layers)
4708            .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
4709            .collect();
4710        let Some(&first) = o1_layers.first() else {
4711            return Ok(None);
4712        };
4713        let (nkv, hd) = {
4714            let c = &self.kv_cache.layers[first];
4715            (c.num_kv_heads, c.head_dim)
4716        };
4717        let nh = self.num_heads;
4718        if chunk == 0
4719            || o1_layers.iter().any(|&li| {
4720                let c = &self.kv_cache.layers[li];
4721                self.layer_num_heads(li) != nh
4722                    || (c.num_kv_heads, c.head_dim) != (nkv, hd)
4723                    || c.seq_len != 0
4724                    || c.o1_pending_boundary().is_none()
4725            })
4726            || !crate::gpu::o1_qcap_begin(&o1_layers)
4727        {
4728            return Ok(None);
4729        }
4730        let hs = self.hidden_size;
4731        let n = ids.len();
4732        let mut last = vec![0f32; hs];
4733        let mut pos = 0;
4734        while pos < n {
4735            let end = (pos + chunk).min(n);
4736            let bk = end - pos;
4737            let mut hiddens = vec![0f32; bk * hs];
4738            for (j, &id) in ids[pos..end].iter().enumerate() {
4739                hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
4740            }
4741            let positions: Vec<usize> = (pos..end).collect();
4742            match self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None) {
4743                crate::gpu::BatchGraphOutcome::Completed => {
4744                    last.copy_from_slice(&hiddens[(bk - 1) * hs..]);
4745                    pos = end;
4746                }
4747                crate::gpu::BatchGraphOutcome::Declined if pos == 0 => {
4748                    let _ = crate::gpu::o1_qcap_take();
4749                    return Ok(None);
4750                }
4751                _ => {
4752                    let _ = crate::gpu::o1_qcap_take();
4753                    return Err("O(1) prefix batch graph stopped after admission".into());
4754                }
4755            }
4756        }
4757        let q = crate::gpu::o1_qcap_take().ok_or("O(1) prefix: query readback failed")?;
4758        let pulls: Vec<(usize, usize, usize)> = o1_layers.iter().map(|&li| (li, 0, n)).collect();
4759        let kv = crate::gpu::graph_kv_read_rows(self.graph_kv_id, &pulls, nkv, hd)
4760            .ok_or("O(1) prefix: K/V readback failed")?;
4761        let (row, qrow) = (nkv * hd, nh * hd);
4762        for ((li, qs), (k, v)) in q.into_iter().zip(kv) {
4763            if k.len() != n * row || v.len() != n * row || qs.len() != n * qrow {
4764                return Err(format!(
4765                    "O(1) prefix: layer {li} read back {}/{}/{} values, expected {}/{}/{}",
4766                    k.len(),
4767                    v.len(),
4768                    qs.len(),
4769                    n * row,
4770                    n * row,
4771                    n * qrow
4772                ));
4773            }
4774            let cache = &mut self.kv_cache.layers[li];
4775            for p in 0..n {
4776                cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
4777                cache.o1_push_q(&qs[p * qrow..(p + 1) * qrow]);
4778            }
4779        }
4780        Ok(Some(last))
4781    }
4782
4783    /// Enable/disable the structured per-token telemetry trace (B4).
4784    pub fn set_trace(&mut self, on: bool) {
4785        self.trace = on;
4786    }
4787
4788    /// Replace all request-scoped sampler options and reset the random stream.
4789    /// This is required for deterministic `seed` semantics in pooled servers.
4790    pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4791        self.rng = match config.seed {
4792            Some(seed) => SplitMix64::new(seed),
4793            None => SplitMix64::from_entropy(),
4794        };
4795        self.sampler_config = config;
4796    }
4797
4798    /// Toggle the per-token confidence reduction (a full-vocab
4799    /// softmax each token). `bench --core` turns it off so the timed
4800    /// loop matches llama-bench's core contract; the result's
4801    /// `confidence` vec is empty while off.
4802    pub fn set_confidence(&mut self, on: bool) {
4803        self.confidence_on = on;
4804    }
4805
4806    /// Set the confidence-calibration temperature (B1). Values ≤0 are
4807    /// clamped to raw (1.0).
4808    pub fn set_calib_temp(&mut self, t: f32) {
4809        self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4810    }
4811
4812    /// The active calibration temperature (1.0 = raw probability).
4813    pub fn calib_temp(&self) -> f32 {
4814        self.calib_temp
4815    }
4816
4817    /// The primary (full-attention) RoPE inverse-frequency table selected
4818    /// from the CMF architecture.  This is read-only diagnostic state for
4819    /// parity checks; local sliding layers may use `inv_freq_local` instead.
4820    pub fn rope_inv_freq(&self) -> &[f32] {
4821        self.inv_freq.as_slice()
4822    }
4823
4824    /// Partial rotary (Qwen3.5): rotate only the first `rotary_dim` dims;
4825    /// the frequency table is rebuilt over the rotary dims.
4826    pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4827        self.rotary_dim = rotary_dim.min(self.head_dim);
4828        self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4829        // The packed resident graph owns its own inverse-frequency plane;
4830        // changing RoPE after it was built must not leave a stale device
4831        // model behind the exact host configuration.
4832        self.embryo_graph = None;
4833    }
4834
4835    fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4836        QwenAttnCfg {
4837            mrope: None,
4838            num_heads: self.num_heads,
4839            num_kv_heads: self.num_kv_heads,
4840            head_dim: self.head_dim,
4841            hidden_size: self.hidden_size,
4842            position,
4843            inv_freq: &self.inv_freq,
4844            rotary_dim: self.rotary_dim,
4845            scale: self.attn_scale,
4846            softcap: self.attn_softcap,
4847            window: None,
4848            v_norm: false,
4849            qk_norm_after_rope: self.qk_norm_after_rope,
4850            gate_sigmoid: self.proj_gate_sigmoid,
4851            q_norm: None,
4852            k_norm: None,
4853            output_gate: false,
4854            softplus_gate: None,
4855            rope_scale: self.rope_scale,
4856            bias: None,
4857            rms_eps: self.rms_eps,
4858            norm_style: self.norm_style,
4859            pool: self.pool.as_deref(),
4860            v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4861        }
4862    }
4863
4864    /// Generate text from a plain-text prompt. Streams tokens via `on_token`.
4865    pub fn generate(
4866        &mut self,
4867        prompt: &str,
4868        max_tokens: usize,
4869        task_mask: Option<&TaskMask>,
4870        on_token: Option<TokenCallback>,
4871    ) -> Result<GenerateResult, String> {
4872        let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4873        self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4874    }
4875
4876    /// Generate from a V4.1 multimodal prompt prepared by the vision module.
4877    /// Vision rows are encoded once and fed through the same bounded token walk as text.
4878    pub fn generate_from_vl(
4879        &mut self,
4880        input: &crate::dsv41_vision::PreparedVlInputs,
4881        max_tokens: usize,
4882        task_mask: Option<&TaskMask>,
4883        on_token: Option<TokenCallback>,
4884    ) -> Result<GenerateResult, String> {
4885        let Some(dsv41) = &self.dsv41 else {
4886            return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4887        };
4888        if input.token_ids.is_empty() {
4889            return Err("empty V4.1 multimodal prompt".into());
4890        }
4891        if input.token_types.len() != input.token_ids.len() {
4892            return Err(format!(
4893                "V4.1 token type count {} != token count {}",
4894                input.token_types.len(),
4895                input.token_ids.len()
4896            ));
4897        }
4898        let dim = dsv41.2.dim;
4899        let mut embeddings = vec![None; input.token_ids.len()];
4900        let mut participates = vec![true; input.token_ids.len()];
4901        if !input.images.is_empty() {
4902            let vision = self
4903                .dsv41_vision
4904                .as_ref()
4905                .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4906            for image in &input.images {
4907                let end = image.start.saturating_add(image.types.len());
4908                if end > input.token_ids.len() {
4909                    return Err(format!(
4910                        "V4.1 image span {}..{} exceeds prompt length {}",
4911                        image.start,
4912                        end,
4913                        input.token_ids.len()
4914                    ));
4915                }
4916                let mut span = vec![0.0f32; image.types.len() * dim];
4917                vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4918                for (offset, &kind) in image.types.iter().enumerate() {
4919                    let pos = image.start + offset;
4920                    if input.token_types[pos] != kind {
4921                        return Err(format!(
4922                            "V4.1 image type mismatch at position {pos}: {} != {kind}",
4923                            input.token_types[pos]
4924                        ));
4925                    }
4926                    embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4927                    participates[pos] = false;
4928                }
4929            }
4930        }
4931        for (pos, &kind) in input.token_types.iter().enumerate() {
4932            if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4933                return Err(format!("V4.1 text position {pos} has an image embedding"));
4934            }
4935            if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4936                return Err(format!("V4.1 image position {pos} has no image embedding"));
4937            }
4938        }
4939        self.dsv41_prefill = Some((embeddings, participates));
4940        let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4941        self.dsv41_prefill = None;
4942        result
4943    }
4944
4945    /// `None` when the mask forbids nothing (see `TaskMask::fully_open`).
4946    fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4947        m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4948    }
4949
4950    /// Generate from prepared token ids (e.g. a chat template).
4951    ///
4952    /// With an MTP head, greedy generation without a task mask takes the
4953    /// speculative path: the MTP module drafts the token after next and
4954    /// the main model verifies both in one fused two-position forward
4955    /// (weights streamed once). The output is EXACTLY the vanilla greedy
4956    /// sequence — a rejected draft is rolled back — MTP only buys speed.
4957    pub fn generate_from_ids(
4958        &mut self,
4959        input_ids: &[u32],
4960        max_tokens: usize,
4961        task_mask: Option<&TaskMask>,
4962        on_token: Option<TokenCallback>,
4963    ) -> Result<GenerateResult, String> {
4964        self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4965    }
4966
4967    /// Generate from complete prompt embeddings [token_count, hidden_size].
4968    /// Text rows can be obtained with `embed_id`; media rows replace only
4969    /// their expanded placeholder positions. Rows are already scaled and
4970    /// enter `PrefillIn::Hidden`, so a device graph must not re-embed them.
4971    /// Token-only KV reuse is disabled both into and out of this request.
4972    pub fn generate_from_embeds(
4973        &mut self,
4974        input_ids: &[u32],
4975        prompt_rows: &[f32],
4976        max_tokens: usize,
4977        task_mask: Option<&TaskMask>,
4978        on_token: Option<TokenCallback>,
4979    ) -> Result<GenerateResult, String> {
4980        if input_ids.is_empty()
4981            || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4982        {
4983            return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4984        }
4985        if prompt_rows.iter().any(|x| !x.is_finite()) {
4986            return Err("embedded prompt contains non-finite values".into());
4987        }
4988        if !self.can_prefill_batched()
4989            || self.dyn_router.is_some()
4990            || self.o1_active()
4991            || self.mtp.is_some()
4992            || self.gpu_plan.is_some()
4993        {
4994            return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4995        }
4996        self.generate_with_prompt_rows(
4997            input_ids,
4998            Some(prompt_rows),
4999            max_tokens,
5000            task_mask,
5001            on_token,
5002        )
5003    }
5004
5005    fn generate_with_prompt_rows(
5006        &mut self,
5007        input_ids: &[u32],
5008        prompt_rows: Option<&[f32]>,
5009        max_tokens: usize,
5010        task_mask: Option<&TaskMask>,
5011        mut on_token: Option<TokenCallback>,
5012    ) -> Result<GenerateResult, String> {
5013        #[cfg(target_os = "macos")]
5014        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
5015        if std::env::var("CMF_TRACE_H").is_ok() {
5016            eprintln!("input_ids: {input_ids:?}");
5017        }
5018        if input_ids.is_empty() {
5019            return Err("empty prompt: nothing to generate from".to_string());
5020        }
5021        // A prior graph failure is terminal for that sequence but must not
5022        // poison the next independent request.  Keep this flag separate from
5023        // the externally-owned cooperative cancel bit.
5024        self.graph_failed
5025            .store(false, std::sync::atomic::Ordering::Relaxed);
5026        // A mask that forbids nothing still costs every fused path and
5027        // whole-token graph, all of which are gated on `is_none()`. A
5028        // narrowed file whose one segment is always on carries exactly
5029        // such a mask — drop it here rather than pay 5x for a no-op.
5030        let task_mask = self.drop_open_mask(task_mask);
5031        // A block-diffusion model generates whole blocks by denoising
5032        // previews, unless the request asks for the causal decode.
5033        if self.clover.is_some() && !self.clover_causal_request() {
5034            if prompt_rows.is_some() || task_mask.is_some() {
5035                return Err("block-diffusion generation takes token prompts without task masks".into());
5036            }
5037            return self.clover_generate(input_ids, max_tokens, on_token);
5038        }
5039
5040        // Cross-turn KV reuse: a chat app resends the whole history
5041        // every turn; when the new ids strictly EXTEND what the cache
5042        // already holds, prefill only the tail — turn latency stays
5043        // proportional to the new text instead of the whole session.
5044        // Extension-only (no rollback), so it is exact for every layer
5045        // kind including recurrent state; MTP/o1/task-mask runs keep
5046        // the fresh-sequence path. CMF_KV_REUSE=0 disables.
5047        let mut reuse_from = {
5048            let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
5049            if on
5050                && prompt_rows.is_none()
5051                && task_mask.is_none()
5052                && self.mtp.is_none()
5053                && !(self.mimo_mtp.is_some() && self.speculative)
5054                && self.o1_cfg.is_none()
5055                && self.dsv41.is_none()
5056            {
5057                self.cached_prefix_len(input_ids)
5058            } else {
5059                0
5060            }
5061        };
5062        // The device may own rows the host tail prefill needs (wgpu decode
5063        // writes only its mirror): hand them to the host, or start fresh.
5064        if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
5065            reuse_from = 0;
5066        }
5067        // A diverging turn (`rollback_prefix_len`): drop every row past the
5068        // common prefix, so the tail prefill appends at `reuse_from`.
5069        if reuse_from > 0 && !self.bounded_native() && reuse_from < self.kv_history.len() {
5070            for l in &mut self.kv_cache.layers {
5071                let d = l.pos_len() - reuse_from;
5072                l.truncate_last(d);
5073            }
5074            if std::env::var("CMF_PREFILL_PROF").is_ok() {
5075                eprintln!(
5076                    "kv-reuse: rolled back {} answer position(s) to the common prefix",
5077                    self.kv_history.len() - reuse_from
5078                );
5079            }
5080            self.kv_history.truncate(reuse_from);
5081            self.graph_logits = None;
5082        }
5083        self.swa_trim_anchor = input_ids.len();
5084        self.last_prefill_tokens = input_ids.len() - reuse_from;
5085        let bounded_native = self.bounded_native();
5086        if reuse_from == 0 {
5087            // Fresh sequence — the cache holds absolute positions.
5088            // MTP never participates in cross-turn KV reuse, so a
5089            // high-vocabulary token from the previous request must not keep
5090            // this request on its expensive full draft head.
5091            self.reset_fresh_mtp_draft_state();
5092            self.clear_sequence_state();
5093        } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
5094            eprintln!(
5095                "kv-reuse: {} of {} prompt positions already cached",
5096                reuse_from,
5097                input_ids.len()
5098            );
5099        }
5100        crate::gpu::graph_race_begin_generation();
5101        // Optional bounded calibration prefix. Keep the requested value
5102        // even when it is longer than the prompt; the collecting layer will
5103        // defer at the effective boundary and remain exact for short input.
5104        let o1_prefill = if self.o1_active() && task_mask.is_none() {
5105            std::env::var("CMF_O1_PREFILL")
5106                .ok()
5107                .and_then(|v| v.parse::<usize>().ok())
5108                .filter(|&p| p > 0)
5109        } else {
5110            None
5111        };
5112        if task_mask.is_none() {
5113            self.o1_begin_with_prefix(o1_prefill);
5114        }
5115
5116        // Speculative decode is off under o1: a rejected draft can't be
5117        // rolled back out of the far accumulators / ring window (the
5118        // Nyström insertion is irreversible by design).
5119        // The wgpu token graph owns a device K/V mirror that speculative
5120        // rollback would desync — the two are mutually exclusive.
5121        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
5122        // Graph speculative decode (`CMF_GRAPH_SPEC=1`): the MTP head
5123        // drafts, ONE batched graph submit verifies the whole chain.
5124        //
5125        // It now PAYS on Qwen3.6-27B / RTX 5090 — 51.1 tok/s against a
5126        // plain 49.4 at k=3, medians of three, 89% of drafts accepted,
5127        // and the greedy continuation is byte-identical to the plain
5128        // path. That took the batch matvec sharing its nibble unpack
5129        // across the batch (`CMF_MV_BK=2`); before it, the same round
5130        // measured 43.6, an 11% LOSS, which is what the earlier note
5131        // here described.
5132        //
5133        // Still opt-in. One model's win is not a default: the verify
5134        // rides `gdn_spec_restore` and a batched frame whose numerics
5135        // are the batch kernels', and that has to be shown on more than
5136        // one architecture before every greedy decode takes it.
5137        // Greedy (with or without penalties) verifies by argmax equality.
5138        // Sampling (temperature > 0) can go through speculative SAMPLING —
5139        // draft from the MTP head's own post-chain distribution, accept
5140        // with min(1, p/q), correct from max(0, p − q); the emitted stream
5141        // is distributed exactly as the plain sampler's — but it is
5142        // OPT-IN (`CMF_GRAPH_SPEC_SAMPLE=1`): measured on Qwen3.8-27B /
5143        // RTX 5090 at the instruct row (0.7 / 0.80 / 20 / presence 1.5)
5144        // it decoded 19-22 tok/s against a plain 40 — nine post-chain
5145        // distributions a round plus a lower acceptance than greedy's,
5146        // against a verify that costs 2.7 single tokens. The greedy arms
5147        // pay +10%; the sampling arm needs a cheaper verify first.
5148        // Native Metal has a fast row-batch verify, but it is NOT admitted
5149        // by default: a real-model exactness check found an emitted greedy
5150        // divergence from the serial path.  Every native-Metal speculative
5151        // arm therefore requires the explicit `CMF_GRAPH_SPEC=1` experiment
5152        // gate until the batch kernels are numerically qualified.  If it is
5153        // explicitly enabled, its sampling arm is useful only for a config
5154        // the SPARSE chain serves (a top-k within `sparse_ok`): without it a
5155        // round builds nine 248k-float distributions on the host, which is
5156        // the 5090's measured loss and not a cost the round-token proxy
5157        // below can see. A top-k-less sampling config keeps the plain path
5158        // unless asked for by name.
5159        #[cfg(target_os = "macos")]
5160        let metal_graph = crate::gpu::q1_force()
5161            && crate::gpu::enabled_here()
5162            && std::env::var("CMF_GPU_BLOCK")
5163                .map(|v| v != "0")
5164                .unwrap_or(true);
5165        // `CMF_GPU_BLOCK=0` declines the ordinary native block route but
5166        // does not change the backend identity: a separately requested wgpu
5167        // graph can still reach `graph_spec_step`, whose native branch uses
5168        // VerifyGraph. Keep the quality policy tied to that identity, not to
5169        // the particular block-route admission above.
5170        #[cfg(target_os = "macos")]
5171        let native_metal_backend = crate::gpu::q1_force();
5172        #[cfg(not(target_os = "macos"))]
5173        let metal_graph = false;
5174        #[cfg(not(target_os = "macos"))]
5175        let native_metal_backend = false;
5176        let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
5177        // A round whose cost is the MEASURED one: greedy (argmax rows), or
5178        // sampling through the sparse chain. Anything else pays the dense
5179        // chain's host time, which no proxy can price.
5180        let spec_cheap_round =
5181            self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
5182        let spec_sampling_ok = self.sampler_config.temperature < 1e-6
5183            || match spec_sample_env.as_deref() {
5184                Some("1") => true,
5185                Some(_) => false,
5186                None => metal_graph && spec_cheap_round,
5187            };
5188        // ON by default for greedy on the wgpu graph: with the draft on
5189        // the graph and the verify bit-exact, it measured 58.7 tok/s
5190        // against a plain 48.1 on Qwen3.8-27B q4tp / RTX 5090 (k=4) and
5191        // 51.1 against 49.4 on Qwen3.6-27B, and a round that stops
5192        // paying turns itself off below (acceptance watchdog).
5193        // `CMF_GRAPH_SPEC=0` disables; `=1` is the explicit opt-in spelling.
5194        // …but only where the batched verify has its register-blocked
5195        // kernel: q4tp dense FFNs (graph kind 6). q4t and q8_2f verify
5196        // through tile GEMMs today and measured a LOSS (q8_2f 22 against
5197        // 29 tok/s), the 2-bit plane the same; those stay opt-in
5198        // (`CMF_GRAPH_SPEC=1`).
5199        // …at least in nine dense FFNs of ten: a healed file carries its
5200        // last two layers at q8_2f, and two tile-GEMM verifies among 64 do
5201        // not change the arithmetic (measured: the healed q4tp file
5202        // decodes at the plain file's rate and would otherwise sit out).
5203        let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
5204        for lw in &self.weights.layers {
5205            if let FfnKind::Dense(d) = &lw.ffn {
5206                dense_n += 1;
5207                if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
5208                    && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
5209                    && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
5210                {
5211                    dense_q4tp += 1;
5212                }
5213            }
5214        }
5215        // …and only where the MTP block itself runs on the graph. A MoE
5216        // draft block (Qwen3.6-35B-A3B, Apex-35B: 256 experts in the MTP
5217        // layer) fails `mtp_graph_ok`, so it drafts — and warms its cache
5218        // over the whole prompt — op by op on the host. Apex-35B q4tp on an
5219        // A40 + Xeon Gold 6342: greedy 51.8 against 65.6 tok/s plain (300
5220        // tokens, 57 % accepted), and 2.5 ms per prompt position (a
5221        // 4096-token ingest 171 against 295 tok/s); the published
5222        // Qwen3.6-35B-A3B 79.7 against 86.6. `CMF_GRAPH_SPEC=1` opts in.
5223        #[cfg(feature = "gpu")]
5224        let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| self.mtp_graph_ok(m));
5225        #[cfg(not(feature = "gpu"))]
5226        let mtp_on_graph = true;
5227        let spec_default_ok = (dense_n == 0 || dense_q4tp * 10 >= dense_n * 9) && mtp_on_graph;
5228        // Penalties break the draft head's agreement with the trunk (a
5229        // 1.1 repetition penalty measured 2 of 16 accepted): not by
5230        // default there either — off Metal that rule is untouched, and
5231        // suppressed ids keep counting as a penalty there, because no
5232        // measurement on a discrete card says otherwise.
5233        //
5234        // On native Metal the penalized arms DO pay: the draft applies
5235        // the same penalty and the verify scores the penalized rows
5236        // exactly (`greedy_pen`, the plain loop's arithmetic), so the
5237        // text is the plain path's and only the round's shape changes.
5238        // Measured on this M4 — see the report for the interleaved run.
5239        let penalized = !metal_graph
5240            && (self.sampler_config.repetition_penalty != 1.0
5241                || self.sampler_config.presence_penalty != 0.0
5242                || !self.sampler_config.suppress_tokens.is_empty());
5243        // …and not on wgpu-over-Metal: the batched verify graph there
5244        // returned 0 accepted drafts and garbage text on a GDN hybrid
5245        // (16.08, Qwen3.5-0.8B) while Vulkan is bit-exact; the Mac's
5246        // default backend is native Metal without a batch graph anyway.
5247        #[cfg(feature = "gpu")]
5248        let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
5249        #[cfg(not(feature = "gpu"))]
5250        let metal_wgpu = false;
5251        let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
5252        let spec_wanted = graph_spec_wanted_by_policy(
5253            spec_env.as_deref(),
5254            spec_default_ok,
5255            penalized,
5256            metal_wgpu,
5257            native_metal_backend,
5258        );
5259        if spec_wanted && metal_wgpu {
5260            tracing::warn!(
5261                "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
5262                 verified on this backend (garbage measured on Qwen3.5-0.8B)"
5263            );
5264        }
5265        // Native Metal: the b-row verify graph (`try_batch_graph_metal`)
5266        // stands where the wgpu batch graph stands on discrete cards
5267        // (`metal_graph`, above).
5268        let graph_spec = self.speculative
5269            && (graph_on || metal_graph)
5270            && self.mtp.is_some()
5271            && task_mask.is_none()
5272            && !self.o1_active()
5273            && spec_sampling_ok
5274            && spec_wanted;
5275        // The host fused pair is an optimization, not a verifier. On real
5276        // q4tp Qwen3.5 a near-tied row produced a different greedy argmax
5277        // from two serial target forwards. That makes a seemingly accepted
5278        // draft change text, which is worse than losing speculative speed.
5279        // Keep it explicitly experimental until a model/backend has passed
5280        // a long exact-ID gate; the native Metal batch verifier has its own
5281        // separate graph admission and is not controlled by this switch.
5282        //
5283        // GDN hybrids are also excluded by default: their recurrence is
5284        // sequential, so the pair lane cannot parallelize and measured 2x
5285        // slower on the 35B. `CMF_MTP_PAIR=1` is deliberately the only
5286        // opt-in for the host fused-pair experiment (not `CMF_MTP=1`).
5287        let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
5288        let pair_pays = host_pair_opt_in
5289            && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
5290        // Native Metal: say the route ONCE (RUST_LOG=info), including the
5291        // deliberate quality gate that keeps the unqualified batch verifier
5292        // off unless the user explicitly requested the experiment.
5293        #[cfg(target_os = "macos")]
5294        if metal_graph {
5295            static SAID: std::sync::Once = std::sync::Once::new();
5296            SAID.call_once(|| {
5297                let spec = if graph_spec {
5298                    let k = std::env::var("CMF_GRAPH_SPEC_K")
5299                        .ok()
5300                        .and_then(|v| v.parse::<usize>().ok())
5301                        .filter(|&v| (1..=8).contains(&v))
5302                        .unwrap_or(7);
5303                    let arm = if self.sampler_config.temperature < 1e-6 {
5304                        "greedy"
5305                    } else {
5306                        "sampling"
5307                    };
5308                    format!(
5309                        "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
5310                        Self::draft_vocab_rows(usize::MAX)
5311                    )
5312                } else if !self.speculative {
5313                    "spec off (CMF_MTP=0)".to_string()
5314                } else if self.mtp.is_none() {
5315                    "spec off (no MTP head)".to_string()
5316                } else if !spec_sampling_ok {
5317                    if spec_cheap_round {
5318                        "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
5319                    } else {
5320                        "spec off (sampling without a top-k: the dense chain \
5321                         costs more than it saves)"
5322                            .to_string()
5323                    }
5324                } else if !spec_wanted {
5325                    if spec_env.is_none() {
5326                        "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
5327                    } else {
5328                        "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
5329                    }
5330                } else if task_mask.is_some() {
5331                    "spec off (task mask)".to_string()
5332                } else if !graph_spec && !pair_pays {
5333                    "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
5334                        .to_string()
5335                } else {
5336                    "spec off (O(1) attention)".to_string()
5337                };
5338                let on = |var: &str| {
5339                    if std::env::var(var).as_deref() == Ok("0") {
5340                        "off"
5341                    } else {
5342                        "on"
5343                    }
5344                };
5345                tracing::info!(
5346                    "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
5347                     MTP graph {}, attend {}, probe {}",
5348                    if crate::gpu_metal::state4_on() {
5349                        "on"
5350                    } else {
5351                        "off"
5352                    },
5353                    if crate::gpu_metal::async_replay_on() {
5354                        "on"
5355                    } else {
5356                        "off"
5357                    },
5358                    on("CMF_METAL_PREFILL"),
5359                    on("CMF_MTP_GRAPH"),
5360                    std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
5361                    if crate::gpu::probe_enabled() {
5362                        "bypassed (q1 force)"
5363                    } else {
5364                        "off"
5365                    },
5366                );
5367            });
5368        }
5369        let spec_active = self.speculative
5370            && self.mtp.is_some()
5371            && task_mask.is_none()
5372            && !self.o1_active()
5373            && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
5374        // The MTP module is detached during generation so its mutable
5375        // state does not fight the borrow on `self`.
5376        let mut mtp = if spec_active { self.mtp.take() } else { None };
5377        if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
5378            eprintln!(
5379                "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
5380                mtp.is_some(),
5381                self.speculative,
5382                self.sampler_config.temperature < 1e-6,
5383            );
5384        }
5385        if let Some(m) = &mut mtp {
5386            m.kv.clear();
5387            // The MTP block's own device mirror starts over with its cache.
5388            crate::gpu::graph_kv_reset(self.mtp_kv_id());
5389            self.mtp_graph_mode = None;
5390        }
5391        // MiMo-V2's draft stack: greedy rounds (draft K with the chained
5392        // MTP layers, verify K+1 rows in one batched forward). Sampling
5393        // decodes plain; `CMF_MTP=0` / `CMF_MIMO_MTP=0` turn it off.
5394        let mimo_spec = self.speculative
5395            && self.mimo_mtp.is_some()
5396            && task_mask.is_none()
5397            && !self.o1_active()
5398            && self.dyn_router.is_none()
5399            && self.sampler_config.temperature < 1e-6
5400            && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
5401        if let Some(st) = self.mimo_mtp.as_mut() {
5402            st.reset();
5403            if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
5404                Self::mimo_mtp_hist_cap(st, input_ids.len());
5405            }
5406        }
5407        // Dynamic router detached during decode (same borrow trick as MTP).
5408        // Speculative decode and dynamic routing are mutually exclusive
5409        // for now — the fused-pair path doesn't carry per-token φ.
5410        let mut router = if mtp.is_none() {
5411            self.dyn_router.take()
5412        } else {
5413            None
5414        };
5415        let mut reuse_from = reuse_from;
5416        if let Some(r) = &mut router {
5417            r.reset(); // active=backbone, matching a fresh overlay
5418            self.dyn_phi_seen = 0; // fresh φ EMA per generation
5419            if self.dyn_active.is_some() {
5420                // A real switch back to the backbone invalidates the
5421                // cache the reuse key was computed against.
5422                let _ = self.set_active_skill(None);
5423                reuse_from = 0;
5424                self.last_prefill_tokens = input_ids.len();
5425            }
5426        }
5427
5428        let mut all_ids = input_ids.to_vec();
5429        // A block-diffusion model's response penalties count the response
5430        // only (`CloverPenaltyState`), never the prompt.
5431        let penalty_from = if self.clover.is_some() { input_ids.len() } else { 0 };
5432        let mut generated = 0usize;
5433        let mut finish_reason = "max_tokens".to_string();
5434        let mut drafted = 0usize;
5435        let mut accepted = 0usize;
5436        // DeepSeek-V4's draft quality is strongly content-dependent.  Two
5437        // consecutive paid rounds with no extra token put it on a bounded
5438        // cooldown; predictable text keeps batching, ordinary prose falls
5439        // back to the exact walk instead of paying a slow draft forever.
5440        // Local to one generation so one difficult request cannot poison the
5441        // next one, and deliberately automatic — this is not a user knob.
5442        let mut dsv4_spec_bad = 0usize;
5443        let mut dsv4_spec_retry_at = 0usize;
5444        let mut confidence: Vec<f32> = Vec::new();
5445        let trace_on = self.trace;
5446        let calib_temp = self.calib_temp;
5447        let mut traces: Vec<TokenTrace> = Vec::new();
5448
5449        // ── Prefill: forward each prompt token once, KEEP the last hidden.
5450        //    Dense prefill runs in fused pairs (weights streamed once per
5451        //    two positions — bit-identical to sequential, proven by the
5452        //    pair tests). With MTP: warm the draft head on
5453        //    (hidden_p, token_{p+1}) pairs.
5454        let mut hidden = vec![0.0f32; self.hidden_size];
5455        let mut pos = reuse_from;
5456        // lm_head-in-graph is only sound when the very next logits
5457        // consumer is this loop's own (MTP and skill routing interleave
5458        // other forwards / can swap lm_head between forward and sample).
5459        // CMF_GPU_LMHEAD=0 keeps lm_head off the graph: the token reads back
5460        // the 8 KB hidden instead of ~1 MB of logits, and the head runs on
5461        // the host. A probe for how much of the graph's fixed per-token cost
5462        // is the logits readback (the layer sweep puts that fixed part at
5463        // 3.88 ms of an 18.5 ms frame).
5464        let fuse_lm = mtp.is_none()
5465            && router.is_none()
5466            && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5467        self.graph_logits = None;
5468        self.graph_want_logits = false;
5469        let _tpf = std::time::Instant::now();
5470        let batch_k = self.generation_batch_k();
5471        if let Some(rows) = prompt_rows {
5472            let hs = self.hidden_size;
5473            // The batched graph's row count when the prompt rides it.
5474            let chunk = if batch_k > 0 { batch_k } else { self.prefill_chunk().max(1) };
5475            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5476                let end = (pos + chunk).min(input_ids.len());
5477                let hb = match self.prefill_input_rows(
5478                    PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5479                    pos,
5480                    task_mask,
5481                ) {
5482                    Ok(hb) => hb,
5483                    Err(err) => {
5484                        self.prompt_mrope = None;
5485                        self.finish_generation(&mut mtp, &mut router, true);
5486                        return Err(err);
5487                    }
5488                };
5489                if mimo_spec {
5490                    self.mimo_note_rows(&hb, pos);
5491                }
5492                hidden.copy_from_slice(&hb[hb.len() - hs..]);
5493                pos = end;
5494            }
5495        }
5496        // The prompt's MRoPE rows are in the cache; decode positions are
5497        // plain (`sensenova::mrope_positions` shifted them so).
5498        self.prompt_mrope = None;
5499        // DeepSeek-V4 owns a separate hyper-connection stack. Route it
5500        // before the generic prefill choices: those correctly reject an
5501        // empty `weights.layers`, but their final per-position fallback used
5502        // to consume the whole prompt before `dsv4::forward_chunk` could see
5503        // it. The batch implementation therefore existed without a live
5504        // production entry point.
5505        //
5506        // Bounded chunks preserve cancellation responsiveness. Only the
5507        // prompt's final chunk asks for logits; every earlier head projection
5508        // would produce 129 280 values that no caller reads.
5509        while self.qwen4_exp.is_some()
5510            && mtp.is_none()
5511            && pos < input_ids.len()
5512            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5513        {
5514            // The device path takes several prompt tokens per layer frame;
5515            // the host path runs them one by one inside the same call.
5516            let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5517            let want_logits = end == input_ids.len();
5518            let mut lg = Vec::new();
5519            if let Some(b) = &mut self.qwen4_exp {
5520                crate::qwen4_exp::forward_tokens(
5521                    &b.0,
5522                    &b.1,
5523                    &b.2,
5524                    &mut b.3,
5525                    &input_ids[pos..end],
5526                    pos,
5527                    &self.inv_freq,
5528                    self.pool.as_deref(),
5529                    &mut lg,
5530                    want_logits,
5531                );
5532            }
5533            if want_logits {
5534                self.graph_logits = Some(lg);
5535            }
5536            pos = end;
5537            hidden.fill(0.0);
5538        }
5539        while self.dsv4.is_some()
5540            && mtp.is_none()
5541            && pos < input_ids.len()
5542            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5543        {
5544            let end = (pos + prefill_chunk()).min(input_ids.len());
5545            let ids: Vec<u32> = input_ids[pos..end].to_vec();
5546            let mut lg = Vec::new();
5547            if let Some(b) = &mut self.dsv4 {
5548                let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5549                crate::dsv4::forward_chunk(
5550                    g,
5551                    layers,
5552                    &cfg,
5553                    st,
5554                    &ids,
5555                    pos,
5556                    &self.inv_freq,
5557                    self.pool.as_deref(),
5558                    &mut lg,
5559                    end == input_ids.len(),
5560                );
5561            }
5562            if end == input_ids.len() {
5563                self.graph_logits = Some(lg);
5564            }
5565            pos = end;
5566            hidden = vec![0.0; self.hidden_size];
5567        }
5568        let dsv41_prefill = self.dsv41_prefill.take();
5569        while self.dsv41.is_some()
5570            && mtp.is_none()
5571            && pos < input_ids.len()
5572            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5573        {
5574            let end = (pos + prefill_chunk()).min(input_ids.len());
5575            let ids: Vec<u32> = input_ids[pos..end].to_vec();
5576            let mut lg = Vec::new();
5577            if let Some(b) = &mut self.dsv41 {
5578                let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5579                if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5580                    crate::dsv41::forward_chunk_masked_with_embeddings(
5581                        g,
5582                        layers,
5583                        cfg,
5584                        st,
5585                        &ids,
5586                        pos,
5587                        &embeddings[pos..end],
5588                        &participates[pos..end],
5589                        self.pool.as_deref(),
5590                        &mut lg,
5591                    );
5592                } else {
5593                    crate::dsv41::forward_chunk(
5594                        g,
5595                        layers,
5596                        cfg,
5597                        st,
5598                        &ids,
5599                        pos,
5600                        self.pool.as_deref(),
5601                        &mut lg,
5602                    );
5603                }
5604            }
5605            if end == input_ids.len() {
5606                self.graph_logits = Some(lg);
5607            }
5608            pos = end;
5609            hidden = vec![0.0; self.hidden_size];
5610        }
5611        // With dynamic routing, prefill sequentially so the φ hook fires
5612        // over the PROMPT — the router enters decode with a warm φ (the
5613        // fused-pair path skips the per-layer φ capture). o1 layers
5614        // collect their query trace in both the single and pair paths.
5615        let dyn_prefill = router.is_some();
5616        // Optional bounded calibration prefix for generation.  The normal
5617        // O(1) path seals after the full prompt; this explicit knob instead
5618        // runs only the requested prefix through exact attention, seals the
5619        // Nyström state, and streams the rest of the prompt through the same
5620        // O(1) step used by decode.  It keeps the O(1) layers' Q trace and
5621        // temporary full KV bounded by the prefix while leaving the default
5622        // full-prompt quality profile untouched.
5623        let o1_prefill_limit = o1_prefill
5624            .and_then(|requested| self.o1_effective_boundary(requested))
5625            .map(|boundary| boundary.min(input_ids.len()));
5626        let mut o1_sealed = false;
5627        if let Some(limit) = o1_prefill_limit {
5628            // On the device route the prefix takes the batch graph and its
5629            // sealed suffix the O(1) batches after it (`o1_prefix_on_graph`):
5630            // the host walk read it at 16 tok/s on Qwen3.8-27B / A40, 33 s
5631            // of a 72 s TTFT at 4k. Only when a suffix follows — a prompt
5632            // inside the boundary stays exact on the host as before.
5633            // `CMF_O1_PREFIX_GPU=0` keeps the host prefill.
5634            if pos == 0
5635                && limit < input_ids.len()
5636                && batch_k > 0
5637                && task_mask.is_none()
5638                && mtp.is_none()
5639                && !dyn_prefill
5640                && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5641                && std::env::var("CMF_O1_PREFIX_GPU").as_deref() != Ok("0")
5642                && (0..self.num_layers).all(|li| self.phys_layer(li) == li)
5643            {
5644                let t_px = std::time::Instant::now();
5645                match self.o1_prefix_on_graph(&input_ids[..limit], batch_k) {
5646                    Ok(Some(last)) => {
5647                        hidden.copy_from_slice(&last);
5648                        pos = limit;
5649                        if std::env::var("CMF_PREFILL_PROF").is_ok() {
5650                            eprintln!(
5651                                "o1 prefix on the batch graph: {limit} rows in {:.1} ms",
5652                                t_px.elapsed().as_secs_f64() * 1e3
5653                            );
5654                        }
5655                    }
5656                    Ok(None) => {}
5657                    Err(err) => {
5658                        self.finish_generation(&mut mtp, &mut router, true);
5659                        return Err(err);
5660                    }
5661                }
5662            }
5663            // Reuse the exact batched prefix machinery when available; it
5664            // records the same per-position Q trace as the full prefill.
5665            if pos < limit && self.can_prefill_batched() && limit > 2 {
5666                let chunk = self.prefill_chunk();
5667                let hs = self.hidden_size;
5668                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5669                    let end = (pos + chunk).min(limit);
5670                    let hb = self.prefill_batch(&input_ids[pos..end], pos);
5671                    hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5672                    pos = end;
5673                }
5674            } else {
5675                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5676                    hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5677                    pos += 1;
5678                }
5679            }
5680            if pos >= limit {
5681                o1_sealed = match self.o1_seal_checked() {
5682                    Ok(sealed) => sealed,
5683                    Err(err) => {
5684                        self.finish_generation(&mut mtp, &mut router, true);
5685                        return Err(err);
5686                    }
5687                };
5688                tracing::info!(
5689                    "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5690                    o1_prefill.unwrap_or(0),
5691                    self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5692                        .unwrap_or(limit),
5693                    limit,
5694                    input_ids.len()
5695                );
5696            }
5697        }
5698        // q1 hybrids on Metal: the per-position GPU token graph beats
5699        // the CPU chunk-GEMM (whose wall is the sequential scalar GDN
5700        // recurrence), so prefill goes position-by-position through the
5701        // same graph as decode. Pure-attention models keep the batched
5702        // path — there the chunk-GEMM amortization wins.
5703        let graph_prefill = self.graph_prefill_preferred();
5704        // Native Metal, q4tp GDN hybrids: the prompt through the b-row
5705        // rows graph — projections as GEMMs over up to 512 positions, the
5706        // GDN recurrence in registers on the device, K/V rows appended by
5707        // the chunk — instead of one token-graph submit per position (the
5708        // 27B: 8 tok/s → GEMM-bound). The MTP warm-up rows come out of one
5709        // batched run of the block per chunk. Any refusal leaves the rest
5710        // of the prompt to the sequential paths below.
5711        #[cfg(target_os = "macos")]
5712        if task_mask.is_none()
5713            && !dyn_prefill
5714            && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5715            && crate::gpu::enabled_here()
5716            && self.gdn_cfg.is_some()
5717            && !self.per_token_stack()
5718            && input_ids.len() > 8
5719            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5720            && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5721        {
5722            let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5723                .ok()
5724                .and_then(|v| v.parse().ok())
5725                .filter(|&v| (16..=512).contains(&v))
5726                .unwrap_or(256);
5727            let hs = self.hidden_size;
5728            let _tp = std::time::Instant::now();
5729            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5730                let end = (pos + chunk).min(input_ids.len());
5731                let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5732                    MetalPrefillOutcome::Completed(hb) => hb,
5733                    MetalPrefillOutcome::Declined => break,
5734                    MetalPrefillOutcome::Failed => {
5735                        self.finish_generation(&mut mtp, &mut router, true);
5736                        return Err("ordinary Metal prefill failed after admission".into());
5737                    }
5738                };
5739                if let Some(m) = &mut mtp {
5740                    let n_pairs = if end < input_ids.len() {
5741                        end - pos
5742                    } else {
5743                        end - pos - 1
5744                    };
5745                    if n_pairs > 0 {
5746                        let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5747                            .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5748                            .collect();
5749                        if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5750                            for (j, (h, t)) in pairs.iter().enumerate() {
5751                                let h = h.to_vec();
5752                                let _ = self.mtp_step(m, &h, *t, pos + j);
5753                            }
5754                        }
5755                    }
5756                }
5757                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5758                pos = end;
5759            }
5760            if std::env::var("CMF_PREFILL_PROF").is_ok() {
5761                eprintln!(
5762                    "metal-prefill: {} of {} tokens in {:.1} ms",
5763                    pos,
5764                    input_ids.len(),
5765                    _tp.elapsed().as_secs_f64() * 1e3
5766                );
5767            }
5768        }
5769        // Gemma-4 on a discrete card: the prompt through the batched wgpu
5770        // graph (`g4_batch_ingest`); its K/V land in the device mirrors
5771        // decode reads. A refusal leaves the rest of the prompt to the
5772        // chunked host prefill below.
5773        #[cfg(not(target_os = "macos"))]
5774        if task_mask.is_none()
5775            && !dyn_prefill
5776            && mtp.is_none()
5777            && o1_prefill.is_none()
5778            && batch_k > 0
5779            && pos + 1 < input_ids.len()
5780            && self.g4_batch_prefill()
5781        {
5782            let t_g4 = std::time::Instant::now();
5783            match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5784                Ok((n, last)) => {
5785                    if n > 0 {
5786                        hidden.copy_from_slice(&last);
5787                    }
5788                    if std::env::var("CMF_PREFILL_PROF").is_ok() {
5789                        eprintln!(
5790                            "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5791                            input_ids.len() - pos,
5792                            t_g4.elapsed().as_secs_f64() * 1e3
5793                        );
5794                    }
5795                    pos += n;
5796                }
5797                Err(e) => {
5798                    self.finish_generation(&mut mtp, &mut router, true);
5799                    return Err(e);
5800                }
5801            }
5802        }
5803        self.mimo_moe_prepare();
5804        // A MoE stack larger than the card (MiMo-V2 q4tp on 96 GB): the
5805        // batched wgpu graph runs the device prefix of every chunk — its
5806        // experts resident — and the host's batched layer walk finishes
5807        // the chunk. Any refusal leaves the rest of the prompt to the
5808        // chunked prefill below.
5809        #[cfg(not(target_os = "macos"))]
5810        if task_mask.is_none()
5811            && !dyn_prefill
5812            && !graph_prefill
5813            && mtp.is_none()
5814            && o1_prefill.is_none()
5815            && !self.o1_active()
5816            && input_ids.len() > 2
5817            && self.batch_prefix_prefill()
5818        {
5819            let chunk = self.prefill_chunk().max(1);
5820            let hs = self.hidden_size;
5821            let t_bp = std::time::Instant::now();
5822            let pos0 = pos;
5823            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5824                let end = (pos + chunk).min(input_ids.len());
5825                let bk = end - pos;
5826                let mut hiddens = vec![0f32; bk * hs];
5827                for (j, &id) in input_ids[pos..end].iter().enumerate() {
5828                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5829                }
5830                let positions: Vec<usize> = (pos..end).collect();
5831                let mut run = 0usize;
5832                let outcome = self.try_batch_graph_wgpu_prefix(
5833                    &mut hiddens,
5834                    &positions,
5835                    bk,
5836                    None,
5837                    Some(&mut run),
5838                );
5839                match outcome {
5840                    crate::gpu::BatchGraphOutcome::Completed => {
5841                        let hb = if run < self.num_layers {
5842                            self.prefill_batch_span(
5843                                PrefillIn::Hidden(&hiddens),
5844                                pos,
5845                                None,
5846                                run,
5847                                self.num_layers,
5848                            )
5849                        } else {
5850                            hiddens
5851                        };
5852                        if mimo_spec {
5853                            self.mimo_note_rows(&hb, pos);
5854                        }
5855                        hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5856                        pos = end;
5857                    }
5858                    crate::gpu::BatchGraphOutcome::Failed => {
5859                        self.finish_generation(&mut mtp, &mut router, true);
5860                        return Err("batched prefix prefill failed after admission".into());
5861                    }
5862                    crate::gpu::BatchGraphOutcome::Declined => {
5863                        // Earlier chunks left their prefix rows on the
5864                        // device only: the host walk below needs them.
5865                        #[cfg(feature = "gpu")]
5866                        if pos > pos0 {
5867                            self.pull_lagging_host_kv(0, self.num_layers, pos);
5868                        }
5869                        break;
5870                    }
5871                }
5872            }
5873            if std::env::var("CMF_PREFILL_PROF").is_ok() {
5874                eprintln!(
5875                    "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5876                    pos - pos0,
5877                    input_ids.len(),
5878                    t_bp.elapsed().as_secs_f64() * 1e3
5879                );
5880            }
5881        }
5882        if task_mask.is_none()
5883            && !dyn_prefill
5884            && !graph_prefill
5885            && self.can_prefill_batched()
5886            && !self.per_token_stack()
5887            && o1_prefill.is_none()
5888            && input_ids.len() > 2
5889        {
5890            // Production prefill = the same chunked prefill-GEMM that
5891            // bench/PPL measure (roadmap §3 P0: generation used to warm
5892            // the prompt with the slower pair path — the published
5893            // prefill number didn't match real TTFT). MTP warm-up reads
5894            // each position's hidden straight from the chunk result.
5895            let chunk = self.prefill_chunk();
5896            let hs = self.hidden_size;
5897            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5898                let end = (pos + chunk).min(input_ids.len());
5899                let hb = self.prefill_batch(&input_ids[pos..end], pos);
5900                if mimo_spec {
5901                    self.mimo_note_rows(&hb, pos);
5902                }
5903                if let Some(m) = &mut mtp {
5904                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5905                        .ok()
5906                        .and_then(|v| v.parse().ok())
5907                        .unwrap_or(0);
5908                    for p in pos..end {
5909                        if p + 1 < input_ids.len() {
5910                            if probe >= 1 && p + 2 < input_ids.len() {
5911                                // Teacher-forced chain acceptance (see the
5912                                // tail loop's twin): the warm-up row stays,
5913                                // the chain's rows roll back.
5914                                let (d1, mut hx) = self.mtp_step_h(
5915                                    m,
5916                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
5917                                    input_ids[p + 1],
5918                                    p,
5919                                );
5920                                let mut ok = d1 == input_ids[p + 2];
5921                                Self::chain_probe_note(0, ok);
5922                                let mut d_prev = d1;
5923                                let mut extra = 0usize;
5924                                for j in 1..probe {
5925                                    if p + 2 + j >= input_ids.len() {
5926                                        break;
5927                                    }
5928                                    let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5929                                    extra += 1;
5930                                    ok = ok && dj == input_ids[p + 2 + j];
5931                                    Self::chain_probe_note(j, ok);
5932                                    d_prev = dj;
5933                                    hx = hj;
5934                                }
5935                                m.kv.truncate_last(extra);
5936                            } else {
5937                                let _ = self.mtp_step(
5938                                    m,
5939                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
5940                                    input_ids[p + 1],
5941                                    p,
5942                                );
5943                            }
5944                        }
5945                    }
5946                }
5947                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5948                pos = end;
5949            }
5950        }
5951        let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5952        if task_mask.is_none()
5953            && !dyn_prefill
5954            && !graph_prefill
5955            && !pair_off
5956            && self.pair_supported()
5957            && o1_prefill.is_none()
5958        {
5959            while pos + 1 < input_ids.len()
5960                && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5961            {
5962                let e1 = self.embed_single(input_ids[pos]);
5963                let e2 = self.embed_single(input_ids[pos + 1]);
5964                let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5965                if mimo_spec {
5966                    self.mimo_note_rows(&h1, pos);
5967                    self.mimo_note_rows(&h2, pos + 1);
5968                }
5969                // Both prefill tokens are real → commit lane-2 states.
5970                self.commit_linear_scratch();
5971                if let Some(m) = &mut mtp {
5972                    let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5973                    if pos + 2 < input_ids.len() {
5974                        let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5975                            .ok()
5976                            .and_then(|v| v.parse().ok())
5977                            .unwrap_or(0);
5978                        if probe >= 1 && pos + 3 < input_ids.len() {
5979                            // Same teacher-forced chain table as the tail
5980                            // loop below, fed from the pair path that owns
5981                            // most prefill positions.
5982                            let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5983                            let mut ok = d1 == input_ids[pos + 3];
5984                            Self::chain_probe_note(0, ok);
5985                            let mut d_prev = d1;
5986                            let mut extra = 0usize;
5987                            for j in 1..probe {
5988                                if pos + 3 + j >= input_ids.len() {
5989                                    break;
5990                                }
5991                                let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5992                                extra += 1;
5993                                ok = ok && dj == input_ids[pos + 3 + j];
5994                                Self::chain_probe_note(j, ok);
5995                                d_prev = dj;
5996                                hx = hj;
5997                            }
5998                            m.kv.truncate_last(extra);
5999                        } else {
6000                            let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
6001                        }
6002                    }
6003                }
6004                hidden = h2;
6005                pos += 2;
6006            }
6007        }
6008        // Batched GPU prefill for the wgpu decode graph (GDN hybrids): K prompt
6009        // positions per submit — projections/FFN as GEMMs (weight once per K),
6010        // attention/GDN looped inside — instead of one whole-graph submit per
6011        // position. Falls through to the per-position graph on any refusal.
6012        // Batched prefill is opt-in (CMF_BATCH_K>0). Default 0 = per-position
6013        // graph prefill. (Steady-state decode is provably identical either way —
6014        // token-graph submit and lm_head both unchanged — so this only trades
6015        // prefill wall.)
6016        // A bounded O(1) prefix is the one post-seal prompt interval: only
6017        // admit its batch when the device O(1) route is explicitly enabled and
6018        // every sealed layer exposes a portable view. The same batch size and
6019        // refusal behavior remain the ordinary controls/comparator.
6020        let o1_batch_ready = o1_sealed
6021            && o1_prefill.is_some()
6022            && mtp.is_none()
6023            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6024            && (0..self.num_layers).all(|li| {
6025                let cache = &self.kv_cache.layers[self.phys_layer(li)];
6026                cache.o1.is_none() || cache.o1_views().is_some()
6027            });
6028        // The ordinary graph-prefill route can share each completed trunk
6029        // chunk with an attached MTP head.  Keep chain probing on its
6030        // established per-position path: the probe deliberately needs every
6031        // teacher-forced draft row and its rollback table.
6032        let mtp_batch_prefill = mtp.is_some()
6033            && graph_prefill
6034            && task_mask.is_none()
6035            && !dyn_prefill
6036            && !self.o1_active()
6037            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
6038        if batch_k > 0
6039            && (graph_prefill || o1_batch_ready)
6040            && task_mask.is_none()
6041            && (!self.o1_active() || o1_batch_ready)
6042            && (mtp.is_none() || mtp_batch_prefill)
6043            && !dyn_prefill
6044            && pos + 1 < input_ids.len()
6045        {
6046            let hs = self.hidden_size;
6047            let chunk = batch_k;
6048            while pos < input_ids.len() {
6049                let end = (pos + chunk).min(input_ids.len());
6050                let bk = end - pos;
6051                let mut hiddens = vec![0f32; bk * hs];
6052                for (j, &id) in input_ids[pos..end].iter().enumerate() {
6053                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
6054                }
6055                let positions: Vec<usize> = (pos..end).collect();
6056                let t_chunk = std::time::Instant::now();
6057                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6058                let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
6059                if std::env::var("CMF_GRAPH_PROF").is_ok() {
6060                    let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
6061                    eprintln!(
6062                        "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6063                        if o1_batch_ready {
6064                            "o1"
6065                        } else if mtp_batch_prefill {
6066                            "ordinary_mtp"
6067                        } else {
6068                            "ordinary"
6069                        },
6070                        bk as f64 / (ms / 1000.0)
6071                    );
6072                }
6073                {
6074                    use std::sync::atomic::{AtomicBool, Ordering};
6075                    static SAID: AtomicBool = AtomicBool::new(false);
6076                    if !SAID.swap(true, Ordering::Relaxed) {
6077                        if ok_b {
6078                            tracing::info!(
6079                                "batched prefill: ACTIVE mode={} (k={bk})",
6080                                if o1_batch_ready {
6081                                    "o1"
6082                                } else if mtp_batch_prefill {
6083                                    "ordinary_mtp"
6084                                } else {
6085                                    "ordinary"
6086                                }
6087                            );
6088                        } else {
6089                            tracing::warn!("batched prefill {:?} — per-position graph", outcome);
6090                        }
6091                    }
6092                }
6093                if ok_b {
6094                    if mimo_spec {
6095                        self.mimo_note_rows(&hiddens, pos);
6096                    }
6097                    if mtp_batch_prefill {
6098                        let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
6099                        if n_pairs > 0 {
6100                            // `hiddens` is owned by this chunk, so materialize
6101                            // row slices before borrowing the detached MTP
6102                            // module.  The last prompt row has no successor;
6103                            // the helper above is the single source of that
6104                            // boundary rule.
6105                            let rows: Vec<Vec<f32>> = (0..n_pairs)
6106                                .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
6107                                .collect();
6108                            let pairs: Vec<(&[f32], u32)> = rows
6109                                .iter()
6110                                .enumerate()
6111                                .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
6112                                .collect();
6113                            if std::env::var("CMF_GRAPH_PROF").is_ok() {
6114                                eprintln!(
6115                                    "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
6116                                    pos,
6117                                    n_pairs,
6118                                    pos + n_pairs - 1,
6119                                );
6120                            }
6121                            let warm_error = if let Some(m) = mtp.as_mut() {
6122                                self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
6123                            } else {
6124                                None
6125                            };
6126                            if let Some(err) = warm_error {
6127                                // The trunk batch was already admitted.  A
6128                                // failed MTP warm-up therefore clears both
6129                                // mirrors and exits; continuing would pair a
6130                                // current trunk state with a stale MTP cache.
6131                                self.finish_generation(&mut mtp, &mut router, true);
6132                                return Err(err.to_string());
6133                            }
6134                        }
6135                    }
6136                    hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
6137                    pos = end;
6138                } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
6139                    // A failed batch may have advanced a device recurrent
6140                    // state (ordinary GDN or sealed O(1)). A CPU fallback
6141                    // would then observe stale accumulators, so clear the
6142                    // request state and make the failure explicit.
6143                    self.finish_generation(&mut mtp, &mut router, true);
6144                    return Err(if o1_batch_ready {
6145                        "sealed O(1) batch graph failed after admission".to_string()
6146                    } else {
6147                        "ordinary recurrent batch graph failed after admission".to_string()
6148                    });
6149                } else {
6150                    break; // unsupported → per-position graph handles the rest
6151                }
6152            }
6153        }
6154        // Resident Embryo graph: the prompt in chunks of one submit each
6155        // instead of one whole-graph submit per position; the last chunk
6156        // carries the logits exactly as the per-position walk would.
6157        if graph_prefill
6158            && task_mask.is_none()
6159            && mtp.is_none()
6160            && !dyn_prefill
6161            && pos == 0
6162            && input_ids.len() > 1
6163            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
6164        {
6165            if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
6166                self.graph_logits = Some(lg);
6167                hidden = vec![0.0; self.hidden_size];
6168                pos = input_ids.len();
6169            }
6170        }
6171        while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
6172            self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
6173            hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
6174            if mimo_spec {
6175                self.mimo_note_rows(&hidden, pos);
6176            }
6177            if let Some(m) = &mut mtp {
6178                if pos + 1 < input_ids.len() {
6179                    // `CMF_MTP_CHAIN_PROBE=k`: teacher-forced acceptance of a
6180                    // CHAINED draft — iterate the head on its own hidden k
6181                    // deep and score every depth against the prompt's real
6182                    // continuation. The economics of a k-token speculative
6183                    // round stand or fall on this table.
6184                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
6185                        .ok()
6186                        .and_then(|v| v.parse().ok())
6187                        .unwrap_or(0);
6188                    if probe >= 1 && pos + 2 < input_ids.len() {
6189                        let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
6190                        let mut ok = d1 == input_ids[pos + 2];
6191                        Self::chain_probe_note(0, ok);
6192                        let mut d_prev = d1;
6193                        let mut extra = 0usize;
6194                        for j in 1..probe {
6195                            if pos + 2 + j >= input_ids.len() {
6196                                break;
6197                            }
6198                            let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
6199                            extra += 1;
6200                            ok = ok && dj == input_ids[pos + 2 + j];
6201                            Self::chain_probe_note(j, ok);
6202                            d_prev = dj;
6203                            hx = hj;
6204                        }
6205                        // The chain's rows are speculation, not the prompt —
6206                        // keep only the warmup row the plain path would add.
6207                        m.kv.truncate_last(extra);
6208                    } else {
6209                        let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
6210                    }
6211                }
6212            }
6213            pos += 1;
6214        }
6215        if std::env::var("CMF_PREFILL_PROF").is_ok() {
6216            eprintln!(
6217                "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
6218                input_ids.len(),
6219                _tpf.elapsed().as_secs_f64() * 1000.0
6220            );
6221        }
6222        if self
6223            .graph_failed
6224            .swap(false, std::sync::atomic::Ordering::Relaxed)
6225        {
6226            // MTP is detached for speculative generation.  Restore the
6227            // module before returning the terminal graph error; otherwise a
6228            // failed request would silently remove the head from a pooled
6229            // pipeline and the next request would lose its configured route.
6230            self.finish_generation(&mut mtp, &mut router, true);
6231            return Err("GPU token graph failed during prefill".to_string());
6232        }
6233        // Cancelled mid-prefill: the cache holds a partial prompt —
6234        // drop the reuse history and return an empty generation.
6235        if self
6236            .cancel
6237            .swap(false, std::sync::atomic::Ordering::Relaxed)
6238        {
6239            // A cancelled prefill can already have advanced the device
6240            // mirror. Drop the whole partial sequence so a pooled pipeline
6241            // cannot carry that state into its next request.
6242            self.finish_generation(&mut mtp, &mut router, true);
6243            return Ok(GenerateResult {
6244                text: String::new(),
6245                token_ids: Vec::new(),
6246                prompt_tokens: input_ids.len(),
6247                tokens_generated: 0,
6248                finish_reason: "cancelled".to_string(),
6249                mtp_drafted: 0,
6250                mtp_accepted: 0,
6251                token_confidence: Vec::new(),
6252                traces: Vec::new(),
6253            });
6254        }
6255
6256        // Prompt absorbed → freeze the o1 layers' skeletons; from here
6257        // every decode step on those layers is O(W + m·dv + m²).
6258        if !o1_sealed {
6259            match self.o1_seal_checked() {
6260                Ok(_) => {}
6261                Err(err) => {
6262                    self.finish_generation(&mut mtp, &mut router, true);
6263                    return Err(err);
6264                }
6265            }
6266        }
6267
6268        // Commit one token: push, check EOS, stream. Returns false = stop.
6269        macro_rules! commit {
6270            ($id:expr) => {{
6271                all_ids.push($id);
6272                generated += 1;
6273                self.note_draft_id($id);
6274                if self.tokenizer.is_eos($id) && !self.ignore_eos {
6275                    finish_reason = "stop".to_string();
6276                    false
6277                } else {
6278                    let token_text = self.tokenizer.decode_token($id);
6279                    let mut go = true;
6280                    if let Some(ref mut cb) = on_token {
6281                        if !cb(&token_text) {
6282                            finish_reason = "cancelled".to_string();
6283                            go = false;
6284                        }
6285                    }
6286                    go
6287                }
6288            }};
6289        }
6290
6291        // Speculation is decided by MEASUREMENT, not by an acceptance
6292        // model. A k=4 round costs ~3.8 plain tokens on the 5090 (draft
6293        // 6.6 + verify 66.6 + commit 4.8 ms against a 20.6 ms token), so it
6294        // pays only when the head lands ~2.8 of 4 — predictable text (code,
6295        // structured output) does, free prose often does not, and the
6296        // ratio at which the two cross depends on the card and the context
6297        // depth. So: four speculative rounds timed, then eight plain
6298        // tokens timed, and the faster arm runs until a re-check 256
6299        // tokens later (context growth moves the balance). The trial
6300        // costs at most a few tokens of the slower arm per 256.
6301        let mut spec_trial = SpecTrial::Spec {
6302            t0: std::time::Instant::now(),
6303            gen0: generated,
6304            rounds: 0,
6305        };
6306        // The token-count proxy prices a round at ~1.9 plain tokens. That
6307        // holds for the Metal rounds whose cost was measured — greedy and
6308        // the sparse sampling chain — so an expensive round (the dense
6309        // chain, reachable only by `CMF_GRAPH_SPEC_SAMPLE=1`) still times
6310        // the plain path before it decides.
6311        let mut spec_mon = SpecMon {
6312            metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
6313            ..SpecMon::default()
6314        };
6315        let mut spec_watchdog_off = false;
6316        // CMF_GRAPH_SPEC_TIME: the round walls so far (round 1 excluded —
6317        // it pays the scratch), for the outlier test on each new one
6318        let mut spec_walls: Vec<f32> = Vec::new();
6319        // ... and the end of the last round: the host time between rounds
6320        // (token commits, streaming, the loop top) is printed at level 2
6321        let mut spec_round_end: Option<std::time::Instant> = None;
6322        if mimo_spec {
6323            if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
6324                if let Some(mut st) = self.mimo_mtp.take() {
6325                    self.mimo_mtp_probe(&mut st, input_ids, &path);
6326                    self.mimo_mtp = Some(st);
6327                }
6328            }
6329        }
6330        // ── Decode ──
6331        let mut next_pos = input_ids.len();
6332        'decode: while generated < max_tokens {
6333            if self
6334                .graph_failed
6335                .swap(false, std::sync::atomic::Ordering::Relaxed)
6336            {
6337                // Keep the detached MTP module attached after a terminal
6338                // graph error so the pipeline can be reused for a fresh
6339                // sequence.  `clear_sequence_state` only clears mirrors and
6340                // host KV; it cannot recover a module dropped here.
6341                self.finish_generation(&mut mtp, &mut router, true);
6342                return Err("GPU token graph failed during decode".to_string());
6343            }
6344            if self
6345                .cancel
6346                .swap(false, std::sync::atomic::Ordering::Relaxed)
6347            {
6348                finish_reason = "cancelled".to_string();
6349                break 'decode;
6350            }
6351            // A rejected speculative draft already drew this position's
6352            // token from the residual distribution (graph_spec_step); it
6353            // is committed as-is — sampling again from the row's logits
6354            // would bias the stream toward the target's mode.
6355            if mimo_spec && next_pos > 0 {
6356                // Every path leaves `hidden` = the backbone output at
6357                // next_pos-1; the draft layers read it (idempotent).
6358                self.mimo_note_rows(&hidden, next_pos - 1);
6359            }
6360            let forced = self.spec_forced.take();
6361            let mut logits = match (forced, self.graph_logits.take()) {
6362                (Some(_), _) => Vec::new(),
6363                (None, Some(lg)) => lg,
6364                (None, None) => {
6365                    let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
6366                    // Pooled-tail mode: the head as one layer-less device
6367                    // pass (final-norm + matvec + readback). Any decline —
6368                    // no pool, unresolvable weight, backend off — keeps the
6369                    // host head exactly as before.
6370                    #[cfg(feature = "gpu")]
6371                    let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
6372                        lg
6373                    } else {
6374                        inference::rms_norm_into(
6375                            &hidden,
6376                            &self.weights.final_norm,
6377                            self.rms_eps,
6378                            self.norm_style,
6379                            &mut self.ws.n1,
6380                        );
6381                        self.lm_head_forward(&self.ws.n1)
6382                    };
6383                    #[cfg(not(feature = "gpu"))]
6384                    let logits_out = {
6385                        inference::rms_norm_into(
6386                            &hidden,
6387                            &self.weights.final_norm,
6388                            self.rms_eps,
6389                            self.norm_style,
6390                            &mut self.ws.n1,
6391                        );
6392                        self.lm_head_forward(&self.ws.n1)
6393                    };
6394                    logits_out
6395                }
6396            };
6397            // CMF_LOGIT_DUMP=<path>: the first decode step's hidden + logits
6398            // as raw f32 (hidden first) — cross-backend numerics diffing.
6399            if generated
6400                == std::env::var("CMF_LOGIT_DUMP_STEP")
6401                    .ok()
6402                    .and_then(|v| v.parse().ok())
6403                    .unwrap_or(0)
6404            {
6405                if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
6406                    let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
6407                    for v in hidden.iter().chain(logits.iter()) {
6408                        bytes.extend_from_slice(&v.to_le_bytes());
6409                    }
6410                    if let Err(e) = std::fs::write(&path, &bytes) {
6411                        eprintln!("logit dump: failed to write {path}: {e}");
6412                        self.finish_generation(&mut mtp, &mut router, true);
6413                        return Err(format!("logit dump write failed: {e}"));
6414                    }
6415                }
6416            }
6417            // CMF_LOGIT_DUMP_ALL=<dir>: every decode step's logits as raw
6418            // f32, `<dir>/step{n:05}.f32` — step-by-step backend diffing
6419            // (a greedy run on two backends compares until they diverge).
6420            if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
6421                if !logits.is_empty() {
6422                    let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
6423                    let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
6424                    if let Err(e) =
6425                        std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
6426                    {
6427                        eprintln!("logit dump: failed to write {}: {e}", path.display());
6428                    }
6429                }
6430            }
6431            self.clover_causal_logits(&mut logits, generated, &all_ids[penalty_from..]);
6432            let t_next = match forced.or_else(|| forced_ids_at(generated)) {
6433                Some(c) => c,
6434                None => {
6435                    let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
6436                    sampler::sample_with_scratch_pool(
6437                        &logits,
6438                        &self.sampler_config,
6439                        self.sampler_config.penalty_past(&all_ids[penalty_from..], bounded_native),
6440                        &mut self.rng,
6441                        &mut self.sampler_scratch,
6442                        self.pool.as_deref(),
6443                    )
6444                }
6445            };
6446            if self.confidence_on {
6447                confidence.push(if logits.is_empty() {
6448                    0.0
6449                } else {
6450                    sampler::top1_prob_pool(
6451                        self.pool.as_deref(),
6452                        &mut self.sampler_scratch,
6453                        &logits,
6454                        t_next,
6455                        calib_temp,
6456                    )
6457                });
6458            }
6459            if !logits.is_empty() {
6460                attention::recycle_buf(&mut logits);
6461            }
6462            if trace_on {
6463                // active_skill = the overlay in force while this token was
6464                // generated; recon/switched are filled after the post-emit
6465                // routing eval below (freshest coherence for this token).
6466                let skill = router.as_ref().and_then(|r| r.active_id());
6467                traces.push(TokenTrace {
6468                    t: generated,
6469                    token_id: t_next,
6470                    confidence: confidence.last().copied().unwrap_or(0.0),
6471                    active_skill: skill,
6472                    recon: None,
6473                    switched: false,
6474                });
6475            }
6476            if !commit!(t_next) {
6477                break 'decode;
6478            }
6479            if generated >= max_tokens {
6480                break 'decode;
6481            }
6482
6483            // Catch-all for decode paths that bypass the walks' own trim
6484            // (a graph step, a speculative round).
6485            self.swa_trim_tails();
6486            if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
6487                // Say it ONCE, loudly: past this point the model keeps
6488                // talking but has lost half its context, and on a GDN
6489                // hybrid the graph's device state goes stale on top. The
6490                // Qwen3.8 bring-up spent a day reading this cliff as
6491                // three different model bugs.
6492                static SAID: std::sync::Once = std::sync::Once::new();
6493                SAID.call_once(|| {
6494                    tracing::warn!(
6495                        "KV cache full at {} positions — evicting half; quality \
6496                         will degrade. Raise CMF_MAX_SEQ.",
6497                        self.kv_cache.max_seq_len,
6498                    );
6499                });
6500                let keep = (self.kv_cache.max_seq_len / 2).max(1);
6501                self.kv_cache.evict(keep);
6502            }
6503
6504            // Advance the speculation trial: plain-phase accounting and
6505            // the periodic re-check happen here, on every token.
6506            if graph_spec {
6507                match spec_trial {
6508                    SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6509                        spec_mon.plain_ms =
6510                            t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6511                        let keep = spec_mon.pays();
6512                        tracing::info!(
6513                            "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6514                            spec_mon.tokens,
6515                            spec_mon.round_ms,
6516                            spec_mon.plain_ms,
6517                            if keep { "speculating" } else { "plain" }
6518                        );
6519                        spec_mon.fails = 0;
6520                        spec_trial = SpecTrial::Decided {
6521                            spec: keep,
6522                            recheck_at: if keep { usize::MAX } else { generated + 128 },
6523                        };
6524                    }
6525                    SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6526                        spec_mon.n = 0;
6527                        spec_mon.zero_accept_rounds = 0;
6528                        spec_trial = SpecTrial::Spec {
6529                            t0: std::time::Instant::now(),
6530                            gen0: generated,
6531                            rounds: 0,
6532                        };
6533                    }
6534                    _ => {}
6535                }
6536                spec_watchdog_off = matches!(
6537                    spec_trial,
6538                    SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6539                );
6540            }
6541            // ── MiMo-V2 draft stack: draft K, verify K+1 rows in one batch ──
6542            if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6543                let budget = max_tokens - generated - 1;
6544                if let Some(mut st) = self.mimo_mtp.take() {
6545                    let k = st.depth.min(budget);
6546                    let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6547                    self.mimo_mtp = Some(st);
6548                    let r = match r {
6549                        Ok(r) => r,
6550                        Err(err) => {
6551                            self.finish_generation(&mut mtp, &mut router, true);
6552                            return Err(err);
6553                        }
6554                    };
6555                    if let Some(r) = r {
6556                        drafted += r.drafted;
6557                        accepted += r.accepted.len();
6558                        let mut stopped = false;
6559                        for &id in &r.accepted {
6560                            if self.confidence_on {
6561                                confidence.push(0.0);
6562                            }
6563                            if !commit!(id) {
6564                                stopped = true;
6565                                break;
6566                            }
6567                        }
6568                        if stopped {
6569                            break 'decode;
6570                        }
6571                        next_pos += r.accepted.len() + 1;
6572                        hidden = r.hidden;
6573                        // The loop top chooses the round's own token from
6574                        // these logits — the same sampler, same history.
6575                        self.graph_logits = Some(r.logits);
6576                        continue 'decode;
6577                    }
6578                }
6579            }
6580            // ── Qwen3.8-Flash-Next draft head: k greedy drafts from the MTP
6581            //    sidecar, one batched verify window on the device ──
6582            #[cfg(feature = "gpu")]
6583            if self.speculative
6584                && self.qwen4_exp.is_some()
6585                && task_mask.is_none()
6586                && self.sampler_config.temperature < 1e-6
6587                && generated + 1 < max_tokens
6588                && next_pos > 0
6589                && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6590            {
6591                let r = match &mut self.qwen4_exp {
6592                    Some(b) => crate::qwen4_exp::spec_round(
6593                        &b.0,
6594                        &b.1,
6595                        &b.2,
6596                        &mut b.3,
6597                        next_pos,
6598                        &all_ids,
6599                        &self.inv_freq,
6600                        self.pool.as_deref(),
6601                    ),
6602                    None => None,
6603                };
6604                if let Some(r) = r {
6605                    drafted += r.drafted;
6606                    accepted += r.accepted.len();
6607                    let mut stopped = false;
6608                    for &id in &r.accepted {
6609                        if self.confidence_on {
6610                            confidence.push(0.0);
6611                        }
6612                        if !commit!(id) {
6613                            stopped = true;
6614                            break;
6615                        }
6616                    }
6617                    if stopped {
6618                        break 'decode;
6619                    }
6620                    next_pos += r.accepted.len() + 1;
6621                    hidden.fill(0.0);
6622                    self.graph_logits = Some(r.logits);
6623                    continue 'decode;
6624                }
6625            }
6626            match &mut mtp {
6627                // ── Graph speculation: chain-draft, batch-verify on device ──
6628                #[cfg(feature = "gpu")]
6629                Some(m)
6630                    if graph_spec
6631                        && !spec_watchdog_off
6632                        && generated + 1 < max_tokens
6633                        && next_pos > 0 =>
6634                {
6635                    let t_round = std::time::Instant::now();
6636                    if spec_time_level() >= 2 {
6637                        if let Some(t) = spec_round_end.take() {
6638                            eprintln!(
6639                                "spec-gap {:.2} ms (host between rounds)",
6640                                t.elapsed().as_secs_f64() * 1e3
6641                            );
6642                        }
6643                    }
6644                    spec_stamps_begin();
6645                    // device buffers allocated during this round: a
6646                    // first-touch Shared allocation is zero-filled inside
6647                    // the command buffer that uses it, which is what the
6648                    // long outlier rounds were
6649                    #[cfg(target_os = "macos")]
6650                    let allocs0 =
6651                        crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6652                    #[cfg(not(target_os = "macos"))]
6653                    let allocs0 = 0u64;
6654                    if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6655                        m,
6656                        &hidden,
6657                        t_next,
6658                        next_pos,
6659                        &mut drafted,
6660                        &mut accepted,
6661                        &mut all_ids,
6662                        max_tokens - generated,
6663                    ) {
6664                        next_pos = n_pos;
6665                        hidden = new_h;
6666                        let level = spec_time_level();
6667                        if level > 0 {
6668                            let wall = t_round.elapsed().as_secs_f32() * 1e3;
6669                            let stamps = spec_stamps_take();
6670                            // the running median of the rounds before this
6671                            // one (round 1 pays the scratch: not a sample)
6672                            let median = if spec_walls.len() >= 3 {
6673                                let mut s = spec_walls.clone();
6674                                s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6675                                Some(s[s.len() / 2])
6676                            } else {
6677                                None
6678                            };
6679                            let outlier = median.is_some_and(|m| wall > 1.4 * m);
6680                            #[cfg(target_os = "macos")]
6681                            let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6682                                .load(std::sync::atomic::Ordering::Relaxed)
6683                                - allocs0;
6684                            #[cfg(not(target_os = "macos"))]
6685                            let allocs = allocs0;
6686                            eprintln!(
6687                                "spec-round wall {wall:.1} ms → {} tokens{}{}",
6688                                extra.len() + 1,
6689                                if allocs > 0 {
6690                                    format!(" [{allocs} new device buffers]")
6691                                } else {
6692                                    String::new()
6693                                },
6694                                match (outlier, median) {
6695                                    (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6696                                    _ => String::new(),
6697                                }
6698                            );
6699                            if level >= 2 || outlier {
6700                                let sum: f32 = stamps.iter().map(|s| s.1).sum();
6701                                eprintln!(
6702                                    "spec-stamps: {}| untracked {:.1}",
6703                                    spec_stamps_format(&stamps),
6704                                    wall - sum
6705                                );
6706                            }
6707                            if spec_mon.n >= 1 {
6708                                spec_walls.push(wall);
6709                            }
6710                        }
6711                        // One speculative round done: the monitor counts it
6712                        // (round 1 untimed — it pays the batch scratch and
6713                        // the draft mirror), and the trial advances.
6714                        spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6715                        // the round's tokens land in `generated` below; the
6716                        // plain phase must start counting AFTER them
6717                        spec_trial = Self::spec_trial_round(
6718                            spec_trial,
6719                            &mut spec_mon,
6720                            generated + extra.len() + 1,
6721                        );
6722                        let mut stopped = false;
6723                        for &id in &extra {
6724                            if self.confidence_on {
6725                                confidence.push(0.0);
6726                            }
6727                            // `graph_spec_step` commits the accepted MTP
6728                            // drafts as a batch.  They do not pass through
6729                            // the ordinary loop-top sampling block below,
6730                            // which is normally where the telemetry row is
6731                            // created.  Keep the public trace contract
6732                            // (one row per emitted token) here as well.
6733                            //
6734                            // The graph-spec path does not surface a
6735                            // per-row probability for accepted extras.  It
6736                            // deliberately matches the zero placed in the
6737                            // confidence channel above instead of inventing
6738                            // one or adding a separate probability pass.
6739                            if trace_on {
6740                                traces.push(TokenTrace {
6741                                    t: generated,
6742                                    token_id: id,
6743                                    confidence: confidence.last().copied().unwrap_or(0.0),
6744                                    active_skill: None,
6745                                    recon: None,
6746                                    switched: false,
6747                                });
6748                            }
6749                            if !commit!(id) {
6750                                stopped = true;
6751                                break;
6752                            }
6753                        }
6754                        if stopped {
6755                            break 'decode;
6756                        }
6757                        if spec_time_level() >= 2 {
6758                            spec_round_end = Some(std::time::Instant::now());
6759                        }
6760                        continue 'decode;
6761                    }
6762                    if self
6763                        .graph_failed
6764                        .swap(false, std::sync::atomic::Ordering::Relaxed)
6765                    {
6766                        // `graph_spec_step` may have detached MTP while a
6767                        // warm-up was in flight.  Do not reinterpret its
6768                        // terminal device failure as a plain decode step;
6769                        // restore the head, clear both mirrors, and surface
6770                        // one explicit error to the caller.
6771                        self.finish_generation(&mut mtp, &mut router, true);
6772                        return Err("GPU MTP graph failed during speculative decode".to_string());
6773                    }
6774                    // Declined (batch graph refused): plain forward below —
6775                    // and a round that produced one token for the trial's
6776                    // ledger, so a graph that keeps refusing is measured out
6777                    // like a head that keeps missing (it was spinning
6778                    // forever on a file whose batch graph declines).
6779                    // A declined round is not a cheap one-token round — it
6780                    // is a verify that does not exist for this file (a
6781                    // healed q8_2f tail measured 760 drafts, 0 accepted, 33
6782                    // against 48.8 tok/s while the monitor called the draft
6783                    // alone "paying"). Count it as the losing streak in one.
6784                    spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6785                    spec_mon.tokens = 0.0;
6786                    spec_mon.fails = 3;
6787                    spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6788                    hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6789                    next_pos += 1;
6790                    continue 'decode;
6791                }
6792                // ── Speculative: draft t+2, verify in a fused pair ──
6793                Some(m) if !graph_spec && generated + 1 < max_tokens => {
6794                    let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6795                    drafted += 1;
6796                    let emb1 = self.embed_single(t_next);
6797                    let emb2 = self.embed_single(draft);
6798                    let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6799
6800                    inference::rms_norm_into(
6801                        &h1,
6802                        &self.weights.final_norm,
6803                        self.rms_eps,
6804                        self.norm_style,
6805                        &mut self.ws.n1,
6806                    );
6807                    let mut logits1 = self.lm_head_forward(&self.ws.n1);
6808                    let t_after = sampler::sample_with_scratch_pool(
6809                        &logits1,
6810                        &self.sampler_config,
6811                        self.sampler_config.penalty_past(&all_ids, bounded_native),
6812                        &mut self.rng,
6813                        &mut self.sampler_scratch,
6814                        self.pool.as_deref(),
6815                    );
6816                    if self.confidence_on {
6817                        confidence.push(sampler::top1_prob_pool(
6818                            self.pool.as_deref(),
6819                            &mut self.sampler_scratch,
6820                            &logits1,
6821                            t_after,
6822                            calib_temp,
6823                        ));
6824                    }
6825                    attention::recycle_buf(&mut logits1);
6826                    if trace_on {
6827                        // Speculative decode is mutually exclusive with
6828                        // dynamic routing (router is None here) — no skill.
6829                        traces.push(TokenTrace {
6830                            t: generated,
6831                            token_id: t_after,
6832                            confidence: confidence.last().copied().unwrap_or(0.0),
6833                            active_skill: None,
6834                            recon: None,
6835                            switched: false,
6836                        });
6837                    }
6838                    let stop = !commit!(t_after);
6839
6840                    if t_after == draft {
6841                        accepted += 1;
6842                        self.commit_linear_scratch();
6843                        let _ = self.mtp_step(m, &h1, t_after, next_pos);
6844                        hidden = h2;
6845                        next_pos += 2;
6846                    } else {
6847                        // The draft lane is wrong: roll its KV entry back.
6848                        for layer in &mut self.kv_cache.layers {
6849                            layer.truncate_last(1);
6850                        }
6851                        if !stop {
6852                            let _ = self.mtp_step(m, &h1, t_after, next_pos);
6853                            hidden = self.forward_layers(
6854                                &self.embed_single(t_after),
6855                                next_pos + 1,
6856                                None,
6857                            );
6858                        }
6859                        next_pos += 2;
6860                    }
6861                    if stop {
6862                        break 'decode;
6863                    }
6864                }
6865                // ── Vanilla: forward the sampled token ──
6866                _ => {
6867                    // ── DeepSeek-V4 speculative decode (CMF_DSV4_SPEC=1):
6868                    // draft five on the card, verify batched, commit the
6869                    // accepted prefix. Greedy only; a rejected token's state
6870                    // is restored and replayed, so output equals the walk. ──
6871                    #[cfg(feature = "gpu")]
6872                    if Self::dsv4_spec_on() && self.dsv4.is_some() {
6873                        static SAID: std::sync::Once = std::sync::Once::new();
6874                        SAID.call_once(|| {
6875                            eprintln!(
6876                                "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6877                                !self.dsv4_mtp.is_empty(),
6878                                task_mask.is_none(),
6879                                router.is_none(),
6880                                !trace_on,
6881                                self.sampler_config.temperature < 1e-6,
6882                                self.sampler_config.repetition_penalty == 1.0,
6883                            );
6884                        });
6885                    }
6886                    #[cfg(feature = "gpu")]
6887                    if Self::dsv4_spec_on()
6888                        && self.dsv4.is_some()
6889                        && !self.dsv4_mtp.is_empty()
6890                        && task_mask.is_none()
6891                        && router.is_none()
6892                        && !trace_on
6893                        && self.sampler_config.temperature < 1e-6
6894                        && self.sampler_config.repetition_penalty == 1.0
6895                        && generated + 1 < max_tokens
6896                        && all_ids.len() >= 2
6897                        && generated >= dsv4_spec_retry_at
6898                    {
6899                        let tip_token = all_ids[all_ids.len() - 2];
6900                        let drafted0 = drafted;
6901                        let round = self.dsv4_spec_step(
6902                            tip_token,
6903                            t_next,
6904                            next_pos,
6905                            max_tokens.saturating_sub(generated),
6906                            &mut drafted,
6907                            &mut accepted,
6908                        );
6909                        if drafted > drafted0 {
6910                            let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6911                            if useful {
6912                                dsv4_spec_bad = 0;
6913                            } else {
6914                                dsv4_spec_bad += 1;
6915                                if dsv4_spec_bad >= 2 {
6916                                    dsv4_spec_bad = 0;
6917                                    dsv4_spec_retry_at = generated.saturating_add(32);
6918                                    tracing::info!(
6919                                        "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6920                                    );
6921                                }
6922                            }
6923                        }
6924                        if let Some((extra, n_pos)) = round {
6925                            next_pos = n_pos;
6926                            let mut stopped = false;
6927                            for &id in &extra {
6928                                if self.confidence_on {
6929                                    confidence.push(0.0);
6930                                }
6931                                if !commit!(id) {
6932                                    stopped = true;
6933                                    break;
6934                                }
6935                            }
6936                            if stopped {
6937                                break 'decode;
6938                            }
6939                            continue 'decode;
6940                        }
6941                    }
6942                    self.graph_want_logits = fuse_lm;
6943                    // Greedy burst (CMF_MULTISTEP, off unless explicitly set): while
6944                    // nothing observes per-token state — pure argmax sampling,
6945                    // no router/trace/confidence/mask — decode k tokens per
6946                    // submit and commit them wholesale. The trailing normal
6947                    // forward leaves logits for the loop top, as always.
6948                    let mut t_fwd = t_next;
6949                    let pure_greedy = self.sampler_config.temperature < 1e-6
6950                        && self.sampler_config.repetition_penalty == 1.0
6951                        && self.sampler_config.presence_penalty == 0.0
6952                        && self.sampler_config.suppress_tokens.is_empty();
6953                    // Off by default: at every k the burst measured at or
6954                    // below the plain path on this graph shape (k=1 loses
6955                    // the argmax dispatches vs a 1 MB readback, k>=8 loses
6956                    // inter-step drains vs the saved sync). Experimental.
6957                    let burst_k = std::env::var("CMF_MULTISTEP")
6958                        .ok()
6959                        .and_then(|v| v.parse::<usize>().ok())
6960                        .unwrap_or(0);
6961                    if pure_greedy
6962                        && burst_k >= 1
6963                        && fuse_lm
6964                        && task_mask.is_none()
6965                        && router.is_none()
6966                        && !trace_on
6967                        && !self.confidence_on
6968                        // Diagnostics and teacher forcing observe every
6969                        // individual logit row.  A device-only burst returns
6970                        // only ids, so decline it rather than silently
6971                        // skipping requested evidence or forced ids.
6972                        && std::env::var_os("CMF_FORCE_IDS").is_none()
6973                        && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6974                        && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6975                    {
6976                        let mut stopped = false;
6977                        loop {
6978                            // The outer loop checks cancellation too, but a
6979                            // long run can stay in this burst loop for many
6980                            // submits.  Bound cancellation latency to one
6981                            // exact k-token frame.
6982                            if self
6983                                .cancel
6984                                .swap(false, std::sync::atomic::Ordering::Relaxed)
6985                            {
6986                                finish_reason = "cancelled".to_string();
6987                                stopped = true;
6988                                break;
6989                            }
6990                            let room = max_tokens.saturating_sub(generated);
6991                            if room <= 2 {
6992                                break;
6993                            }
6994                            let k = burst_k.min(room - 1);
6995                            if k < 1 {
6996                                break;
6997                            }
6998                            let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6999                                if self
7000                                    .graph_failed
7001                                    .swap(false, std::sync::atomic::Ordering::Relaxed)
7002                                {
7003                                    self.finish_generation(&mut mtp, &mut router, true);
7004                                    return Err(
7005                                        "GPU token graph failed during greedy burst".to_string()
7006                                    );
7007                                }
7008                                break;
7009                            };
7010                            next_pos += k;
7011                            for &id in &ids {
7012                                if !commit!(id) {
7013                                    stopped = true;
7014                                    break;
7015                                }
7016                            }
7017                            if stopped {
7018                                break;
7019                            }
7020                            t_fwd = *ids.last().unwrap();
7021                        }
7022                        if stopped {
7023                            break 'decode;
7024                        }
7025                    }
7026                    // Metal: keep the draft head's cache in step through
7027                    // the trial's plain phase and a paused speculation —
7028                    // the pair (hidden, t_fwd) at next_pos−1, the step the
7029                    // round's draft 0 would take. Without it the head's
7030                    // cache lagged the trunk by every plain token for the
7031                    // rest of the generation: the batched warm-up declined
7032                    // every later round and its rows went one by one (a
7033                    // whole MTP step per accepted token), and the drafts
7034                    // attended a context with those tokens missing.
7035                    #[cfg(target_os = "macos")]
7036                    if graph_spec
7037                        && spec_watchdog_off
7038                        && next_pos > 0
7039                        && self.mtp_graph_mode == Some(true)
7040                        && crate::gpu::q1_force()
7041                    {
7042                        if let Some(m) = mtp.as_mut() {
7043                            let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
7044                        }
7045                    }
7046                    hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
7047                    next_pos += 1;
7048                    // Dynamic routing: the forward updated φ; ask the
7049                    // router whether to switch skills before the next token.
7050                    if let Some(r) = &mut router {
7051                        let phi = self.dyn_phi_ema.clone();
7052                        let decision = r.step(&phi, generated);
7053                        if let Some(new_active) = decision {
7054                            let _ = self.set_active_skill(new_active);
7055                        }
7056                        // Backfill this token's coherence + switch flag from
7057                        // the just-run eval (freshest measured values).
7058                        if trace_on {
7059                            if let Some(last) = traces.last_mut() {
7060                                let e = r.last_best_e();
7061                                last.recon = e.is_finite().then_some(e);
7062                                last.switched = decision.is_some();
7063                            }
7064                        }
7065                    }
7066                }
7067            }
7068        }
7069
7070        let cancelled = finish_reason == "cancelled";
7071        // A generation during which the router switched weights holds no
7072        // state any single overlay would produce (each switch cleared the
7073        // cache mid-sequence), so it leaves no reuse key behind either.
7074        let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
7075        if mimo_spec {
7076            if let Some(st) = self.mimo_mtp.as_ref() {
7077                let line = st.stats.line();
7078                tracing::info!("{line}");
7079                if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
7080                    eprintln!("{line}");
7081                }
7082            }
7083        }
7084        self.finish_generation(&mut mtp, &mut router, cancelled);
7085
7086        let output_ids = &all_ids[input_ids.len()..];
7087        // Forwarded = prompt + all generated but the LAST sampled token
7088        // (emitted without being fed back). Exact only without MTP —
7089        // reuse is gated off when MTP is active.
7090        let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
7091        // A MiMo speculative round that stopped on an accepted draft (EOS,
7092        // cancel) leaves verify rows past the committed stream in the cache:
7093        // never offer that cache for reuse. Neither does a router that
7094        // switched weights mid-sequence (no single overlay produced it).
7095        let consumed = std::mem::take(&mut all_ids);
7096        if dyn_switched {
7097            self.clear_sequence_state();
7098        } else if cancelled || mimo_spec || prompt_rows.is_some() {
7099            self.clear_history();
7100        } else {
7101            self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
7102        }
7103        all_ids = consumed;
7104        let output_ids = &all_ids[input_ids.len()..];
7105        confidence.truncate(output_ids.len()); // guard against any overshoot
7106        traces.truncate(output_ids.len());
7107        Ok(GenerateResult {
7108            text: self.tokenizer.decode(output_ids),
7109            token_ids: output_ids.to_vec(),
7110            prompt_tokens: input_ids.len(),
7111            tokens_generated: generated,
7112            finish_reason,
7113            mtp_drafted: drafted,
7114            mtp_accepted: accepted,
7115            token_confidence: confidence,
7116            traces,
7117        })
7118    }
7119
7120    /// One MTP step: feed `(hidden_p, token_{p+1})` into the draft head,
7121    /// advance its KV cache at position `p`, return the drafted token
7122    /// for position `p+2`.
7123    fn mtp_step(
7124        &mut self,
7125        m: &mut MtpModule,
7126        hidden: &[f32],
7127        next_token: u32,
7128        position: usize,
7129    ) -> u32 {
7130        self.mtp_step_h(m, hidden, next_token, position).0
7131    }
7132
7133    /// Tally for `CMF_MTP_CHAIN_PROBE`: per depth, how often the CHAIN is
7134    /// still an exact prefix of the real continuation. Printed every 128
7135    /// depth-0 samples so a killed run still shows its table.
7136    fn chain_probe_note(depth: usize, prefix_ok: bool) {
7137        use std::sync::Mutex;
7138        static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
7139        let mut t = T.lock().unwrap();
7140        if t.len() <= depth {
7141            t.resize(depth + 1, (0, 0));
7142        }
7143        t[depth].0 += 1;
7144        t[depth].1 += prefix_ok as u64;
7145        if depth == 0 && t[0].0 % 128 == 0 {
7146            let line: Vec<String> = t
7147                .iter()
7148                .enumerate()
7149                .map(|(d, (n, k))| {
7150                    format!(
7151                        "d{}={:.0}%({n})",
7152                        d + 1,
7153                        100.0 * *k as f64 / (*n).max(1) as f64
7154                    )
7155                })
7156                .collect();
7157            eprintln!("mtp-chain: {}", line.join(" "));
7158        }
7159    }
7160
7161    /// `mtp_step` that also hands back the block's own output hidden — the
7162    /// state a CHAINED draft feeds the next step, the way a multi-token
7163    /// speculative round iterates the head on itself.
7164    /// One MTP block step from (trunk hidden, token): the head's LOGITS
7165    /// and the block's own hidden for chaining. The draft is argmax of the
7166    /// logits on the greedy path and a draw from their post-chain
7167    /// distribution on the sampling path.
7168    fn mtp_step_hl(
7169        &mut self,
7170        m: &mut MtpModule,
7171        hidden: &[f32],
7172        next_token: u32,
7173        position: usize,
7174    ) -> (Vec<f32>, Vec<f32>) {
7175        // The graph arm: the MTP block as a one-layer token graph with the
7176        // head fused — device attention over the block's own KV mirror,
7177        // one submit for block + head, hidden and logits back together.
7178        // Decided once per generation (see `mtp_graph_mode`).
7179        #[cfg(target_os = "macos")]
7180        if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
7181            if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
7182                self.mtp_graph_mode = Some(true);
7183                return r;
7184            }
7185            if self.mtp_graph_mode == Some(true) {
7186                tracing::error!("mtp Metal graph failed after admission");
7187                self.clear_sequence_state();
7188                self.graph_failed
7189                    .store(true, std::sync::atomic::Ordering::Relaxed);
7190                self.cancel
7191                    .store(true, std::sync::atomic::Ordering::Relaxed);
7192                return (Vec::new(), Vec::new());
7193            }
7194            self.mtp_graph_mode = Some(false);
7195        }
7196        #[cfg(feature = "gpu")]
7197        if self.mtp_graph_mode != Some(false) {
7198            if !self.mtp_graph_ok(m) {
7199                if self.mtp_graph_mode == Some(true) {
7200                    // A mirror was already admitted, so a capability change
7201                    // cannot safely switch this request to the stale CPU
7202                    // cache.  Keep the same terminal contract as a failed
7203                    // token graph.
7204                    tracing::error!("mtp graph became unavailable after admission");
7205                    self.clear_sequence_state();
7206                    self.graph_failed
7207                        .store(true, std::sync::atomic::Ordering::Relaxed);
7208                    self.cancel
7209                        .store(true, std::sync::atomic::Ordering::Relaxed);
7210                    return (Vec::new(), Vec::new());
7211                }
7212                self.mtp_graph_mode = Some(false);
7213            } else {
7214                if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
7215                    self.mtp_graph_mode = Some(true);
7216                    return r;
7217                }
7218                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7219                    // A token graph can have admitted a persistent MTP/GDN
7220                    // mirror before its readback failed.  The CPU MTP cache
7221                    // is not a valid continuation in that state; leave the
7222                    // flag set so the generation caller returns through its
7223                    // terminal error path instead of silently switching
7224                    // arithmetic.
7225                    return (Vec::new(), Vec::new());
7226                }
7227                // `mtp_graph_ok` was true, so a None here means a refusal or
7228                // failure after graph admission.  Do not fall through to a
7229                // CPU cache whose rows may lag the device mirror.
7230                tracing::error!("mtp graph failed or declined after admission");
7231                self.clear_sequence_state();
7232                self.graph_failed
7233                    .store(true, std::sync::atomic::Ordering::Relaxed);
7234                self.cancel
7235                    .store(true, std::sync::atomic::Ordering::Relaxed);
7236                return (Vec::new(), Vec::new());
7237            }
7238        }
7239        // fc concat order is [enorm(embed); hnorm(hidden)] — EMBEDDING
7240        // FIRST. Verified by the oracle (converter/mtp_oracle.py):
7241        // [emb;hid] → 45.8% acceptance, [hid;emb] → 0.00%.
7242        let e = self.embed_single(next_token);
7243        let mut cat = vec![0.0f32; 2 * self.hidden_size];
7244        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7245        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7246        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7247        let mut x = vec![0.0f32; self.hidden_size];
7248        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7249
7250        // One standard transformer block over the MTP's own cache.
7251        let lw = &m.layer;
7252        inference::rms_norm_into(
7253            &x,
7254            &lw.input_norm,
7255            self.rms_eps,
7256            self.norm_style,
7257            &mut self.ws.n1,
7258        );
7259        let attn = match &lw.attn {
7260            // MLA models carry no MTP head; this path cannot see them.
7261            AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
7262            AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
7263            AttnKind::Full {
7264                wq,
7265                wk,
7266                wv,
7267                wo,
7268                q_norm,
7269                k_norm,
7270                output_gate,
7271                softplus_gate,
7272                bias,
7273            } => {
7274                let mut cfg = self.attn_cfg(position);
7275                cfg.q_norm = q_norm.as_deref();
7276                cfg.k_norm = k_norm.as_deref();
7277                cfg.output_gate = *output_gate;
7278                cfg.softplus_gate = softplus_gate
7279                    .as_ref()
7280                    .map(|(gate, per_head)| (gate, *per_head));
7281                cfg.bias = bias
7282                    .as_ref()
7283                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7284                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7285            }
7286            AttnKind::Linear(_)
7287            | AttnKind::LinearGdn(_)
7288            | AttnKind::ShortConv(_)
7289            | AttnKind::Bounded(_) => {
7290                unreachable!("MTP block is full attention")
7291            }
7292        };
7293        for (i, &a) in attn.iter().enumerate() {
7294            x[i] += a;
7295        }
7296        inference::rms_norm_into(
7297            &x,
7298            &lw.post_norm,
7299            self.rms_eps,
7300            self.norm_style,
7301            &mut self.ws.p1,
7302        );
7303        let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
7304        for (i, &f) in ffn.iter().enumerate() {
7305            x[i] += f;
7306        }
7307
7308        inference::rms_norm_into(
7309            &x,
7310            &m.final_norm,
7311            self.rms_eps,
7312            self.norm_style,
7313            &mut self.ws.n1,
7314        );
7315        let lg = self.lm_head_forward(&self.ws.n1);
7316        (lg, x)
7317    }
7318
7319    /// `mtp_step_hl` reduced to the greedy draft: argmax of the head.
7320    fn mtp_step_h(
7321        &mut self,
7322        m: &mut MtpModule,
7323        hidden: &[f32],
7324        next_token: u32,
7325        position: usize,
7326    ) -> (u32, Vec<f32>) {
7327        let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
7328        let draft = sampler::argmax(&lg);
7329        attention::recycle_buf(&mut lg);
7330        (draft, x)
7331    }
7332
7333    /// One speculative round for the trial: rounds 1..5 of a `Spec` phase
7334    /// advance it (the monitor already averaged this round); after five,
7335    /// the plain phase runs (once — a known plain rate decides at once);
7336    /// a decided speculation keeps re-checking the rule every round and
7337    /// stops after four losing rounds in a row.
7338    fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
7339        match trial {
7340            SpecTrial::Spec { t0, gen0, rounds } => {
7341                let rounds = rounds + 1;
7342                // A round that accepts no draft still has to run the MTP
7343                // chain and a K+1 verifier, but emits only its verifier tip.
7344                // After two such real rounds the speculative arm cannot win
7345                // on any backend; time plain immediately rather than spend
7346                // the remaining exploratory rounds on the same one-token
7347                // result. A later 128-token re-check still gives changing
7348                // context a chance to re-admit it.
7349                if mon.repeated_zero_acceptance() {
7350                    tracing::info!(
7351                        "speculation trial: no accepted drafts in two rounds — timing plain"
7352                    );
7353                    return SpecTrial::Plain {
7354                        t0: std::time::Instant::now(),
7355                        gen0: generated,
7356                    };
7357                }
7358                if rounds >= 5 {
7359                    if mon.plain_ms > 0.0 {
7360                        let keep = mon.pays();
7361                        mon.fails = 0;
7362                        tracing::info!(
7363                            "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
7364                            mon.tokens,
7365                            mon.round_ms,
7366                            mon.plain_ms,
7367                            if keep { "speculating" } else { "plain" }
7368                        );
7369                        SpecTrial::Decided {
7370                            spec: keep,
7371                            recheck_at: if keep { usize::MAX } else { generated + 128 },
7372                        }
7373                    } else if mon.pays() {
7374                        // Metal: the rounds land enough tokens each that no
7375                        // plain measurement is needed — keep speculating,
7376                        // and re-check every round (a losing streak sends
7377                        // the loop to the plain phase, below).
7378                        mon.fails = 0;
7379                        tracing::info!(
7380                            "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
7381                            mon.tokens,
7382                            mon.round_ms,
7383                        );
7384                        SpecTrial::Decided {
7385                            spec: true,
7386                            recheck_at: usize::MAX,
7387                        }
7388                    } else {
7389                        SpecTrial::Plain {
7390                            t0: std::time::Instant::now(),
7391                            gen0: generated,
7392                        }
7393                    }
7394                } else {
7395                    SpecTrial::Spec { t0, gen0, rounds }
7396                }
7397            }
7398            SpecTrial::Decided { spec: true, .. } => {
7399                if mon.pays() {
7400                    mon.fails = 0;
7401                    trial
7402                } else {
7403                    mon.fails += 1;
7404                    if mon.fails >= 4 {
7405                        if mon.plain_ms <= 0.0 {
7406                            // Metal, plain never timed: four doubtful rounds
7407                            // buy the (bounded) plain measurement, and the
7408                            // exact rule decides from it.
7409                            tracing::info!(
7410                                "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
7411                                mon.tokens,
7412                                mon.round_ms,
7413                            );
7414                            return SpecTrial::Plain {
7415                                t0: std::time::Instant::now(),
7416                                gen0: generated,
7417                            };
7418                        }
7419                        tracing::info!(
7420                            "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
7421                            mon.tokens,
7422                            mon.round_ms,
7423                            mon.plain_ms
7424                        );
7425                        SpecTrial::Decided {
7426                            spec: false,
7427                            recheck_at: generated + 128,
7428                        }
7429                    } else {
7430                        trial
7431                    }
7432                }
7433            }
7434            other => other,
7435        }
7436    }
7437
7438    /// The MTP block's device-mirror id: the trunk's id with a high bit,
7439    /// so the (kv_id, layer) mirror keys never collide.
7440    fn mtp_kv_id(&self) -> u64 {
7441        self.graph_kv_id | (1u64 << 40)
7442    }
7443
7444    /// The MTP block's mirror layer index: 0 — its own kv_id keeps it
7445    /// apart from the trunk, and the BATCH graph (the warm-up path) keys
7446    /// its mirrors at layer 0 with no base of its own, so the draft's
7447    /// token graph must key the same slot.
7448    const MTP_LAYER_BASE: usize = 0;
7449
7450    /// The wgpu MTP draft writes speculative rows straight into its device
7451    /// mirror while the CPU owner retains only the real prompt/decode anchor.
7452    /// After verification, move that mirror cursor back to the anchor before
7453    /// replaying accepted pairs.  The next graph append then sees the same
7454    /// contiguous position as the CPU/Metal path without uploading stale
7455    /// speculative rows.
7456    #[cfg(feature = "gpu")]
7457    fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
7458        self.mtp_graph_mode != Some(true)
7459            || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
7460    }
7461
7462    /// A speculative verify graph appends the full `k+1` trunk rows before
7463    /// the acceptance count is known.  GDN state already has a snapshot
7464    /// restore; Full-attention mirrors need the matching logical cursor
7465    /// rewind so the next graph call does not reject an ahead-of-position KV
7466    /// cache after a partial acceptance.
7467    #[cfg(feature = "gpu")]
7468    fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
7469        let mut ok = true;
7470        let mut expected = false;
7471        for li in 0..self.num_layers {
7472            if matches!(
7473                self.weights.layers[self.phys_layer(li)].attn,
7474                AttnKind::Full { .. }
7475            ) {
7476                expected = true;
7477                ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
7478            }
7479        }
7480        !expected || ok
7481    }
7482
7483    /// Count the recurrent layers participating in the trunk verify graph.
7484    /// Snapshot restore is all-or-nothing across that set; deriving the count
7485    /// from the model keeps the restore contract valid for looped models too.
7486    fn graph_gdn_layer_count(&self) -> usize {
7487        (0..self.num_layers)
7488            .filter(|&li| {
7489                matches!(
7490                    &self.weights.layers[self.phys_layer(li)].attn,
7491                    AttnKind::LinearGdn(_)
7492                )
7493            })
7494            .count()
7495    }
7496
7497    /// The block's input from (trunk hidden, token): eh_proj · [enorm(e);
7498    /// hnorm(h)] — the same arithmetic the per-op path starts with.
7499    fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
7500        let e = self.embed_single(next_token);
7501        let mut cat = vec![0.0f32; 2 * self.hidden_size];
7502        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7503        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7504        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7505        let mut x = vec![0.0f32; self.hidden_size];
7506        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7507        x
7508    }
7509
7510    /// Is the MTP block graphable at all (device up, full attention
7511    /// without softplus, dense FFN)? The plan itself is built per call.
7512    #[cfg(feature = "gpu")]
7513    fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7514        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7515            return false;
7516        }
7517        if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7518            || !crate::gpu::enabled_here()
7519            || self.attn_softcap > 0.0
7520            || self.attention_heads_per_layer.is_some()
7521            // The block graph caches V as wide as K and feeds o_proj
7522            // nh·head_dim; a narrow-V model keeps its MTP block per-op.
7523            || self.v_head_dim.is_some()
7524        {
7525            return false;
7526        }
7527        matches!(
7528            &m.layer.attn,
7529            AttnKind::Full {
7530                softplus_gate: None,
7531                ..
7532            }
7533        ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7534    }
7535
7536    /// Full MTP token-graph eligibility, including the fused lm-head and all
7537    /// block projection weights.  Keep this distinct from the block-only
7538    /// check: prompt warm-up does not need the head, while a draft step does.
7539    #[cfg(feature = "gpu")]
7540    fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7541        if !self.mtp_block_graph_ok(m) {
7542            return false;
7543        }
7544        let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7545            return false;
7546        };
7547        let FfnKind::Dense(d) = &m.layer.ffn else {
7548            return false;
7549        };
7550        d.segs.is_empty()
7551            && wq.graph_weight().is_some()
7552            && wk.graph_weight().is_some()
7553            && wv.graph_weight().is_some()
7554            && wo.graph_weight().is_some()
7555            && d.gate_proj.graph_weight().is_some()
7556            && d.up_proj.graph_weight().is_some()
7557            && d.down_proj.graph_weight().is_some()
7558            && self.weights.lm_head.graph_weight().is_some()
7559    }
7560
7561    /// One MTP block step on the wgpu token graph: block + fused head in
7562    /// one submit, the block hidden and the logits read back together.
7563    /// None = the graph cannot take this block (softplus gate, non-dense
7564    /// FFN, unquantized head, no device) — the caller keeps the per-op
7565    /// path for the whole generation.
7566    #[cfg(feature = "gpu")]
7567    fn mtp_step_graph(
7568        &mut self,
7569        m: &mut MtpModule,
7570        hidden: &[f32],
7571        next_token: u32,
7572        position: usize,
7573    ) -> Option<(Vec<f32>, Vec<f32>)> {
7574        if !self.mtp_graph_ok(m) {
7575            return None;
7576        }
7577        let lw = &m.layer;
7578        let AttnKind::Full {
7579            wq,
7580            wk,
7581            wv,
7582            wo,
7583            q_norm,
7584            k_norm,
7585            output_gate,
7586            softplus_gate,
7587            bias,
7588        } = &lw.attn
7589        else {
7590            return None;
7591        };
7592        if softplus_gate.is_some() {
7593            return None;
7594        }
7595        let FfnKind::Dense(d) = &lw.ffn else {
7596            return None;
7597        };
7598        if !d.segs.is_empty() {
7599            return None; // tube layers run on the segmented path
7600        }
7601        // The block's input first: it borrows `self` mutably (embed scratch,
7602        // pool), the plan below borrows the weights immutably.
7603        let mut x = self.mtp_block_input(m, hidden, next_token);
7604        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7605            let (_, i, kind, rs) = t.graph_weight()?;
7606            Some(crate::gpu::GraphW {
7607                idx: i,
7608                kind,
7609                row_scale: rs,
7610                data: &[],
7611                prism: crate::gpu::GraphPrismOp::None,
7612                affine: false,
7613            })
7614        }
7615        let (model, _, _, _) = wq.graph_weight()?;
7616        let model = model.clone();
7617        let (lm_gw, lm_rows) = {
7618            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7619            // The draft's head over the CMF_DRAFT_VOCAB shortlist (the same
7620            // cut the native Metal draft takes): 662 MB a step on Qwen3.8
7621            // becomes 170 MB at 65536; the verify keeps the full head.
7622            let rows = if kind == 6 {
7623                self.draft_head_rows(self.weights.lm_head.rows())
7624            } else {
7625                self.weights.lm_head.rows()
7626            };
7627            (
7628                crate::gpu::GraphW {
7629                    idx: i,
7630                    kind,
7631                    row_scale: rs,
7632                    data: &[],
7633                    prism: crate::gpu::GraphPrismOp::None,
7634                    affine: false,
7635                },
7636                rows,
7637            )
7638        };
7639        let layer = crate::gpu::GraphLayer {
7640            mhc: None,
7641            input_norm: &lw.input_norm,
7642            attn: crate::gpu::GraphAttn::Full {
7643                wq: gw(wq)?,
7644                wk: gw(wk)?,
7645                wv: gw(wv)?,
7646                wo: gw(wo)?,
7647                q_norm: q_norm.as_deref(),
7648                k_norm: k_norm.as_deref(),
7649                late_qk_norm: self.qk_norm_after_rope,
7650                bias: bias
7651                    .as_ref()
7652                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7653                output_gate: *output_gate,
7654                cpu_k: m.kv.k_heads(),
7655                cpu_v: m.kv.v_heads(),
7656                cpu_base: m.kv.base(),
7657                geom: None,
7658                head_gate: None,
7659            },
7660            post_norm: &lw.post_norm,
7661            attn_out_norm: None,
7662            ffn_out_norm: None,
7663            layer_scale: None,
7664            ffn: crate::gpu::GraphFfn::Dense {
7665                gate: gw(&d.gate_proj)?,
7666                up: gw(&d.up_proj)?,
7667                down: gw(&d.down_proj)?,
7668                act: d.act.graph_act()?,
7669            },
7670        };
7671        let nh = self.num_heads;
7672        let (nkv, hd, rd) = self.layer_geom(0);
7673        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7674        let mut logits = Vec::new();
7675        let ok = crate::gpu::forward_token_graph(
7676            &model,
7677            self.mtp_kv_id(),
7678            std::slice::from_ref(&layer),
7679            &[None],
7680            self.o1_epoch,
7681            &self.inv_freq,
7682            &mut x,
7683            nh,
7684            nkv,
7685            hd,
7686            self.attn_scale,
7687            rd,
7688            self.hidden_size,
7689            // An MTP draft can have an independently compacted FFN. The
7690            // graph must use the checked local tensor width, not the trunk
7691            // arch-wide intermediate size.
7692            d.gate_proj.rows(),
7693            position,
7694            self.kv_cache.max_seq_len,
7695            gemma,
7696            self.rms_eps as f32,
7697            Some((&lm_gw, lm_rows)),
7698            &m.final_norm,
7699            &mut logits,
7700            &[],
7701            1,
7702            None,
7703            None,
7704            None,
7705            Self::MTP_LAYER_BASE,
7706            true,
7707        );
7708        match ok {
7709            crate::gpu::TokenGraphOutcome::Completed => {}
7710            crate::gpu::TokenGraphOutcome::Declined => return None,
7711            crate::gpu::TokenGraphOutcome::Failed => {
7712                // The backend has already admitted persistent state.  Keep
7713                // this distinct from a capability refusal so the caller
7714                // cannot switch to the stale CPU MTP cache.
7715                self.clear_sequence_state();
7716                self.graph_failed
7717                    .store(true, std::sync::atomic::Ordering::Relaxed);
7718                self.cancel
7719                    .store(true, std::sync::atomic::Ordering::Relaxed);
7720                return None;
7721            }
7722        }
7723        // The graph may deliberately evaluate only a prefix of the draft
7724        // head.  Do not turn every omitted token into a zero-logit candidate:
7725        // that can win an all-negative shortlist and collapses acceptance.
7726        pad_mtp_draft_logits(&mut logits, self.vocab_size);
7727        Some((logits, x))
7728    }
7729
7730    /// The warm-ups of one speculative round on the device: every accepted
7731    /// (hidden, token) pair as ONE batched graph run over the MTP block
7732    /// (no head) — its kv_append lands the pairs in the block's mirror.
7733    /// `pairs` are consecutive positions from `first_pos`.  The tri-state
7734    /// result is intentional: a refusal before admission may use the
7735    /// per-row/CPU route, while a failure after admission must terminate the
7736    /// sequence rather than fall through to a stale CPU cache.
7737    #[cfg(feature = "gpu")]
7738    fn mtp_warm_graph(
7739        &mut self,
7740        m: &mut MtpModule,
7741        pairs: &[(&[f32], u32)],
7742        first_pos: usize,
7743    ) -> crate::gpu::BatchGraphOutcome {
7744        if pairs.is_empty() {
7745            return crate::gpu::BatchGraphOutcome::Completed;
7746        }
7747        if !self.mtp_block_graph_ok(m) {
7748            return crate::gpu::BatchGraphOutcome::Declined;
7749        }
7750        let hs = self.hidden_size;
7751        // Block inputs for every pair (eh_proj on the per-op path, one
7752        // matvec each — the plan's own prologue).
7753        let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7754        for (h, t) in pairs {
7755            hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7756        }
7757        let lw = &m.layer;
7758        let AttnKind::Full {
7759            wq,
7760            wk,
7761            wv,
7762            wo,
7763            q_norm,
7764            k_norm,
7765            output_gate,
7766            bias,
7767            ..
7768        } = &lw.attn
7769        else {
7770            return crate::gpu::BatchGraphOutcome::Declined;
7771        };
7772        let FfnKind::Dense(d) = &lw.ffn else {
7773            return crate::gpu::BatchGraphOutcome::Declined;
7774        };
7775        if !d.segs.is_empty() {
7776            return crate::gpu::BatchGraphOutcome::Declined; // tube layers run on the segmented path
7777        }
7778        // The graph has no arm for a projected output gate, and only the
7779        // activations `graph_act` names.
7780        let (
7781            AttnKind::Full {
7782                softplus_gate: None,
7783                ..
7784            },
7785            Some(gact),
7786        ) = (&lw.attn, d.act.graph_act())
7787        else {
7788            return crate::gpu::BatchGraphOutcome::Declined;
7789        };
7790        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7791            let (_, i, kind, rs) = t.graph_weight()?;
7792            Some(crate::gpu::GraphW {
7793                idx: i,
7794                kind,
7795                row_scale: rs,
7796                data: &[],
7797                prism: crate::gpu::GraphPrismOp::None,
7798                affine: false,
7799            })
7800        }
7801        let Some((model, _, _, _)) = wq.graph_weight() else {
7802            return crate::gpu::BatchGraphOutcome::Declined;
7803        };
7804        let model = model.clone();
7805        let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7806            gw(wq),
7807            gw(wk),
7808            gw(wv),
7809            gw(wo),
7810            gw(&d.gate_proj),
7811            gw(&d.up_proj),
7812            gw(&d.down_proj),
7813        ) else {
7814            return crate::gpu::BatchGraphOutcome::Declined;
7815        };
7816        let layer = crate::gpu::GraphLayer {
7817            mhc: None,
7818            input_norm: &lw.input_norm,
7819            attn: crate::gpu::GraphAttn::Full {
7820                wq: gwq,
7821                wk: gwk,
7822                wv: gwv,
7823                wo: gwo,
7824                q_norm: q_norm.as_deref(),
7825                k_norm: k_norm.as_deref(),
7826                late_qk_norm: self.qk_norm_after_rope,
7827                bias: bias
7828                    .as_ref()
7829                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7830                output_gate: *output_gate,
7831                cpu_k: m.kv.k_heads(),
7832                cpu_v: m.kv.v_heads(),
7833                cpu_base: m.kv.base(),
7834                geom: None,
7835                head_gate: None,
7836            },
7837            post_norm: &lw.post_norm,
7838            attn_out_norm: None,
7839            ffn_out_norm: None,
7840            layer_scale: None,
7841            ffn: crate::gpu::GraphFfn::Dense {
7842                gate: gg,
7843                up: gu,
7844                down: gd,
7845                act: gact,
7846            },
7847        };
7848        let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7849        let nh = self.num_heads;
7850        let (nkv, hd, rd) = self.layer_geom(0);
7851        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7852        crate::gpu::forward_batch_graph(
7853            &model,
7854            self.mtp_kv_id(),
7855            std::slice::from_ref(&layer),
7856            &self.inv_freq,
7857            &mut hiddens,
7858            nh,
7859            nkv,
7860            hd,
7861            rd,
7862            hs,
7863            // See `mtp_step_graph`: draft FFN geometry is local to this
7864            // block after independent MTP compaction.
7865            d.gate_proj.rows(),
7866            &positions,
7867            self.kv_cache.max_seq_len,
7868            gemma,
7869            self.rms_eps as f32,
7870            self.attn_scale,
7871            pairs.len(),
7872            &[],
7873            0,
7874            None,
7875            None,
7876        )
7877    }
7878
7879    /// Complete an MTP warm-up after the batched graph has refused.  A
7880    /// graphable block is retried one row at a time; once any device row has
7881    /// been admitted, a CPU fallback would observe a stale mirror, so every
7882    /// token-graph refusal is terminal.  If the block is not graphable and no
7883    /// mirror exists yet, warming on the CPU is safe and records the CPU mode
7884    /// for the rest of the generation.
7885    #[cfg(feature = "gpu")]
7886    fn mtp_warm_graph_fallback(
7887        &mut self,
7888        m: &mut MtpModule,
7889        pairs: &[(&[f32], u32)],
7890        first_pos: usize,
7891    ) -> bool {
7892        if pairs.is_empty() {
7893            return true;
7894        }
7895        let graphable = self.mtp_block_graph_ok(m);
7896        if !graphable {
7897            // A previously admitted mirror cannot be made coherent by
7898            // appending to the host cache.  The caller turns this into a
7899            // terminal generation error and clears both mirrors.
7900            if self.mtp_graph_mode == Some(true) {
7901                return false;
7902            }
7903            self.mtp_graph_mode = Some(false);
7904            for (j, (h, t)) in pairs.iter().enumerate() {
7905                self.mtp_warm(m, h, *t, first_pos + j);
7906            }
7907            return true;
7908        }
7909
7910        // The batch refusal is recoverable only through the same device
7911        // state.  Keep rows owned until each token graph has completed; a
7912        // None is treated as unsafe because the token-graph API deliberately
7913        // collapses its backend refusal/failure into that result.
7914        for (j, (h, t)) in pairs.iter().enumerate() {
7915            if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7916                return false;
7917            }
7918        }
7919        self.mtp_graph_mode = Some(true);
7920        true
7921    }
7922
7923    /// Warm a contiguous set of MTP pairs using the existing graph seam, with
7924    /// an all-or-nothing error contract for callers that already admitted the
7925    /// trunk batch.  The non-GPU build keeps the same pair accounting while
7926    /// using the established CPU warm path.
7927    #[cfg(feature = "gpu")]
7928    fn mtp_warm_prefill_pairs(
7929        &mut self,
7930        m: &mut MtpModule,
7931        pairs: &[(&[f32], u32)],
7932        first_pos: usize,
7933    ) -> Result<(), &'static str> {
7934        // Keep unsupported token-graph heads on the established CPU MTP
7935        // route before admitting any block mirror.  Once a device mirror is
7936        // active, the same condition is terminal because CPU rows cannot
7937        // repair its state.
7938        if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7939            if self.mtp_graph_mode == Some(true) {
7940                return Err("MTP token graph became unavailable after admission");
7941            }
7942            self.mtp_graph_mode = Some(false);
7943            for (j, (h, t)) in pairs.iter().enumerate() {
7944                self.mtp_warm(m, h, *t, first_pos + j);
7945            }
7946            return Ok(());
7947        }
7948        match self.mtp_warm_graph(m, pairs, first_pos) {
7949            crate::gpu::BatchGraphOutcome::Completed => {
7950                if !pairs.is_empty() {
7951                    self.mtp_graph_mode = Some(true);
7952                }
7953                Ok(())
7954            }
7955            crate::gpu::BatchGraphOutcome::Declined => {
7956                if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7957                    Ok(())
7958                } else {
7959                    Err("MTP warm-up fallback failed after device admission")
7960                }
7961            }
7962            crate::gpu::BatchGraphOutcome::Failed => {
7963                Err("MTP warm batch graph failed after admission")
7964            }
7965        }
7966    }
7967
7968    #[cfg(not(feature = "gpu"))]
7969    fn mtp_warm_prefill_pairs(
7970        &mut self,
7971        m: &mut MtpModule,
7972        pairs: &[(&[f32], u32)],
7973        first_pos: usize,
7974    ) -> Result<(), &'static str> {
7975        for (j, (h, t)) in pairs.iter().enumerate() {
7976            self.mtp_warm(m, h, *t, first_pos + j);
7977        }
7978        Ok(())
7979    }
7980
7981    /// The MTP block alone — advance its KV with a (hidden, token) pair the
7982    /// verify just proved, without paying the head. What keeps the draft's
7983    /// attention context warm between speculative rounds.
7984    fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7985        let e = self.embed_single(next_token);
7986        let mut cat = vec![0.0f32; 2 * self.hidden_size];
7987        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7988        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7989        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7990        let mut x = vec![0.0f32; self.hidden_size];
7991        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7992        inference::rms_norm_into(
7993            &x,
7994            &m.layer.input_norm,
7995            self.rms_eps,
7996            self.norm_style,
7997            &mut self.ws.n1,
7998        );
7999        let attn = match &m.layer.attn {
8000            AttnKind::Full {
8001                wq,
8002                wk,
8003                wv,
8004                wo,
8005                q_norm,
8006                k_norm,
8007                output_gate,
8008                softplus_gate,
8009                bias,
8010            } => {
8011                let mut cfg = self.attn_cfg(position);
8012                cfg.q_norm = q_norm.as_deref();
8013                cfg.k_norm = k_norm.as_deref();
8014                cfg.output_gate = *output_gate;
8015                cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
8016                cfg.bias = bias
8017                    .as_ref()
8018                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
8019                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
8020            }
8021            _ => return,
8022        };
8023        let _ = attn;
8024    }
8025
8026    /// Speculative decode ON the wgpu whole-token graph: draft k with the
8027    /// MTP head, verify all of them plus the tip in ONE batched graph
8028    /// submit whose tail folds the head, commit the accepted prefix and
8029    /// roll the GDN state back to the last real position. Greedy only —
8030    /// output equals the plain graph's token for token, the way the DSV4
8031    /// verify equals the walk.
8032    #[cfg(feature = "gpu")]
8033    #[allow(clippy::too_many_arguments)]
8034    fn graph_spec_step(
8035        &mut self,
8036        m: &mut MtpModule,
8037        hidden: &[f32],
8038        t_next: u32,
8039        next_pos: usize,
8040        drafted: &mut usize,
8041        accepted: &mut usize,
8042        // The committed stream (prompt + generated so far, `t_next`
8043        // included): the sampler chain's penalties read it, and the
8044        // sampling arm extends it with the drafts position by position.
8045        all_ids: &mut Vec<u32>,
8046        // Tokens left before `max_tokens`. A round commits up to k
8047        // accepted drafts, and those positions are already in the cache,
8048        // so the depth is capped here — trimming the output afterwards
8049        // would leave cache rows the committed stream does not have.
8050        room: usize,
8051    ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
8052        // 3 is the measured optimum on Qwen3.6-27B / RTX 5090 (medians
8053        // of three, greedy): 51.1 tok/s against a plain 49.4, where k=2
8054        // gives 46.1, k=4 50.0, k=5 47.4, k=6 45.2. Acceptance is 89-91%
8055        // throughout — what turns the curve over is the verify, which
8056        // costs ~7.4 ms per extra position, and the draft ~3 ms a step.
8057        // 4 since the draft moved onto the graph (Qwen3.8-27B / 5090:
8058        // k=3 51.2, k=4 51.8 with the per-op draft; the graph draft
8059        // halves the draft cost, so the extra draft is cheaper still).
8060        // 5 with the int8 verify (the default: measured 76.5 against
8061        // k=4's 72-74 and k=6's 74 on the 5090), 4 with the f32 one.
8062        #[cfg(target_os = "macos")]
8063        let metal_native = crate::gpu::q1_force();
8064        #[cfg(not(target_os = "macos"))]
8065        let metal_native = false;
8066        #[cfg(feature = "gpu")]
8067        let k_default = if metal_native {
8068            // the Metal verify's GEMM tile is 8 rows wide and flat in b:
8069            // seven drafts + the tip fill it for free
8070            7
8071        } else if crate::gpu_wgpu::verify_i8_on() {
8072            5
8073        } else {
8074            4
8075        };
8076        #[cfg(not(feature = "gpu"))]
8077        let k_default = 4;
8078        let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
8079            .ok()
8080            .and_then(|v| v.parse().ok())
8081            .filter(|&v| (1..=8).contains(&v));
8082        // Adaptive depth: start below the card's flat-verify optimum and
8083        // let the accepted fraction move it — predictable text climbs to
8084        // the old default within a few rounds, prose settles at 2-3 where
8085        // the shorter verify pays.
8086        let (k_start, k_max) = if metal_native {
8087            (7, 7)
8088        } else {
8089            (3, k_default.max(5))
8090        };
8091        let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
8092        let k_spec = k_full.min(room).max(1);
8093        // a tail round cut short by `room` says nothing about the text:
8094        // it must not move the adaptive depth the next request starts at
8095        let k_capped = k_spec < k_full;
8096        if next_pos == 0 {
8097            return None;
8098        }
8099        let t_round = std::time::Instant::now();
8100        // Submissions per phase — and they say where the round's money is.
8101        // Qwen3.6-27B on an RTX 5090, k=3:
8102        //
8103        //   draft   9.3 ms / 12 submissions   (four per MTP step)
8104        //   verify 52.8 ms /  1               (the batched graph)
8105        //   commit  5.4 ms /  6               (two per warm)
8106        //
8107        // The verify is already one submit. The draft's own work is 834 MB
8108        // a step — 0.8 ms at this card's measured 1056 GB/s — against 3.1
8109        // ms measured, so ~0.58 ms of every step is round trip, not
8110        // arithmetic, and the same holds for the warms. Eighteen round
8111        // trips a round at roughly half a millisecond each is ~11 ms of a
8112        // 68 ms round: fusing the MTP block into ONE submit the way the
8113        // trunk already is projects to ~64 tok/s against today's 50.9.
8114        // That is the largest measured item left on this path.
8115        let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
8116        let sub0 = subs();
8117        // Greedy without penalties verifies by argmax equality (bit-exact
8118        // against the plain path). Anything else is speculative SAMPLING:
8119        // each draft is a DRAW from the MTP head's post-chain distribution
8120        // q_j, kept for the accept test; the verify's rows give p_j.
8121        let cfg = self.sampler_config.clone();
8122        let penalized = !(cfg.repetition_penalty == 1.0
8123            && cfg.presence_penalty == 0.0
8124            && cfg.suppress_tokens.is_empty());
8125        // Three verify regimes: plain greedy (argmax of the raw rows),
8126        // greedy WITH penalties (argmax of the penalized rows — a single
8127        // pass each, no distributions), and sampling (draw / accept /
8128        // correct on post-chain distributions).
8129        let greedy_pen = cfg.temperature < 1e-6 && penalized;
8130        let sampling = cfg.temperature >= 1e-6;
8131        // Sampling with a top-k goes through the SPARSE chain: the dense
8132        // one builds nine 248k-float distributions a round (four drafts,
8133        // five verify rows) and measured 19-22 tok/s against a plain 40 —
8134        // the host, not the card. Sparse, the same nine cost tens of
8135        // microseconds each.
8136        let sparse = sampling && sampler::sparse_ok(&cfg);
8137        let base_len = all_ids.len();
8138        if sampling && !sparse && self.spec_q.len() < k_spec {
8139            self.spec_q.resize_with(k_spec, Vec::new);
8140        }
8141        if sparse && self.spec_qs.len() < k_spec {
8142            self.spec_qs.resize_with(k_spec, Vec::new);
8143        }
8144        // Draft the chain: first from the trunk's tip hidden, then the head
8145        // iterating on itself. Rows land in the MTP KV; the chain rows past
8146        // the first are speculation over speculative state and roll back
8147        // below, replaced by verified pairs.
8148        let mut drafts = Vec::with_capacity(k_spec);
8149        let mut hx = hidden.to_vec();
8150        // CMF_SPEC_DBG=1: draft 0 through BOTH MTP arms (graph and per-op)
8151        // from the same inputs — are the arms the difference, or the inputs?
8152        let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
8153        spec_stamp("pro");
8154        // Plain greedy on native Metal: the whole chain as one command
8155        // buffer (device argmax + embedding gather between the steps).
8156        // A decline before commit hands the round to the per-step loop
8157        // below; a failure after commit is terminal, like any graph
8158        // failure after admission.
8159        #[cfg(target_os = "macos")]
8160        if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
8161            match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
8162                Ok(ids) => {
8163                    self.mtp_graph_mode = Some(true);
8164                    drafts = ids;
8165                }
8166                Err(true) => {
8167                    tracing::error!("mtp Metal draft chain failed after commit");
8168                    self.clear_sequence_state();
8169                    self.graph_failed
8170                        .store(true, std::sync::atomic::Ordering::Relaxed);
8171                    self.cancel
8172                        .store(true, std::sync::atomic::Ordering::Relaxed);
8173                    return None;
8174                }
8175                Err(false) => {}
8176            }
8177        }
8178        for j in drafts.len()..k_spec {
8179            let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
8180            let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
8181            if spec_dbg {
8182                let saved = self.mtp_graph_mode;
8183                self.mtp_graph_mode = Some(false);
8184                let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
8185                self.mtp_graph_mode = saved;
8186                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
8187                    return None;
8188                }
8189                m.kv.truncate_last(1);
8190                dbg_ref = Some(r);
8191            }
8192            let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
8193            if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
8194                return None;
8195            }
8196            if let Some((lg_cpu, h_cpu)) = dbg_ref {
8197                let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
8198                let dl = lg
8199                    .iter()
8200                    .zip(&lg_cpu)
8201                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
8202                let dh = hj
8203                    .iter()
8204                    .zip(&h_cpu)
8205                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
8206                eprintln!(
8207                    "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 {}",
8208                    next_pos - 1 + j,
8209                    sampler::argmax(&lg_cpu),
8210                    sampler::argmax(&lg),
8211                    n(&h_cpu),
8212                    n(&hj),
8213                    m.kv.seq_len
8214                );
8215            }
8216            let dj = if sparse {
8217                let mut q = std::mem::take(&mut self.spec_qs[j]);
8218                let ok = sampler::sparse_distribution_into(
8219                    &lg,
8220                    &cfg,
8221                    all_ids,
8222                    &mut self.sampler_scratch,
8223                    self.pool.as_deref(),
8224                    &mut q,
8225                );
8226                let d = if ok {
8227                    sampler::draw_sparse(&q, &mut self.rng)
8228                } else {
8229                    // everything filtered: the dense chain's greedy fallback
8230                    let t = sampler::argmax(&lg);
8231                    q.clear();
8232                    q.push((t, 1.0));
8233                    t
8234                };
8235                self.spec_qs[j] = q;
8236                all_ids.push(d);
8237                d
8238            } else if sampling {
8239                let mut q = std::mem::take(&mut self.spec_q[j]);
8240                sampler::distribution_into(
8241                    &lg,
8242                    &cfg,
8243                    all_ids,
8244                    &mut self.sampler_scratch,
8245                    self.pool.as_deref(),
8246                    &mut q,
8247                );
8248                let d = sampler::draw(&q, &mut self.rng);
8249                self.spec_q[j] = q;
8250                all_ids.push(d); // the next draft's penalties see this one
8251                d
8252            } else if greedy_pen {
8253                let d = sampler::argmax_penalized(
8254                    &lg,
8255                    &cfg,
8256                    all_ids,
8257                    &mut self.sampler_scratch,
8258                    self.pool.as_deref(),
8259                );
8260                all_ids.push(d);
8261                d
8262            } else {
8263                sampler::argmax(&lg)
8264            };
8265            attention::recycle_buf(&mut lg);
8266            drafts.push(dj);
8267            hx = hj;
8268            spec_stamp("d.pick");
8269        }
8270        all_ids.truncate(base_len);
8271        *drafted += k_spec;
8272        let t_draft = t_round.elapsed();
8273        let sub_draft = subs();
8274        // Verify batch: [t_next, d1 .. d_{k-1}] at next_pos.. — every row's
8275        // logits come back from the graph's own head.
8276        let b = k_spec + 1;
8277        let mut hiddens = vec![0.0f32; b * self.hidden_size];
8278        for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
8279            let e = self.embed_single(t);
8280            hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
8281        }
8282        let positions: Vec<usize> = (next_pos..next_pos + b).collect();
8283        spec_stamp("v.emb");
8284        let (lm_gw, lm_rows) = {
8285            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
8286            (
8287                crate::gpu::GraphW {
8288                    idx: i,
8289                    kind,
8290                    row_scale: rs,
8291                    data: &[],
8292                    prism: crate::gpu::GraphPrismOp::None,
8293                    affine: false,
8294                },
8295                self.weights.lm_head.rows(),
8296            )
8297        };
8298        let mut logits = Vec::new();
8299        let final_norm = self.weights.final_norm.clone();
8300        // Plain greedy on Metal: the b argmaxes come from the device
8301        // (`argmax_rows` after the head) and the 7.9 MB logits plane is
8302        // never read back — the round's decision needs only the ids, and
8303        // the loop top takes the last verified id as `spec_forced`, which
8304        // is exactly what its argmax of the row would give. The full rows
8305        // stay for anything that reads them: sampling, penalties,
8306        // confidence, the verify oracle, the logit dump.
8307        // `CMF_METAL_DEV_ARGMAX=0` keeps the host path.
8308        #[cfg(target_os = "macos")]
8309        let greedy_dev = metal_native
8310            && !sampling
8311            && !greedy_pen
8312            && !self.confidence_on
8313            && self.final_softcap.is_none()
8314            // The host acceptance argmax scans the WHOLE head row
8315            // (`lm_rows`), the sampler's own row only `vocab_size`: they
8316            // coincide exactly when the head has no padding rows, and
8317            // only then is the device argmax (which scores `vocab_size`)
8318            // bit-identical to both.
8319            && self.vocab_size == lm_rows
8320            && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
8321            && std::env::var_os("CMF_LOGIT_DUMP").is_none()
8322            && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
8323        #[cfg(not(target_os = "macos"))]
8324        let greedy_dev = false;
8325        let mut dev_ids: Vec<u32> = Vec::new();
8326        #[cfg(target_os = "macos")]
8327        let verify_outcome = if metal_native {
8328            let lm = self.weights.lm_head.q1_parts()?;
8329            let n_score = self.vocab_size.min(lm_rows);
8330            self.try_batch_graph_metal(
8331                &mut hiddens,
8332                &positions,
8333                b,
8334                Some((lm, &final_norm, &mut logits)),
8335                if greedy_dev {
8336                    Some((n_score, &mut dev_ids))
8337                } else {
8338                    None
8339                },
8340            )
8341        } else {
8342            self.try_batch_graph_wgpu(
8343                &mut hiddens,
8344                &positions,
8345                b,
8346                Some(crate::gpu::SpecTail {
8347                    lm: lm_gw,
8348                    lm_rows,
8349                    final_norm: &final_norm,
8350                    logits_out: &mut logits,
8351                    pick: None,
8352                }),
8353            )
8354        };
8355        #[cfg(not(target_os = "macos"))]
8356        let verify_outcome = self.try_batch_graph_wgpu(
8357            &mut hiddens,
8358            &positions,
8359            b,
8360            Some(crate::gpu::SpecTail {
8361                lm: lm_gw,
8362                lm_rows,
8363                final_norm: &final_norm,
8364                logits_out: &mut logits,
8365                pick: None,
8366            }),
8367        );
8368        match verify_outcome {
8369            crate::gpu::BatchGraphOutcome::Completed => {}
8370            crate::gpu::BatchGraphOutcome::Declined => {
8371                // The verifier refused before admission.  Its draft MTP
8372                // rows are still device-resident, so rewind the separate
8373                // mirror before the caller takes the exact one-token path.
8374                m.kv.truncate_last(k_spec);
8375                if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
8376                    self.clear_sequence_state();
8377                    self.graph_failed
8378                        .store(true, std::sync::atomic::Ordering::Relaxed);
8379                    self.cancel
8380                        .store(true, std::sync::atomic::Ordering::Relaxed);
8381                    tracing::error!("MTP graph mirror rewind failed after verify decline");
8382                }
8383                return None;
8384            }
8385            crate::gpu::BatchGraphOutcome::Failed => {
8386                // A failed batch may have advanced trunk/GDN state.  Clear
8387                // both mirrors and preserve the terminal outcome rather than
8388                // falling through to stale CPU state.
8389                self.clear_sequence_state();
8390                self.graph_failed
8391                    .store(true, std::sync::atomic::Ordering::Relaxed);
8392                self.cancel
8393                    .store(true, std::sync::atomic::Ordering::Relaxed);
8394                tracing::error!("MTP verify batch graph failed after admission");
8395                return None;
8396            }
8397        }
8398        // `CMF_METAL_VERIFY_CHECK=1`: run the same b tokens through the
8399        // plain per-token path and compare each row's argmax + logits with
8400        // the verify's — the bring-up oracle for the batched graph. The
8401        // plain forwards mutate the CPU state; it is snapshotted and put
8402        // back, and the K/V mirrors re-pointed, before the round goes on.
8403        #[cfg(target_os = "macos")]
8404        if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
8405            let snap: Vec<Vec<f32>> = self
8406                .kv_cache
8407                .layers
8408                .iter()
8409                .map(|l| l.linear_state.clone())
8410                .collect();
8411            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8412            let toks: Vec<u32> = std::iter::once(t_next)
8413                .chain(drafts.iter().copied())
8414                .collect();
8415            let want_save = self.graph_want_logits;
8416            self.graph_want_logits = false;
8417            for (i, &t) in toks.iter().enumerate() {
8418                let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8419                let _ = self.graph_logits.take();
8420                // CMF_SPEC_PLAIN_HIDDEN=1: the next round drafts from the
8421                // plain path's hidden instead of the verify's (an experiment
8422                // on the chain's sensitivity to the half-GEMM noise)
8423                if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
8424                    hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
8425                }
8426                let ref_lg = self.logits_from_hidden(&hi);
8427                let row = &logits[i * lm_rows..(i + 1) * lm_rows];
8428                let ra = sampler::argmax(&ref_lg);
8429                let va = sampler::argmax(row);
8430                let mut md = 0f32;
8431                let mut rms = 0f64;
8432                for j in 0..lm_rows.min(ref_lg.len()) {
8433                    let d = (ref_lg[j] - row[j]).abs();
8434                    md = md.max(d);
8435                    rms += (d as f64) * (d as f64);
8436                }
8437                let mut hd = 0f32;
8438                for j in 0..self.hidden_size {
8439                    hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
8440                }
8441                eprintln!(
8442                    "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
8443                    next_pos + i,
8444                    if ra == va { "OK" } else { "MISMATCH" },
8445                    (rms / lm_rows as f64).sqrt()
8446                );
8447            }
8448            self.graph_want_logits = want_save;
8449            // restore IN PLACE: the pending verify graph wraps these very
8450            // allocations (zero-copy) — replacing the Vec would strand it
8451            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8452                if l.linear_state.len() == st.len() {
8453                    l.linear_state.copy_from_slice(&st);
8454                } else {
8455                    l.linear_state = st;
8456                }
8457            }
8458            for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
8459                let extra = l.seq_len.saturating_sub(n0);
8460                if extra > 0 {
8461                    l.truncate_last(extra);
8462                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
8463                }
8464            }
8465        }
8466        let t_verify = t_round.elapsed();
8467        let sub_verify = subs();
8468        // Acceptance. Greedy: row i's argmax is the trunk's token after
8469        // input i. Sampling: accept draft i with min(1, p_i/q_i), and on
8470        // the first rejection draw the correction from max(0, p_i − q_i)
8471        // — that token is committed by the loop top as-is (spec_forced).
8472        let mut a = 0usize;
8473        let mut forced: Option<u32> = None;
8474        let ids: Vec<u32> = if sparse {
8475            let mut p = std::mem::take(&mut self.spec_ps);
8476            let mut res = std::mem::take(&mut self.spec_ress);
8477            while a < k_spec {
8478                let ok = sampler::sparse_distribution_into(
8479                    &logits[a * lm_rows..(a + 1) * lm_rows],
8480                    &cfg,
8481                    all_ids,
8482                    &mut self.sampler_scratch,
8483                    self.pool.as_deref(),
8484                    &mut p,
8485                );
8486                if !ok {
8487                    let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
8488                    p.clear();
8489                    p.push((t, 1.0));
8490                }
8491                match sampler::spec_accept_or_correct_sparse(
8492                    &p,
8493                    &self.spec_qs[a],
8494                    drafts[a],
8495                    &mut self.rng,
8496                    &mut res,
8497                ) {
8498                    None => {
8499                        all_ids.push(drafts[a]);
8500                        a += 1;
8501                    }
8502                    Some(c) => {
8503                        forced = Some(c);
8504                        break;
8505                    }
8506                }
8507            }
8508            all_ids.truncate(base_len);
8509            self.spec_ps = p;
8510            self.spec_ress = res;
8511            drafts.clone()
8512        } else if sampling {
8513            let mut p = std::mem::take(&mut self.spec_p);
8514            let mut res = std::mem::take(&mut self.spec_res);
8515            while a < k_spec {
8516                sampler::distribution_into(
8517                    &logits[a * lm_rows..(a + 1) * lm_rows],
8518                    &cfg,
8519                    all_ids,
8520                    &mut self.sampler_scratch,
8521                    self.pool.as_deref(),
8522                    &mut p,
8523                );
8524                match sampler::spec_accept_or_correct(
8525                    &p,
8526                    &self.spec_q[a],
8527                    drafts[a],
8528                    &mut self.rng,
8529                    &mut res,
8530                    self.pool.as_deref(),
8531                ) {
8532                    None => {
8533                        all_ids.push(drafts[a]);
8534                        a += 1;
8535                    }
8536                    Some(c) => {
8537                        forced = Some(c);
8538                        break;
8539                    }
8540                }
8541            }
8542            all_ids.truncate(base_len);
8543            self.spec_p = p;
8544            self.spec_res = res;
8545            // the accepted drafts ARE the verified tokens after inputs 0..a
8546            drafts.clone()
8547        } else if greedy_pen {
8548            // Row i's penalized argmax, penalties over the stream that
8549            // includes the accepted drafts before it — the plain loop's
8550            // exact arithmetic, one pass per row, no working copy.
8551            let mut ids: Vec<u32> = Vec::with_capacity(b);
8552            for i in 0..b {
8553                let t = sampler::argmax_penalized(
8554                    &logits[i * lm_rows..(i + 1) * lm_rows],
8555                    &cfg,
8556                    all_ids,
8557                    &mut self.sampler_scratch,
8558                    self.pool.as_deref(),
8559                );
8560                ids.push(t);
8561                if i < k_spec && t == drafts[i] {
8562                    all_ids.push(t);
8563                } else {
8564                    break;
8565                }
8566            }
8567            all_ids.truncate(base_len);
8568            while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8569                a += 1;
8570            }
8571            // rows past the first mismatch were never scored; the loop
8572            // top re-samples the last verified row itself.
8573            ids
8574        } else if greedy_dev && dev_ids.len() == b {
8575            let ids = std::mem::take(&mut dev_ids);
8576            while a < k_spec && ids[a] == drafts[a] {
8577                a += 1;
8578            }
8579            ids
8580        } else {
8581            if logits.len() < b * lm_rows {
8582                // the device argmax was asked for and came back short:
8583                // no rows to fall back on — terminal like a failed batch
8584                self.clear_sequence_state();
8585                self.graph_failed
8586                    .store(true, std::sync::atomic::Ordering::Relaxed);
8587                self.cancel
8588                    .store(true, std::sync::atomic::Ordering::Relaxed);
8589                tracing::error!("Metal verify returned neither logits nor argmax ids");
8590                return None;
8591            }
8592            let ids: Vec<u32> = (0..b)
8593                .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8594                .collect();
8595            while a < k_spec && ids[a] == drafts[a] {
8596                a += 1;
8597            }
8598            ids
8599        };
8600        spec_stamp("acc");
8601        if spec_dbg {
8602            eprintln!(
8603                "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8604                drafts, ids
8605            );
8606        }
8607        // CMF_METAL_VERIFY_CHECK=2: the commit oracle — plain-forward the
8608        // a+1 accepted tokens from a snapshot, then diff the replayed GDN
8609        // states and the appended K/V rows against that.
8610        #[cfg(target_os = "macos")]
8611        let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8612            && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8613        {
8614            let snap: Vec<Vec<f32>> = self
8615                .kv_cache
8616                .layers
8617                .iter()
8618                .map(|l| l.linear_state.clone())
8619                .collect();
8620            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8621            let toks: Vec<u32> = std::iter::once(t_next)
8622                .chain(drafts.iter().copied())
8623                .collect();
8624            let want_save = self.graph_want_logits;
8625            self.graph_want_logits = false;
8626            for (i, &t) in toks.iter().take(a + 1).enumerate() {
8627                let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8628                let _ = self.graph_logits.take();
8629            }
8630            self.graph_want_logits = want_save;
8631            let plain_states: Vec<Vec<f32>> = self
8632                .kv_cache
8633                .layers
8634                .iter()
8635                .map(|l| l.linear_state.clone())
8636                .collect();
8637            let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8638            let mut rows = Vec::new();
8639            for (li, (l, n0)) in self
8640                .kv_cache
8641                .layers
8642                .iter_mut()
8643                .zip(attn_lens.iter())
8644                .enumerate()
8645            {
8646                let extra = l.seq_len.saturating_sub(*n0);
8647                if extra > 0 {
8648                    let mut kk = Vec::new();
8649                    let mut vv = Vec::new();
8650                    for g in 0..nkv {
8651                        kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8652                        vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8653                    }
8654                    rows.push((li, kk, vv));
8655                    l.truncate_last(extra);
8656                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8657                }
8658            }
8659            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8660                if l.linear_state.len() == st.len() {
8661                    l.linear_state.copy_from_slice(&st);
8662                } else {
8663                    l.linear_state = st;
8664                }
8665            }
8666            Some((plain_states, rows))
8667        } else {
8668            None
8669        };
8670        let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8671        // Metal: the MTP cache cut and the round's warm-up SUBMIT come
8672        // BEFORE the trunk commit, so the warm-up's command buffer is
8673        // queued ahead of the GDN replay (second queue) and its wait
8674        // below no longer sits behind the replay — measured: the warm-up's
8675        // wait grew with the accepted count exactly like the replay does
8676        // (8 ms at a=1, 17 ms at a=3, 25 ms at a=5 for ~2 ms of its own
8677        // work). The replay now overlaps the warm-up's readback, the
8678        // round's return and the next draft chain.
8679        #[cfg(target_os = "macos")]
8680        let mut warm_pending: Option<MetalWarmPending> = None;
8681        #[cfg(target_os = "macos")]
8682        if metal_native {
8683            m.kv.truncate_last(k_spec.saturating_sub(1));
8684            if self.mtp_graph_mode == Some(true) {
8685                // the mirror rows below the cut are the CPU rows: re-point,
8686                // no re-upload
8687                crate::gpu_metal::kv_mirror_set_stored(
8688                    self.mtp_kv_id(),
8689                    Self::MTP_LAYER_BASE,
8690                    m.kv.seq_len,
8691                );
8692                if !warm_off && a > 0 {
8693                    let pairs: Vec<(&[f32], u32)> = (0..a)
8694                        .map(|j| {
8695                            (
8696                                &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8697                                ids[j],
8698                            )
8699                        })
8700                        .collect();
8701                    warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8702                }
8703            }
8704            spec_stamp("c.wsub");
8705        }
8706        // a fully-accepted round needs no restore: every input was real.
8707        #[cfg(target_os = "macos")]
8708        if metal_native {
8709            // the Metal verify never wrote its states: the commit replays the
8710            // accepted prefix into the CPU owners and appends the K/V rows
8711            if !self.metal_verify_commit(a) {
8712                self.clear_sequence_state();
8713                self.graph_failed
8714                    .store(true, std::sync::atomic::Ordering::Relaxed);
8715                self.cancel
8716                    .store(true, std::sync::atomic::Ordering::Relaxed);
8717                tracing::error!("Metal verify state/KV handoff failed after admission");
8718                return None;
8719            }
8720            if let Some((plain_states, rows)) = commit_ref {
8721                crate::gpu_metal::queue_fence();
8722                // the commit's replay runs on the second queue: collect it
8723                // before the oracle reads the CPU owners it writes into
8724                let _ = crate::gpu_metal::wait_replay();
8725                let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8726                let mut worst_s = 0f32;
8727                let mut worst_li = 0usize;
8728                for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8729                    if l.linear_state.len() != ps.len() || ps.is_empty() {
8730                        continue;
8731                    }
8732                    let d = l
8733                        .linear_state
8734                        .iter()
8735                        .zip(ps)
8736                        .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8737                    let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8738                    let rel = d / n.max(1e-6);
8739                    if rel > worst_s {
8740                        worst_s = rel;
8741                        worst_li = li;
8742                    }
8743                }
8744                let mut worst_k = 0f32;
8745                for (li, kk, vv) in &rows {
8746                    let l = &self.kv_cache.layers[*li];
8747                    let n0 = l.seq_len - (kk.len() / (nkv * hd));
8748                    let mut ck = Vec::new();
8749                    let mut cv = Vec::new();
8750                    for g in 0..nkv {
8751                        ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8752                        cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8753                    }
8754                    if ck.len() == kk.len() {
8755                        let dk = ck
8756                            .iter()
8757                            .zip(kk)
8758                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8759                        let dv = cv
8760                            .iter()
8761                            .zip(vv)
8762                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8763                        worst_k = worst_k.max(dk).max(dv);
8764                    } else {
8765                        eprintln!(
8766                            "commit-check L{li}: kv row count mismatch {} vs {}",
8767                            ck.len(),
8768                            kk.len()
8769                        );
8770                    }
8771                }
8772                eprintln!(
8773                    "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}"
8774                );
8775            }
8776        }
8777        if !metal_native && a + 1 < b {
8778            let expected_gdn_layers = self.graph_gdn_layer_count();
8779            if expected_gdn_layers > 0
8780                && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8781            {
8782                self.clear_sequence_state();
8783                self.graph_failed
8784                    .store(true, std::sync::atomic::Ordering::Relaxed);
8785                self.cancel
8786                    .store(true, std::sync::atomic::Ordering::Relaxed);
8787                tracing::error!("GDN speculative restore failed after verify");
8788                return None;
8789            }
8790        }
8791        if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8792            // The verify graph committed the full batch, but one of its
8793            // persistent Full-attention mirrors could not be re-pointed to
8794            // the accepted prefix.  Treat that as terminal state failure;
8795            // an exact CPU fallback would otherwise consume stale GDN/KV.
8796            self.clear_sequence_state();
8797            self.graph_failed
8798                .store(true, std::sync::atomic::Ordering::Relaxed);
8799            self.cancel
8800                .store(true, std::sync::atomic::Ordering::Relaxed);
8801            tracing::error!("trunk graph KV rewind failed after speculative verify");
8802            return None;
8803        }
8804        *accepted += a;
8805        // MTP cache: keep the first draft row (its inputs were real), drop
8806        // the chain's, then append the verified pairs the round produced.
8807        // Each of those is a whole MTP block on the per-op path and they
8808        // cost 5.8 ms of a 69 ms round at k=3 — a third of what the
8809        // round's own draft costs. PRICED, and they earn it: skipping
8810        // them (`CMF_SPEC_WARM=0`) drops acceptance from 89% to 81% at
8811        // k=3 and 85% to 74% at k=4, and the tok/s goes nowhere at k=3
8812        // (50.3 against 50.5) and backwards at k=4 (48.1 against 50.1).
8813        // The knob stays so the next person can re-price it after the
8814        // warms are batched instead of assuming either way.
8815        if !metal_native {
8816            // (Metal cut its MTP cache before the trunk commit, above)
8817            m.kv.truncate_last(k_spec.saturating_sub(1));
8818        }
8819        spec_stamp("c.trunc");
8820        if !metal_native
8821            && self.mtp_graph_mode == Some(true)
8822            && !self.rewind_mtp_graph_mirror(next_pos)
8823        {
8824            // The graph draft was admitted, so inability to move its cursor
8825            // back to the real anchor is a state failure, not a capability
8826            // refusal.  Do not warm or continue with a stale mirror.
8827            self.clear_sequence_state();
8828            self.graph_failed
8829                .store(true, std::sync::atomic::Ordering::Relaxed);
8830            self.cancel
8831                .store(true, std::sync::atomic::Ordering::Relaxed);
8832            tracing::error!("MTP graph mirror rewind failed after verify commit");
8833            return None;
8834        }
8835        if !warm_off && a > 0 {
8836            // Graph arm: all accepted pairs in ONE batched run over the
8837            // MTP block; the token graph one by one if the batch declines.
8838            let mut warmed = false;
8839            #[cfg(target_os = "macos")]
8840            if metal_native && self.mtp_graph_mode == Some(true) {
8841                // the batched warm-up was submitted before the trunk
8842                // commit: collect it here; one by one on the token graph
8843                // if it declined (or failed)
8844                warmed = match warm_pending.take() {
8845                    Some(p) => self.mtp_warm_batch_finish(m, p),
8846                    None => false,
8847                };
8848                if !warmed {
8849                    warmed = true;
8850                    for j in 0..a {
8851                        let row =
8852                            hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8853                        if self
8854                            .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8855                            .is_none()
8856                        {
8857                            warmed = false;
8858                            break;
8859                        }
8860                    }
8861                }
8862            }
8863            if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8864                let rows: Vec<Vec<f32>> = (0..a)
8865                    .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8866                    .collect();
8867                let pairs: Vec<(&[f32], u32)> = rows
8868                    .iter()
8869                    .zip(ids.iter())
8870                    .map(|(r, &t)| (r.as_slice(), t))
8871                    .collect();
8872                match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8873                    Ok(()) => warmed = true,
8874                    Err(err) => {
8875                        // A warm-up failure after graph admission cannot
8876                        // fall back to `mtp_warm`: the detached CPU cache is
8877                        // not authoritative for the device mirror.  Mark it
8878                        // terminal so the generation caller clears state and
8879                        // returns instead of drafting from stale attention.
8880                        tracing::error!("{err}");
8881                        self.clear_sequence_state();
8882                        self.graph_failed
8883                            .store(true, std::sync::atomic::Ordering::Relaxed);
8884                        self.cancel
8885                            .store(true, std::sync::atomic::Ordering::Relaxed);
8886                        return None;
8887                    }
8888                }
8889            }
8890            if !warmed {
8891                for j in 0..a {
8892                    let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8893                    let row = row.to_vec();
8894                    self.mtp_warm(m, &row, ids[j], next_pos + j);
8895                }
8896            }
8897        }
8898        // The sampler's contract: logits of the LAST verified position —
8899        // unless a rejected draft already drew the correction, in which
8900        // case the loop top commits that token and samples nothing.
8901        spec_stamp("c.warm");
8902        if let Some(c) = forced {
8903            self.spec_forced = Some(c);
8904            self.graph_logits = None;
8905        } else if greedy_dev && logits.is_empty() {
8906            // the row's argmax IS the token the loop top would pick from
8907            // it (plain greedy, no penalties): commit it as forced
8908            self.spec_forced = Some(ids[a]);
8909            self.graph_logits = None;
8910        } else {
8911            let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8912            row.resize(self.vocab_size, 0.0);
8913            if let Some(c) = self.final_softcap {
8914                for l in row.iter_mut() {
8915                    *l = c * (*l / c).tanh();
8916                }
8917            }
8918            self.graph_logits = Some(row);
8919        }
8920        let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8921        spec_stamp("c.row");
8922        // Three phases, not two. The round's wall clock was 4 ms longer
8923        // than draft+verify and the difference had nowhere to be seen:
8924        // the accepted prefix re-runs the MTP block once per token to
8925        // keep the draft head's attention cache warm, and the GDN state
8926        // rolls back on any rejection. Both live here, after the verify.
8927        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8928            let end = subs();
8929            eprintln!(
8930                "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8931                 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8932                t_draft.as_secs_f64() * 1e3,
8933                sub_draft - sub0,
8934                (t_verify - t_draft).as_secs_f64() * 1e3,
8935                sub_verify - sub_draft,
8936                (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8937                end - sub_verify,
8938                self.draft_full_streak,
8939            );
8940        }
8941        // Native Metal's verify tile is flat in b (eight rows for the price
8942        // of one), so a shorter round only forfeits tokens — measured on
8943        // the M4: an essay round at k=2 still verified in 260 ms. The
8944        // adaptation is for cards whose verify grows with the rows.
8945        if k_env.is_none() && !metal_native && !k_capped {
8946            // Slow average and a wide band: a fast one oscillated 2↔3 on
8947            // an essay every other round (measured), which forfeits the
8948            // draft it just paid for.
8949            let f = a as f32 / k_spec.max(1) as f32;
8950            self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8951            let mut k_next = k_spec;
8952            if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8953                k_next = k_spec + 1;
8954            } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8955                k_next = k_spec - 1;
8956            }
8957            if k_next != k_spec {
8958                self.spec_acc_ewma = 0.6;
8959                if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8960                    eprintln!("spec-k: {k_spec} → {k_next}");
8961                }
8962            }
8963            self.spec_k_adapt = Some(k_next);
8964        }
8965        spec_stamp("end");
8966        Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8967    }
8968
8969    /// Micro-benchmark: two single-position forwards vs one fused pair
8970    /// from the current cache state (KV rewound after each probe).
8971    /// Returns (two_singles_ms, fused_pair_ms) per probe, or the (0, 0)
8972    /// sentinel when this model has no pair path to measure — the same
8973    /// answer the o1 arm gives, and the bench prints it the same way.
8974    /// (An architecture that loads its own layers leaves `weights.layers`
8975    /// empty; walking it here was an index panic, found by `bench` on
8976    /// deepseek_v4.)
8977    pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8978        if !self.pair_supported() {
8979            return (0.0, 0.0);
8980        }
8981        // A GDN model that needs an automatic device prefix never uses this
8982        // fused-pair path in normal generation: its recurrence is serial and
8983        // the speculative pair is disabled by default.  More importantly,
8984        // this diagnostic suppresses the token graph, so measuring it would
8985        // stream the non-resident tail through per-op Vulkan just to print a
8986        // non-production number.  Leave inference untouched and report the
8987        // established unavailable sentinel instead.
8988        if self.gdn_cfg.is_some()
8989            && self.graph_prefill_preferred()
8990            && self.automatic_gpu_prefix().is_some()
8991        {
8992            tracing::info!(
8993                "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8994            );
8995            return (0.0, 0.0);
8996        }
8997        // This is a host-side pair micro-benchmark. It truncates the host KV
8998        // after every probe, so letting the whole-token graph participate
8999        // would leave its device GDN/KV mirror ahead of the next probe and
9000        // poison the process-wide graph verdict before the real generation
9001        // benchmark starts. Keep the existing per-op/GPU arithmetic while
9002        // suppressing only the stateful token graph for this measurement.
9003        let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
9004        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
9005        let emb1 = self.embed_single(1);
9006        let emb2 = self.embed_single(2);
9007        let pos = self.kv_cache.seq_len();
9008
9009        let t0 = std::time::Instant::now();
9010        for _ in 0..iters {
9011            let _ = self.forward_layers(&emb1, pos, None);
9012            let _ = self.forward_layers(&emb2, pos + 1, None);
9013            for l in &mut self.kv_cache.layers {
9014                l.truncate_last(2);
9015            }
9016        }
9017        let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
9018
9019        let t1 = std::time::Instant::now();
9020        for _ in 0..iters {
9021            let _ = self.forward_pair(&emb1, &emb2, pos);
9022            for l in &mut self.kv_cache.layers {
9023                l.truncate_last(2);
9024            }
9025        }
9026        let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
9027        match graph_env {
9028            Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
9029            None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
9030        }
9031        (singles_ms, pair_ms)
9032    }
9033
9034    /// Fused two-position forward: weight rows are streamed from memory
9035    /// once per layer for both positions. Full layers → fused GQA pair;
9036    /// linear layers → vmf_phase pair (lane 2 state is tentative in the
9037    /// per-layer scratch until the draft is accepted).
9038    /// Whether the fused two-position path covers every layer kind in
9039    /// this model. MLA and KDA run per position (their pair arms are
9040    /// unreachable); the seq prefill falls back to singles for them.
9041    fn pair_supported(&self) -> bool {
9042        // An EMPTY layer stack means the architecture loaded its own and
9043        // this path has nothing to walk. Checking that directly, rather
9044        // than naming each such architecture, is what makes the guard hold
9045        // for the next one: `any()` over no layers is false, so a
9046        // feature-by-feature test says "supported" for a model that has no
9047        // layers here at all.
9048        !self.weights.layers.is_empty()
9049            && !self.per_token_stack()
9050            && !self
9051                .weights
9052                .layers
9053                .iter()
9054                .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
9055    }
9056
9057    fn forward_pair(
9058        &mut self,
9059        emb1: &[f32],
9060        emb2: &[f32],
9061        position: usize,
9062    ) -> (Vec<f32>, Vec<f32>) {
9063        // A two-token prompt starts here, not in the layer walk: decide the
9064        // MiMo placement before the pair's per-op MoE uploads any expert.
9065        self.mimo_moe_prepare();
9066        let mut h1 = emb1.to_vec();
9067        let mut h2 = emb2.to_vec();
9068        let (_nkv, _hd, hs, _rd, eps) = (
9069            self.num_kv_heads,
9070            self.head_dim,
9071            self.hidden_size,
9072            self.rotary_dim,
9073            self.rms_eps,
9074        );
9075        let pool = self.pool.clone();
9076        // `forward_pair` bypasses `forward_layers_span`, but its per-op
9077        // body must still obey the ordinary layer placement. In particular,
9078        // an over-capacity Vulkan model has a device prefix and a CPU tail;
9079        // letting the pair walk submit the tail's selected MoE experts would
9080        // stream the whole tail through the residency arena and defeat the
9081        // measured VRAM bound.
9082        let automatic_gpu_prefix = self.automatic_gpu_prefix();
9083
9084        for li in 0..self.num_layers {
9085            let _capacity_tail = automatic_gpu_prefix
9086                .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
9087                .map(|_| crate::gpu::enter_cpu_scope());
9088            // Also makes an explicit CMF_GPU_LAYERS split apply to fused
9089            // pairs, just as it does to the ordinary layer walk.
9090            crate::gpu::set_layer(li as i64);
9091            let lw = &self.weights.layers[self.phys_layer(li)];
9092            // Norms into pipeline scratch (4 allocs/layer on the MTP
9093            // decode hot path before this).
9094            inference::rms_norm_into(
9095                &h1,
9096                &lw.input_norm,
9097                self.rms_eps,
9098                self.norm_style,
9099                &mut self.ws.n1,
9100            );
9101            inference::rms_norm_into(
9102                &h2,
9103                &lw.input_norm,
9104                self.rms_eps,
9105                self.norm_style,
9106                &mut self.ws.n2,
9107            );
9108
9109            let (a1, a2) = match &lw.attn {
9110                AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
9111                AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
9112                AttnKind::Bounded(w) => {
9113                    // Two sequential positions of the bounded operator
9114                    // (the ring's causal order is the pair's order).
9115                    let rope = self
9116                        .bounded_rope
9117                        .clone()
9118                        .expect("bounded layer without an installed rotation table");
9119                    let cfg = crate::bounded::BoundedAttnCfg {
9120                        num_heads: self.num_heads,
9121                        num_kv_heads: self.num_kv_heads,
9122                        head_dim: self.head_dim,
9123                        hidden_size: hs,
9124                        scale: self.attn_scale,
9125                        rope: &rope,
9126                        pool: pool.as_deref(),
9127                    };
9128                    let a1 = crate::bounded::bounded_attention(
9129                        &self.ws.n1,
9130                        w,
9131                        &mut self.kv_cache.layers[li],
9132                        &cfg,
9133                    );
9134                    let a2 = crate::bounded::bounded_attention(
9135                        &self.ws.n2,
9136                        w,
9137                        &mut self.kv_cache.layers[li],
9138                        &cfg,
9139                    );
9140                    (a1, a2)
9141                }
9142                AttnKind::Linear(w) => {
9143                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
9144                    let layer = &mut self.kv_cache.layers[li];
9145                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9146                    vmf_phase_pair(
9147                        &self.ws.n1,
9148                        &self.ws.n2,
9149                        w,
9150                        &cfg,
9151                        state,
9152                        scratch,
9153                        self.pool.as_deref(),
9154                    )
9155                }
9156                AttnKind::LinearGdn(w) => {
9157                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9158                    let layer = &mut self.kv_cache.layers[li];
9159                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9160                    gdn_pair(
9161                        &self.ws.n1,
9162                        &self.ws.n2,
9163                        w,
9164                        &cfg,
9165                        state,
9166                        scratch,
9167                        self.pool.as_deref(),
9168                    )
9169                }
9170                AttnKind::ShortConv(w) => {
9171                    let cfg = self
9172                        .short_conv_cfg
9173                        .expect("short-conv layer without short_conv_cfg");
9174                    let layer = &mut self.kv_cache.layers[li];
9175                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9176                    short_conv_pair(
9177                        &self.ws.n1,
9178                        &self.ws.n2,
9179                        w,
9180                        &cfg,
9181                        state,
9182                        scratch,
9183                        self.pool.as_deref(),
9184                    )
9185                }
9186                AttnKind::Full {
9187                    wq,
9188                    wk,
9189                    wv,
9190                    wo,
9191                    q_norm,
9192                    k_norm,
9193                    output_gate,
9194                    softplus_gate,
9195                    bias,
9196                } => {
9197                    let inv_freq_l = self.layer_inv_freq(li);
9198                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9199                    let cfg = QwenAttnCfg {
9200                        mrope: None,
9201                        num_heads: self.layer_num_heads(li),
9202                        num_kv_heads: nkv_l,
9203                        head_dim: hd_l,
9204                        hidden_size: hs,
9205                        position,
9206                        inv_freq: &inv_freq_l,
9207                        rotary_dim: rd_l,
9208                        scale: self.attn_scale,
9209                        softcap: self.attn_softcap,
9210                        window: self.layer_window(li),
9211                        v_norm: self.attn_v_norm,
9212                        qk_norm_after_rope: self.qk_norm_after_rope,
9213                        gate_sigmoid: self.proj_gate_sigmoid,
9214                        q_norm: q_norm.as_deref(),
9215                        k_norm: k_norm.as_deref(),
9216                        output_gate: *output_gate,
9217                        softplus_gate: softplus_gate
9218                            .as_ref()
9219                            .map(|(gate, per_head)| (gate, *per_head)),
9220                        rope_scale: self.layer_rope_scale(li),
9221                        bias: bias
9222                            .as_ref()
9223                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9224                        rms_eps: eps,
9225                        norm_style: self.norm_style,
9226                        pool: pool.as_deref(),
9227                        v_head_dim: self.layer_v_dim(li),
9228                    };
9229                    attention::qwen_attention_pair(
9230                        &self.ws.n1,
9231                        &self.ws.n2,
9232                        wq,
9233                        wk,
9234                        wv,
9235                        wo,
9236                        &mut self.kv_cache.layers[li],
9237                        &cfg,
9238                    )
9239                }
9240            };
9241            let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
9242                Some(w) => (
9243                    inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
9244                    inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
9245                ),
9246                None => (a1, a2),
9247            };
9248            for i in 0..self.hidden_size {
9249                h1[i] += a1[i];
9250                h2[i] += a2[i];
9251            }
9252            let (mut a1, mut a2) = (a1, a2);
9253            attention::recycle_buf(&mut a1);
9254            attention::recycle_buf(&mut a2);
9255
9256            let lw = &self.weights.layers[self.phys_layer(li)];
9257            inference::rms_norm_into(
9258                &h1,
9259                &lw.post_norm,
9260                self.rms_eps,
9261                self.norm_style,
9262                &mut self.ws.p1,
9263            );
9264            inference::rms_norm_into(
9265                &h2,
9266                &lw.post_norm,
9267                self.rms_eps,
9268                self.norm_style,
9269                &mut self.ws.p2,
9270            );
9271            let (f1, f2) = match &lw.ffn {
9272                // Dual-branch layers need the raw residuals — run the
9273                // two positions through the same fn decode uses.
9274                FfnKind::DenseMoe(dm) => (
9275                    dense_moe_ffn(
9276                        dm,
9277                        &self.ws.p1,
9278                        &h1,
9279                        self.rms_eps,
9280                        self.norm_style,
9281                        self.pool.as_deref(),
9282                    ),
9283                    dense_moe_ffn(
9284                        dm,
9285                        &self.ws.p2,
9286                        &h2,
9287                        self.rms_eps,
9288                        self.norm_style,
9289                        self.pool.as_deref(),
9290                    ),
9291                ),
9292                FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
9293                    moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
9294                    moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
9295                ),
9296                _ => ffn_forward_pair(
9297                    &lw.ffn,
9298                    &self.ws.p1,
9299                    &self.ws.p2,
9300                    self.pool.as_deref(),
9301                    None,
9302                ),
9303            };
9304            let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
9305                Some(w) => (
9306                    inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
9307                    inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
9308                ),
9309                None => (f1, f2),
9310            };
9311            for i in 0..self.hidden_size {
9312                h1[i] += f1[i];
9313                h2[i] += f2[i];
9314            }
9315            let (mut f1, mut f2) = (f1, f2);
9316            attention::recycle_buf(&mut f1);
9317            attention::recycle_buf(&mut f2);
9318            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
9319                for i in 0..self.hidden_size {
9320                    h1[i] *= sc;
9321                    h2[i] *= sc;
9322                }
9323            }
9324            // Looped Transformer: apply final norm at the end of each loop iteration.
9325            if self.is_loop_end(li) && li + 1 < self.num_layers {
9326                h1 = inference::rms_norm(
9327                    &h1,
9328                    &self.weights.final_norm,
9329                    self.rms_eps,
9330                    self.norm_style,
9331                );
9332                h2 = inference::rms_norm(
9333                    &h2,
9334                    &self.weights.final_norm,
9335                    self.rms_eps,
9336                    self.norm_style,
9337                );
9338            }
9339        }
9340        crate::gpu::set_layer(-1); // final/O(1) work is outside layer placement
9341        // Real O(1) prefill pairs may also carry tentative lane-2 recurrent
9342        // state. Commit it before publishing the transition epoch so the
9343        // next serial/device row cannot observe a new attention epoch with an
9344        // old GDN state. Speculative pairs run only when O(1) is inactive and
9345        // retain their existing caller-controlled commit/rollback semantics.
9346        if self.o1_active() {
9347            self.commit_linear_scratch();
9348        }
9349        self.o1_progress();
9350        self.swa_trim_tails();
9351        (h1, h2)
9352    }
9353
9354    /// Commit lane-2 linear states after an accepted draft.
9355    fn commit_linear_scratch(&mut self) {
9356        for layer in &mut self.kv_cache.layers {
9357            if !layer.linear_scratch.is_empty() {
9358                std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
9359                layer.linear_scratch.clear();
9360            }
9361        }
9362    }
9363
9364    /// This is the encoder counterpart to `forward_ids`: it runs a fresh
9365    /// sequence through the existing full/GDN hybrid stack and returns the
9366    /// final RMS-normalized hidden row for *every* token.  It is used by
9367    /// component pipelines whose text tower intentionally has no LM head
9368    /// (such as an image-conditioner Qwen3.5 model).  The sequence state is
9369    /// always released before returning, so a large text tower can be dropped
9370    /// immediately after conditioning a subsequent stage.
9371    pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9372        #[cfg(target_os = "macos")]
9373        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9374        if ids.is_empty() {
9375            return Err("empty id sequence".to_string());
9376        }
9377
9378        self.nll_begin()?;
9379        let result = (|| {
9380            let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9381            self.nll_check_graph("encoder prefill", 0)?;
9382            let expected = ids.len() * self.hidden_size;
9383            if hidden.len() != expected {
9384                return Err(format!(
9385                    "encoder prefill returned {} values for {} rows of hidden size {}",
9386                    hidden.len(),
9387                    ids.len(),
9388                    self.hidden_size
9389                ));
9390            }
9391
9392            let mut out = vec![0.0f32; expected];
9393            for (src, dst) in hidden
9394                .chunks_exact(self.hidden_size)
9395                .zip(out.chunks_exact_mut(self.hidden_size))
9396            {
9397                inference::rms_norm_into(
9398                    src,
9399                    &self.weights.final_norm,
9400                    self.rms_eps,
9401                    self.norm_style,
9402                    dst,
9403                );
9404            }
9405            Ok(out)
9406        })();
9407        self.nll_end();
9408        result
9409    }
9410
9411    /// The last layer's hidden row (before the final norm) for every token
9412    /// of `ids`, from a fresh sequence, through the whole-stack prefill (the
9413    /// device chunk graph where there is one) — what a distillation cache of
9414    /// teacher outputs needs, at prefill speed.
9415    pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9416        #[cfg(target_os = "macos")]
9417        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9418        if ids.is_empty() {
9419            return Err("empty id sequence".to_string());
9420        }
9421        self.nll_begin()?;
9422        let result = (|| {
9423            let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9424            self.nll_check_graph("hidden prefill", 0)?;
9425            if hidden.len() != ids.len() * self.hidden_size {
9426                return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
9427            }
9428            Ok(hidden)
9429        })();
9430        self.nll_end();
9431        result
9432    }
9433
9434    /// Forward a full id sequence from a fresh cache and return the
9435    /// logits after the last position (golden-parity harness, bench).
9436    pub fn forward_ids(
9437        &mut self,
9438        ids: &[u32],
9439        task_mask: Option<&TaskMask>,
9440    ) -> Result<Vec<f32>, String> {
9441        #[cfg(target_os = "macos")]
9442        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9443        if ids.is_empty() {
9444            return Err("empty id sequence".to_string());
9445        }
9446        self.clear_sequence_state();
9447        self.check_forward_graph("forward_ids setup", 0)?;
9448        if task_mask.is_none() {
9449            self.o1_begin();
9450        }
9451        let mut hidden = vec![0.0f32; self.hidden_size];
9452        let mut pos = 0usize;
9453        if let Some(b) = &mut self.dsv41 {
9454            let pool = self.pool.clone();
9455            let mut logits = Vec::new();
9456            crate::dsv41::forward_chunk(
9457                &b.0,
9458                &b.1,
9459                &b.2,
9460                &mut b.3,
9461                ids,
9462                0,
9463                pool.as_deref(),
9464                &mut logits,
9465            );
9466            if let Err(err) = self.o1_seal_checked() {
9467                self.clear_sequence_state();
9468                return Err(err);
9469            }
9470            return Ok(logits);
9471        }
9472        // Same routing predicate generation uses. Two reasons it must be
9473        // the same one: (1) a GDN hybrid's recurrent state is GPU-
9474        // resident, and a batched CPU prefill would build it on the host
9475        // only — decode then reads buffers the prefill never wrote;
9476        // (2) bench times THIS function and calls the result "prefill",
9477        // so a different path here reports a number production never
9478        // sees (W2 on 2×5090: 8.7 tok/s reported against 125 real).
9479        // Gemma-4: generation's batched-graph ingest (`g4_batch_ingest`),
9480        // so bench and the parity tools time and score the route run/serve
9481        // take; a refusal leaves the rest to the chunked host prefill.
9482        #[cfg(not(target_os = "macos"))]
9483        if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
9484            let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
9485                Ok(r) => r,
9486                Err(e) => {
9487                    self.clear_sequence_state();
9488                    return Err(e);
9489                }
9490            };
9491            if n > 0 {
9492                hidden.copy_from_slice(&last);
9493                pos = n;
9494            }
9495        }
9496        if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
9497            // prefill-GEMM in chunks; only the last position's hidden is
9498            // needed. (o1-compatible: the batch path attends per position
9499            // through qwen_attention, which carries the collection hook.)
9500            let chunk = self.prefill_chunk();
9501            let hs = self.hidden_size;
9502            while pos < ids.len() {
9503                let end = (pos + chunk).min(ids.len());
9504                let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9505                self.check_forward_graph("forward_ids batched prefill", end - 1)?;
9506                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
9507                pos = end;
9508            }
9509        }
9510        // Same guards as generation's prefill — INCLUDING the graph one.
9511        // The CPU pair walk was intercepting positions that the resident
9512        // token graph would have run itself: on a GDN hybrid over wgpu
9513        // that is 89 ms of host forward against 7 ms of device submit,
9514        // and it made prefill look 12× slower than it is (W2 on an RTX
9515        // 5090, ctx 512: 11.2 tok/s with the walk, 136.6 without).
9516        // CMF_PAIR=0 opts out; a model whose layers live outside
9517        // `weights.layers` has no pair walk to take.
9518        if task_mask.is_none()
9519            && !self.graph_prefill_preferred()
9520            && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9521            && self.pair_supported()
9522        {
9523            while pos + 1 < ids.len() {
9524                let e1 = self.embed_single(ids[pos]);
9525                let e2 = self.embed_single(ids[pos + 1]);
9526                let (_, h2) = self.forward_pair(&e1, &e2, pos);
9527                self.check_forward_graph("forward_ids pair", pos + 1)?;
9528                self.commit_linear_scratch();
9529                hidden = h2;
9530                pos += 2;
9531            }
9532        }
9533        // Resident Embryo graph: the prompt in chunks of one submit each
9534        // (the same device state and logits as the per-position walk).
9535        if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9536            if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9537                self.graph_logits = Some(lg);
9538                hidden = vec![0.0; self.hidden_size];
9539                pos = ids.len();
9540            }
9541        }
9542        while pos < ids.len() {
9543            hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9544            self.check_forward_graph("forward_ids", pos)?;
9545            pos += 1;
9546        }
9547        if let Some(logits) = self.graph_logits.take() {
9548            // Resident stacks already applied final norm and their head in
9549            // the same submit; do not run a second norm/head over the zero
9550            // hidden sentinel returned by forward_layers_span.
9551            if let Err(err) = self.o1_seal_checked() {
9552                self.clear_sequence_state();
9553                return Err(err);
9554            }
9555            return Ok(logits);
9556        }
9557        // Harness contract: after forward_ids the cache is decode-ready —
9558        // under o1 that means sealed (bench measures the seal as part of
9559        // prefill, honestly).
9560        if let Err(err) = self.o1_seal_checked() {
9561            self.clear_sequence_state();
9562            return Err(err);
9563        }
9564        let normed = inference::rms_norm(
9565            &hidden,
9566            &self.weights.final_norm,
9567            self.rms_eps,
9568            self.norm_style,
9569        );
9570        Ok(self.lm_head_forward(&normed))
9571    }
9572
9573    /// Run the V4.1 stack one token at a time and retain logits for every
9574    /// position. This is a diagnostic surface for comparing a converted
9575    /// checkpoint with a tokenwise reference implementation.
9576    #[doc(hidden)]
9577    pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9578        #[cfg(target_os = "macos")]
9579        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9580        if ids.is_empty() {
9581            return Err("empty id sequence".to_string());
9582        }
9583        self.clear_sequence_state();
9584        self.dsv41
9585            .as_ref()
9586            .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9587        self.o1_begin();
9588        let rows = {
9589            let pool = self.pool.clone();
9590            let b = self
9591                .dsv41
9592                .as_mut()
9593                .expect("dsv41 checked above; state cannot change during forward");
9594            let mut rows = Vec::with_capacity(ids.len());
9595            for (position, &id) in ids.iter().enumerate() {
9596                let mut logits = Vec::new();
9597                crate::dsv41::forward_token(
9598                    &b.0,
9599                    &b.1,
9600                    &b.2,
9601                    &mut b.3,
9602                    id,
9603                    position,
9604                    pool.as_deref(),
9605                    &mut logits,
9606                );
9607                rows.push(logits);
9608            }
9609            rows
9610        };
9611        self.o1_seal();
9612        Ok(rows)
9613    }
9614
9615    /// GPTQ calibration of DeepSeek-V4.1 (`cortiq quantize-moe`): the MoE
9616    /// input rows and routed expert ids of layers `lo..hi` for `ids` (a fresh
9617    /// sequence), see [`crate::dsv41::Dsv41MoeCapture`].
9618    pub fn dsv41_moe_capture(
9619        &mut self,
9620        ids: &[u32],
9621        lo: usize,
9622        hi: usize,
9623    ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9624        #[cfg(target_os = "macos")]
9625        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9626        if ids.is_empty() {
9627            return Err("empty id sequence".to_string());
9628        }
9629        self.dsv41
9630            .as_ref()
9631            .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9632        self.clear_sequence_state();
9633        let res = {
9634            let pool = self.pool.clone();
9635            let b = self.dsv41.as_mut().expect("dsv41 checked above");
9636            crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9637        };
9638        self.clear_sequence_state();
9639        res
9640    }
9641
9642    /// Teacher-forced perplexity over a token sequence (phase-C gate:
9643    /// honest quant comparisons instead of prompt vibes).
9644    ///
9645    /// Attention is EXACT even on a model whose layers are flagged for
9646    /// the O(1) kernel — scoring the backbone is the default on purpose
9647    /// (it is the yardstick). `nll_ids_o1` scores the CONVERTED model.
9648    pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9649        let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9650        Ok((nll / cnt.max(1) as f64).exp())
9651    }
9652
9653    /// GPTQ calibration forward: `ids` through layers `0..upto` only, in
9654    /// layer-major chunks of `chunk` positions, with no vocabulary head and
9655    /// no scoring. The Hessian hook (`gptq_capture`) folds every
9656    /// projection's input on the way; the layers above the captured span
9657    /// and the head would only cost time. Falls back to a scored forward
9658    /// where the batched prefill does not apply.
9659    pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9660        if !self.can_prefill_batched() {
9661            return self.nll_ids_from(ids, 0).map(|_| ());
9662        }
9663        self.nll_begin()?;
9664        let result: Result<(), String> = (|| {
9665            let chunk = chunk.max(1);
9666            let mut pos = 0usize;
9667            while pos < ids.len() {
9668                let end = (pos + chunk).min(ids.len());
9669                let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9670                self.nll_check_graph("calibration prefill", pos)?;
9671                pos = end;
9672            }
9673            Ok(())
9674        })();
9675        self.nll_end();
9676        result
9677    }
9678
9679    /// Layer-streamed calibration of one fresh window: layers `from..mid` run
9680    /// on `input` (the hidden state entering layer `from`; `None` = the
9681    /// token embeddings of `ids`) and their output is returned, then layers
9682    /// `mid..upto` run on that output for the capture hooks (their output is
9683    /// dropped). A caller that keeps the returned state per window runs each
9684    /// layer once over the whole conversion instead of re-running the stack
9685    /// below every layer. With `mid == from` the input is returned as is
9686    /// (`None` input: an empty vector).
9687    pub fn calib_window_span(
9688        &mut self,
9689        ids: &[u32],
9690        input: Option<&[f32]>,
9691        from: usize,
9692        mid: usize,
9693        upto: usize,
9694    ) -> Result<Vec<f32>, String> {
9695        if !self.can_prefill_batched() {
9696            return Err("calib_window_span: this architecture has no batched prefill".into());
9697        }
9698        self.nll_begin()?;
9699        let result: Result<Vec<f32>, String> = (|| {
9700            let h = if mid > from {
9701                let inp = match input {
9702                    Some(h) => PrefillIn::Hidden(h),
9703                    None => PrefillIn::Ids(ids),
9704                };
9705                let h = self.prefill_batch_span(inp, 0, None, from, mid);
9706                self.nll_check_graph("calibration span", 0)?;
9707                h
9708            } else {
9709                input.map(<[f32]>::to_vec).unwrap_or_default()
9710            };
9711            if upto > mid {
9712                let inp = if mid > from || input.is_some() {
9713                    PrefillIn::Hidden(&h)
9714                } else {
9715                    PrefillIn::Ids(ids)
9716                };
9717                let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9718                self.nll_check_graph("calibration span", 0)?;
9719            }
9720            Ok(h)
9721        })();
9722        self.nll_end();
9723        result
9724    }
9725
9726    /// GPTQ calibration of a Flash-Next-class MoE (`qwen4_exp`): MoE input
9727    /// rows and router logits of layers `lo..hi` for `ids` (a fresh
9728    /// sequence), per layer `[ids.len()][hidden + n_experts]`.
9729    #[cfg(feature = "gpu")]
9730    pub fn qwen4_capture(
9731        &mut self,
9732        ids: &[u32],
9733        lo: usize,
9734        hi: usize,
9735    ) -> Result<Vec<Vec<f32>>, String> {
9736        let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9737        crate::qwen4_exp::capture_window(
9738            &b.0,
9739            &b.1,
9740            &b.2,
9741            &mut b.3,
9742            ids,
9743            lo,
9744            hi,
9745            &self.inv_freq,
9746            self.pool.as_deref(),
9747        )
9748    }
9749
9750    /// GPTQ calibration capture of an ordinary MoE stack (Qwen3.5/3.6
9751    /// MoE, Clover): `ids` from position 0 through the batched device graph
9752    /// in prefill-sized chunks, the MoE input rows and router logits of
9753    /// layers `lo..hi` copied out after routing. Returns per layer
9754    /// `[ids.len()][hidden + n_experts]`; the sequence state is left fresh.
9755    #[cfg(feature = "gpu")]
9756    pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9757        use crate::gpu_wgpu::clover as cap;
9758        let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9759        let ne = (lo..hi)
9760            .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9761                FfnKind::Moe(m) => Some(m.experts.len()),
9762                _ => None,
9763            })
9764            .ok_or("no MoE layer in the capture range")?;
9765        let chunk = self.prefill_chunk().max(1);
9766        self.clear_sequence_state();
9767        if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9768            return Err("capture unavailable (no wgpu device)".into());
9769        }
9770        let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9771        let res = (|| {
9772            for (ci, part) in ids.chunks(chunk).enumerate() {
9773                let mut hiddens = Vec::with_capacity(part.len() * hs);
9774                for &id in part {
9775                    hiddens.extend_from_slice(&self.embed_single(id));
9776                }
9777                let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9778                match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9779                    crate::gpu::BatchGraphOutcome::Completed => {}
9780                    _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9781                }
9782                let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9783                if written < hi - lo {
9784                    return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9785                }
9786                for (o, r) in out.iter_mut().zip(rows) {
9787                    o.extend_from_slice(&r);
9788                }
9789            }
9790            Ok(())
9791        })();
9792        cap::moe_capture_end();
9793        self.clear_sequence_state();
9794        res.map(|_| out)
9795    }
9796
9797    /// Expert directory triples (gate, up, down) of an ordinary MoE layer,
9798    /// routed and shared.
9799    pub fn moe_expert_dirs(
9800        &self,
9801        li: usize,
9802    ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9803        let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9804            return None;
9805        };
9806        let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9807        let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9808        let shared = match &m.shared {
9809            Some((se, _)) => Some(dirs(se)?),
9810            None => None,
9811        };
9812        Some((routed, shared))
9813    }
9814
9815    /// Expert directory triples of a `qwen4_exp` layer (routed, shared).
9816    pub fn qwen4_expert_dirs(
9817        &self,
9818        li: usize,
9819    ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9820        let b = self.qwen4_exp.as_ref()?;
9821        crate::qwen4_exp::expert_dirs(&b.1, li)
9822    }
9823
9824    /// Teacher-force the installed MTP draft stack and collect each draft
9825    /// FFN's own pre-down activation statistics.
9826    ///
9827    /// This is deliberately separate from [`Self::probe_ffn_mass`]: canonical
9828    /// MTP and MiMo sidecars can have independently compacted FFNs, and MiMo
9829    /// draft layers can differ in width from one another and from the trunk.
9830    /// The returned vector follows the actual draft stack (one entry for a
9831    /// canonical MTP head; one per MiMo draft layer). MiMo layer `k` replays
9832    /// the teacher-forced pairs `(hidden[j], ids[j + k + 1])`.
9833    ///
9834    /// Calibration starts from a fresh sequence and leaves the trunk, draft
9835    /// KVs, history, and device mirrors fresh on both success and error. It
9836    /// is therefore unsuitable for the middle of a conversation whose KV
9837    /// cache must be retained.
9838    pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9839        self.reset_mtp_probe_state();
9840        // The trunk runs as one prefill (the device graph where there is
9841        // one); the draft replays on the host under a CPU scope, where its
9842        // FFN input can be measured. This guard also suppresses gate/oracle
9843        // sparsity experiments for the duration so the statistics and
9844        // admission replay always execute the full dense FFN.
9845        let _exact_calibration = ExactMtpCalibrationGuard::enter();
9846        let result = if ids.len() < 2 {
9847            Err("MTP FFN probe needs at least two token ids".into())
9848        } else if ids
9849            .iter()
9850            .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9851        {
9852            Err("MTP FFN probe received a token id outside the embedding table".into())
9853        } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9854            // A synthetic pipeline can carry both mechanisms, but merging
9855            // unlike draft semantics into one compaction plan is unsafe.
9856            Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9857        } else if self.mimo_mtp.is_some() {
9858            let mut st = self
9859                .mimo_mtp
9860                .take()
9861                .expect("checked MiMo MTP before detaching it");
9862            let result = match self.probe_trunk_rows(ids) {
9863                Ok(trunk) => crate::gpu::cpu_scope(|| {
9864                    self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
9865                }),
9866                Err(e) => Err(e),
9867            };
9868            // The owner is detached during the replay, so the common trunk
9869            // reset cannot clear its KVs. Restore it even on an error.
9870            st.reset();
9871            self.mimo_mtp = Some(st);
9872            result
9873        } else if self.mtp.is_some() {
9874            let mut mtp = self
9875                .mtp
9876                .take()
9877                .expect("checked canonical MTP before detaching it");
9878            let result = match self.probe_trunk_rows(ids) {
9879                Ok(trunk) => crate::gpu::cpu_scope(|| {
9880                    self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
9881                }),
9882                Err(e) => Err(e),
9883            };
9884            mtp.kv.clear();
9885            self.mtp = Some(mtp);
9886            result
9887        } else {
9888            Err("MTP FFN probe: this pipeline has no draft stack".into())
9889        };
9890        // Cover every early/error arm above: no failed calibration can leak
9891        // a partially populated trunk or draft cache into the next request.
9892        self.reset_mtp_probe_state();
9893        result
9894    }
9895
9896    /// Reset every state owner touched by [`Self::probe_mtp_ffn`].
9897    fn reset_mtp_probe_state(&mut self) {
9898        self.clear_sequence_state();
9899        if let Some(mtp) = self.mtp.as_mut() {
9900            mtp.kv.clear();
9901        }
9902        if let Some(mimo) = self.mimo_mtp.as_mut() {
9903            mimo.reset();
9904        }
9905        self.mtp_graph_mode = None;
9906    }
9907
9908    /// The trunk's last hidden row (before the final norm) at every
9909    /// position a teacher-forced draft probe reads (`ids[..len-1]`), from
9910    /// the whole-stack prefill — the device graph where there is one. One
9911    /// position at a time through `forward_layers` on the host this was the
9912    /// probe's whole cost: a 14B's 96k calibration tokens ran for hours.
9913    pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9914        if ids.len() < 2 {
9915            return Err("MTP FFN probe needs at least two token ids".into());
9916        }
9917        let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
9918        self.clear_sequence_state();
9919        Ok(rows)
9920    }
9921
9922    /// Canonical one-layer MTP teacher-forced replay over the trunk rows
9923    /// of [`Self::probe_trunk_rows`]. Its cache advances for every row so
9924    /// later attention inputs match a real draft warm-up rather than a
9925    /// collection of independent one-token measurements. The FFN inputs are
9926    /// measured in batches: nothing after the FFN feeds a later row.
9927    fn mtp_ffn_probe_teacher_forced(
9928        &mut self,
9929        mtp: &mut MtpModule,
9930        ids: &[u32],
9931        trunk: &[f32],
9932    ) -> Result<Vec<MtpFfnProbe>, String> {
9933        let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9934            return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9935        };
9936        let inter = dense.gate_proj.rows();
9937        let (hs, n) = (self.hidden_size, ids.len() - 1);
9938        if trunk.len() != n * hs {
9939            return Err(format!(
9940                "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
9941                trunk.len()
9942            ));
9943        }
9944        let mut mass = vec![0.0f64; inter];
9945        let mut squared_mass = vec![0.0f64; inter];
9946        let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
9947        for position in 0..n {
9948            self.mtp_probe_ffn_input(
9949                mtp,
9950                &trunk[position * hs..(position + 1) * hs],
9951                ids[position + 1],
9952                position,
9953            )?;
9954            inputs.extend_from_slice(&self.ws.p1[..hs]);
9955            if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
9956                let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9957                    unreachable!("checked dense above");
9958                };
9959                accumulate_dense_ffn_activation_mass(
9960                    dense,
9961                    &inputs,
9962                    inputs.len() / hs,
9963                    self.pool.as_deref(),
9964                    &mut mass,
9965                    &mut squared_mass,
9966                )?;
9967                inputs.clear();
9968            }
9969        }
9970        Ok(vec![MtpFfnProbe {
9971            mass,
9972            squared_mass,
9973            observed_positions: n,
9974        }])
9975    }
9976
9977    /// CPU-only MTP block replay up to the FFN's input (the post-attention
9978    /// norm, left in `ws.p1`), where the probe measures. The ordinary
9979    /// `mtp_step_hl` device path keeps these values resident and therefore
9980    /// cannot truthfully provide a per-neuron calibration statistic. The
9981    /// FFN arm itself is not run: its output feeds only this row's logits.
9982    fn mtp_probe_ffn_input(
9983        &mut self,
9984        mtp: &mut MtpModule,
9985        hidden: &[f32],
9986        next_token: u32,
9987        position: usize,
9988    ) -> Result<(), String> {
9989        let e = self.embed_single(next_token);
9990        let mut cat = vec![0.0f32; 2 * self.hidden_size];
9991        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9992        inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9993        inference::rms_norm_into(
9994            hidden,
9995            &mtp.hnorm,
9996            self.rms_eps,
9997            self.norm_style,
9998            cat_h,
9999        );
10000        let mut x = vec![0.0f32; self.hidden_size];
10001        mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
10002
10003        let lw = &mtp.layer;
10004        inference::rms_norm_into(
10005            &x,
10006            &lw.input_norm,
10007            self.rms_eps,
10008            self.norm_style,
10009            &mut self.ws.n1,
10010        );
10011        let attn = match &lw.attn {
10012            AttnKind::Full {
10013                wq,
10014                wk,
10015                wv,
10016                wo,
10017                q_norm,
10018                k_norm,
10019                output_gate,
10020                softplus_gate,
10021                bias,
10022            } => {
10023                let mut cfg = self.attn_cfg(position);
10024                cfg.q_norm = q_norm.as_deref();
10025                cfg.k_norm = k_norm.as_deref();
10026                cfg.output_gate = *output_gate;
10027                cfg.softplus_gate = softplus_gate
10028                    .as_ref()
10029                    .map(|(gate, per_head)| (gate, *per_head));
10030                cfg.bias = bias
10031                    .as_ref()
10032                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
10033                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
10034            }
10035            _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
10036        };
10037        for (dst, src) in x.iter_mut().zip(attn) {
10038            *dst += src;
10039        }
10040        inference::rms_norm_into(
10041            &x,
10042            &lw.post_norm,
10043            self.rms_eps,
10044            self.norm_style,
10045            &mut self.ws.p1,
10046        );
10047        Ok(())
10048    }
10049
10050    /// DTG-MA calibration pass (Patent 2): run `ids` through the model
10051    /// (CPU path, per position) and return each layer's per-neuron
10052    /// activation mass Σ|silu(gate)·up| — the statistic the task-guided
10053    /// FFN mask is derived from.
10054    pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
10055        self.clear_sequence_state();
10056        FFN_PROBE.with(|p| {
10057            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
10058        });
10059        crate::gpu::cpu_scope(|| {
10060            for (pos, &id) in ids.iter().enumerate() {
10061                let emb = self.embed_single(id);
10062                let _ = self.forward_layers(&emb, pos, None);
10063            }
10064        });
10065        self.clear_sequence_state();
10066        FFN_PROBE
10067            .with(|p| p.borrow_mut().take())
10068            .unwrap_or_default()
10069    }
10070
10071    /// `probe_ffn_mass` over the BATCHED prefill: same accumulator, one
10072    /// sweep instead of one forward per token. What makes the statistic
10073    /// affordable on a 27B.
10074    pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
10075        if let Err(err) = self.nll_begin() {
10076            // A recorder can be left by a caller that was interrupted before
10077            // this request entered its scoring block.  Consume it even when
10078            // the preflight failure prevents initialization of a new one.
10079            let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
10080            self.nll_end();
10081            return Err(err);
10082        }
10083        FFN_PROBE.with(|p| {
10084            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
10085        });
10086        let result: Result<(), String> = (|| {
10087            for chunk in ids.chunks(256) {
10088                if chunk.len() < 2 {
10089                    continue;
10090                }
10091                self.nll_ids_masked(chunk, 0, None)?;
10092            }
10093            Ok(())
10094        })();
10095        self.nll_end();
10096        let probe = FFN_PROBE
10097            .with(|p| p.borrow_mut().take())
10098            .unwrap_or_default();
10099        match result {
10100            Ok(()) => Ok(probe),
10101            Err(err) => {
10102                drop(probe);
10103                Err(err)
10104            }
10105        }
10106    }
10107
10108    /// Teacher-forced PPL with a task mask active (sparse execution) —
10109    /// the quality gate for a DTG-MA-masked skill. Sequential per
10110    /// position: the batched prefill path is dense-only.
10111    pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
10112        self.nll_begin()?;
10113        let result: Result<f64, String> = (|| {
10114            let mut nll = 0f64;
10115            let mut cnt = 0usize;
10116            let mut hidden = vec![0f32; self.hidden_size];
10117            for (pos, &id) in ids.iter().enumerate() {
10118                if pos > 0 {
10119                    inference::rms_norm_into(
10120                        &hidden,
10121                        &self.weights.final_norm,
10122                        self.rms_eps,
10123                        self.norm_style,
10124                        &mut self.ws.n1,
10125                    );
10126                    let mut logits = self.lm_head_forward(&self.ws.n1);
10127                    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10128                    let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
10129                    let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
10130                    nll -= p.max(1e-300).ln();
10131                    cnt += 1;
10132                    attention::recycle_buf(&mut logits);
10133                }
10134                let emb = self.embed_single(id);
10135                hidden = self.forward_layers(&emb, pos, Some(mask));
10136                self.nll_check_graph("masked serial forward", pos)?;
10137                // Consume a possible graph logits side channel before the
10138                // next row.  Masked scoring normally disables that route,
10139                // but stale channel state must never survive a request.
10140                let _ = self.graph_logits.take();
10141            }
10142            Ok((nll / cnt.max(1) as f64).exp())
10143        })();
10144        self.nll_end();
10145        result
10146    }
10147
10148    /// Teacher-forced NLL sum + scored-token count over positions
10149    /// `start..len-1`, attention EXACT. Positions below `start` still
10150    /// run — they are the context — they are just not scored, so this
10151    /// pairs with `nll_ids_o1(ids, start)` over the very same tokens.
10152    ///
10153    /// Returning (nll, cnt) rather than a ppl is what lets a windowed
10154    /// caller combine windows before the exp, so every scored token
10155    /// weighs the same regardless of how the windows are cut.
10156    /// `nll_ids_from` with a task mask held active at every position.
10157    ///
10158    /// The batched prefill path does not thread masks, so this walks the
10159    /// per-position forward — slower, but it scores the file exactly the
10160    /// way `run --task` will serve it, which is the point of the gate
10161    /// that calls it. With `None` it defers to the fast path.
10162    /// Masked scoring rides the SAME batched sweep as unmasked scoring —
10163    /// the masked-inference fast path: `prefill_batch_masked` lands the
10164    /// per-visit FFN rows on the activations inside the fused arms. The
10165    /// per-position loop below remains only as the no-batch fallback.
10166    pub fn nll_ids_masked(
10167        &mut self,
10168        ids: &[u32],
10169        start: usize,
10170        task_mask: Option<&TaskMask>,
10171    ) -> Result<(f64, usize), String> {
10172        let task_mask = self.drop_open_mask(task_mask);
10173        self.nll_ids_inner(ids, start, task_mask)
10174    }
10175
10176    pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
10177        self.nll_ids_inner(ids, start, None)
10178    }
10179
10180    /// Diagnostic (`CMF_PREFILL_PARITY=split[,k]` on `cortiq ppl`): the same
10181    /// tokens through three ingests, each from a clean sequence — H the host
10182    /// chunked prefill, G the wgpu batch graph in k-row chunks, M the host
10183    /// prefill up to `split` and the batch graph after it, seeded from the
10184    /// host state — and where they part: NLL and top-1 per 256-position bin
10185    /// from each arm's post-stack rows, every GDN layer's recurrent state
10186    /// (the host `linear_state` against the graph's) at `split` and at the
10187    /// end, and every exact-attention layer's K/V rows.
10188    pub fn prefill_parity_probe(
10189        &mut self,
10190        ids: &[u32],
10191        split: usize,
10192        bk: usize,
10193    ) -> Result<String, String> {
10194        use std::fmt::Write as _;
10195        let (n, hs) = (ids.len(), self.hidden_size);
10196        if split == 0 || split >= n || bk == 0 || !self.can_prefill_batched() {
10197            return Err(format!(
10198                "prefill parity: needs 0 < split < {n} tokens, k > 0 and a batched host prefill"
10199            ));
10200        }
10201        // 1 = GDN, 2 = exact attention, 0 = anything else (not compared).
10202        let kinds: Vec<u8> = (0..self.num_layers)
10203            .map(|li| match &self.weights.layers[self.phys_layer(li)].attn {
10204                AttnKind::LinearGdn(_) => 1,
10205                AttnKind::Full { .. } => 2,
10206                _ => 0,
10207            })
10208            .collect();
10209        let kv_id = self.graph_kv_id;
10210        let host_chunk = self.prefill_chunk().max(1);
10211        let host_states = |p: &Self| -> Vec<Vec<f32>> {
10212            (0..p.num_layers)
10213                .map(|li| {
10214                    if kinds[li] == 1 {
10215                        p.kv_cache.layers[li].linear_state.clone()
10216                    } else {
10217                        Vec::new()
10218                    }
10219                })
10220                .collect()
10221        };
10222        let graph_states = |_p: &Self| -> Vec<Vec<f32>> {
10223            (0..kinds.len())
10224                .map(|li| {
10225                    if kinds[li] == 1 {
10226                        crate::gpu::graph_gdn_state_read(kv_id, li).unwrap_or_default()
10227                    } else {
10228                        Vec::new()
10229                    }
10230                })
10231                .collect()
10232        };
10233        // Position-major K/V rows [0, n) of every exact-attention layer.
10234        let host_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
10235            (0..p.num_layers)
10236                .map(|li| {
10237                    let c = &p.kv_cache.layers[li];
10238                    if kinds[li] != 2
10239                        || c.mode != crate::kv_cache::KvMode::F32
10240                        || c.base() != 0
10241                        || c.seq_len != n
10242                    {
10243                        return (Vec::new(), Vec::new());
10244                    }
10245                    let (nkv, hd) = (c.num_kv_heads, c.head_dim);
10246                    let (mut k, mut v) = (vec![0f32; n * nkv * hd], vec![0f32; n * nkv * hd]);
10247                    for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
10248                        for pos in 0..n {
10249                            let d = (pos * nkv + h) * hd;
10250                            k[d..d + hd].copy_from_slice(&kh[pos * hd..(pos + 1) * hd]);
10251                            v[d..d + hd].copy_from_slice(&vh[pos * hd..(pos + 1) * hd]);
10252                        }
10253                    }
10254                    (k, v)
10255                })
10256                .collect()
10257        };
10258        let graph_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
10259            (0..p.num_layers)
10260                .map(|li| {
10261                    let c = &p.kv_cache.layers[li];
10262                    if kinds[li] != 2 {
10263                        return (Vec::new(), Vec::new());
10264                    }
10265                    crate::gpu::graph_kv_read_rows(kv_id, &[(li, 0, n)], c.num_kv_heads, c.head_dim)
10266                        .and_then(|mut v| v.pop())
10267                        .unwrap_or_default()
10268                })
10269                .collect()
10270        };
10271        let host_walk = |p: &mut Self,
10272                         from: usize,
10273                         to: usize,
10274                         rows: &mut [f32],
10275                         at_split: &mut Option<Vec<Vec<f32>>>| {
10276            let mut pos = from;
10277            while pos < to {
10278                let mut end = (pos + host_chunk).min(to);
10279                if pos < split && end > split {
10280                    end = split;
10281                }
10282                let hb = p.prefill_batch(&ids[pos..end], pos);
10283                rows[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10284                pos = end;
10285                if pos == split {
10286                    *at_split = Some(host_states(p));
10287                }
10288            }
10289        };
10290        let graph_walk = |p: &mut Self,
10291                          from: usize,
10292                          to: usize,
10293                          rows: &mut [f32],
10294                          at_split: &mut Option<Vec<Vec<f32>>>|
10295         -> Result<(), String> {
10296            let mut pos = from;
10297            while pos < to {
10298                let mut end = (pos + bk).min(to);
10299                if pos < split && end > split {
10300                    end = split;
10301                }
10302                let k = end - pos;
10303                let mut hid = vec![0f32; k * hs];
10304                for (j, &id) in ids[pos..end].iter().enumerate() {
10305                    hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10306                }
10307                let positions: Vec<usize> = (pos..end).collect();
10308                let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10309                if outcome != crate::gpu::BatchGraphOutcome::Completed {
10310                    return Err(format!(
10311                        "prefill parity: batch graph {outcome:?} at position {pos}"
10312                    ));
10313                }
10314                rows[pos * hs..end * hs].copy_from_slice(&hid);
10315                pos = end;
10316                if pos == split {
10317                    *at_split = Some(graph_states(p));
10318                }
10319            }
10320            Ok(())
10321        };
10322        let score = |p: &Self, rows: &[f32], from: usize| p.parity_score(ids, rows, from);
10323
10324        let t = std::time::Instant::now();
10325        self.clear_sequence_state();
10326        let (mut hh, mut h_split) = (vec![0f32; n * hs], None);
10327        host_walk(self, 0, n, &mut hh, &mut h_split);
10328        let (h_end, h_kv) = (host_states(self), host_kv(self));
10329        let t_h = t.elapsed().as_secs_f64();
10330
10331        let t = std::time::Instant::now();
10332        self.clear_sequence_state();
10333        let (mut hg, mut g_split) = (vec![0f32; n * hs], None);
10334        let g_run = graph_walk(self, 0, n, &mut hg, &mut g_split);
10335        let (g_end, g_kv) = (graph_states(self), graph_kv(self));
10336        let t_g = t.elapsed().as_secs_f64();
10337
10338        let t = std::time::Instant::now();
10339        self.clear_sequence_state();
10340        let (mut hm, mut m_split) = (vec![0f32; n * hs], None);
10341        host_walk(self, 0, split, &mut hm, &mut m_split);
10342        let m_run = graph_walk(self, split, n, &mut hm, &mut None);
10343        let (m_end, m_kv) = (graph_states(self), graph_kv(self));
10344        let t_m = t.elapsed().as_secs_f64();
10345        self.clear_sequence_state();
10346        g_run?;
10347        m_run?;
10348
10349        let (sh, sg, sm) = (
10350            score(self, &hh, 0),
10351            score(self, &hg, 0),
10352            score(self, &hm, split),
10353        );
10354        let mut r = String::new();
10355        let _ = writeln!(
10356            r,
10357            "prefill parity: {n} tokens, split {split}, host chunk {host_chunk}, graph k {bk} \
10358             (host {t_h:.1} s, graph {t_g:.1} s, host+graph {t_m:.1} s)"
10359        );
10360        let _ = writeln!(
10361            r,
10362            "  bin          | NLL host  graph  h+g   | top1 host=graph h+g=graph | hidden rel host:graph h+g:graph"
10363        );
10364        let bin = 256usize;
10365        let mut b0 = 0;
10366        while b0 + 1 < n {
10367            let b1 = (b0 + bin).min(n - 1);
10368            let mean = |s: &[(f64, usize)], off: usize| -> Option<f64> {
10369                let v: Vec<f64> = (b0.max(off)..b1).map(|p| s[p - off].0).collect();
10370                (!v.is_empty()).then(|| v.iter().sum::<f64>() / v.len() as f64)
10371            };
10372            let agree = |a: &[(f64, usize)], ao: usize, b: &[(f64, usize)], bo: usize| {
10373                let ps: Vec<usize> = (b0.max(ao).max(bo)..b1).collect();
10374                if ps.is_empty() {
10375                    return None;
10376                }
10377                let same = ps.iter().filter(|&&p| a[p - ao].1 == b[p - bo].1).count();
10378                Some(100.0 * same as f64 / ps.len() as f64)
10379            };
10380            let hid_rel = |a: &[f32], from: usize| -> Option<f64> {
10381                (b0.max(from)..b1)
10382                    .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &hg[p * hs..(p + 1) * hs]))
10383                    .reduce(f64::max)
10384            };
10385            let f =
10386                |v: Option<f64>, prec: usize| v.map_or("-".to_string(), |x| format!("{x:.prec$}"));
10387            let e = |v: Option<f64>| v.map_or("-".to_string(), |x| format!("{x:.1e}"));
10388            let _ = writeln!(
10389                r,
10390                "  {b0:5}..{b1:<5} | {:>8} {:>6} {:>6} | {:>14} {:>9} | {:>16} {:>9}",
10391                f(mean(&sh, 0), 3),
10392                f(mean(&sg, 0), 3),
10393                f(mean(&sm, split), 3),
10394                f(agree(&sh, 0, &sg, 0), 1),
10395                f(agree(&sm, split, &sg, 0), 1),
10396                e(hid_rel(&hh, 0)),
10397                e(hid_rel(&hm, split)),
10398            );
10399            b0 = b1;
10400        }
10401        // Recurrent state, layer by layer: [conv ring | S] split at the ring.
10402        let ring_len = self
10403            .gdn_cfg
10404            .map(|c| c.conv_dim() * c.conv_kernel.saturating_sub(1))
10405            .unwrap_or(0);
10406        let state_line = |r: &mut String, label: &str, a: &[Vec<f32>], b: &[Vec<f32>]| {
10407            let _ = write!(r, "  GDN {label} (ring/S rel):");
10408            let mut first = None;
10409            for li in (0..kinds.len()).filter(|&li| kinds[li] == 1) {
10410                let (x, y) = (&a[li], &b[li]);
10411                if x.len() != y.len() || x.len() <= ring_len {
10412                    let _ = write!(r, " L{li} len {}/{}", x.len(), y.len());
10413                    continue;
10414                }
10415                let (rr, rs) = (
10416                    rel_l2(&x[..ring_len], &y[..ring_len]),
10417                    rel_l2(&x[ring_len..], &y[ring_len..]),
10418                );
10419                if first.is_none() && rr.max(rs) > 1e-2 {
10420                    first = Some(li);
10421                }
10422                let _ = write!(r, " L{li} {rr:.0e}/{rs:.0e}");
10423            }
10424            let _ = writeln!(
10425                r,
10426                "\n    first GDN layer past 1e-2: {}",
10427                first.map_or("none".to_string(), |l| l.to_string())
10428            );
10429        };
10430        match (&h_split, &g_split) {
10431            (Some(a), Some(b)) => state_line(&mut r, "host vs graph at split", a, b),
10432            _ => {
10433                let _ = writeln!(r, "  GDN at split: not captured");
10434            }
10435        }
10436        if let (Some(a), Some(b)) = (&m_split, &h_split) {
10437            state_line(&mut r, "h+g host part vs host at split", a, b);
10438        }
10439        state_line(&mut r, "host vs graph at end", &h_end, &g_end);
10440        state_line(&mut r, "h+g vs graph at end", &m_end, &g_end);
10441        // Exact-attention K/V: the worst 256-row bin per layer and the first
10442        // bin past 1e-2.
10443        let kv_line = |r: &mut String, label: &str, a: &[(Vec<f32>, Vec<f32>)], from: usize| {
10444            let _ = writeln!(r, "  K/V {label} (worst bin K/V rel, first bin past 1e-2):");
10445            for li in (0..kinds.len()).filter(|&li| kinds[li] == 2) {
10446                let ((ak, av), (gk, gv)) = (&a[li], &g_kv[li]);
10447                if ak.len() != gk.len() || ak.is_empty() {
10448                    let _ = writeln!(r, "    L{li}: rows {}/{}", ak.len(), gk.len());
10449                    continue;
10450                }
10451                let row = ak.len() / n;
10452                let (mut wk, mut wv, mut first) = (0f64, 0f64, None);
10453                let mut b0 = from;
10454                while b0 < n {
10455                    let b1 = (b0 + bin).min(n);
10456                    let (s0, s1) = (b0 * row, b1 * row);
10457                    let (rk, rv) = (
10458                        rel_l2(&ak[s0..s1], &gk[s0..s1]),
10459                        rel_l2(&av[s0..s1], &gv[s0..s1]),
10460                    );
10461                    (wk, wv) = (wk.max(rk), wv.max(rv));
10462                    if first.is_none() && rk.max(rv) > 1e-2 {
10463                        first = Some(b0);
10464                    }
10465                    b0 = b1;
10466                }
10467                let _ = writeln!(
10468                    r,
10469                    "    L{li}: {wk:.1e}/{wv:.1e} first {}",
10470                    first.map_or("none".to_string(), |p| p.to_string())
10471                );
10472            }
10473        };
10474        kv_line(&mut r, "host vs graph", &h_kv, 0);
10475        kv_line(&mut r, "h+g vs graph", &m_kv, 0);
10476        Ok(r)
10477    }
10478
10479    /// (nll, top-1 id) of every position in [from, len − 1) from post-stack
10480    /// rows (`rows` indexed by absolute position), through the scorer's
10481    /// final norm and head.
10482    fn parity_score(&self, ids: &[u32], rows: &[f32], from: usize) -> Vec<(f64, usize)> {
10483        let (n, hs) = (ids.len(), self.hidden_size);
10484        let vrows = self.weights.lm_head.rows();
10485        let vs = self.vocab_size.min(vrows);
10486        let mut out = Vec::with_capacity(n.saturating_sub(from + 1));
10487        let mut pos = from;
10488        while pos + 1 < n {
10489            let end = (pos + 32).min(n - 1);
10490            let sb = end - pos;
10491            let mut normed = vec![0f32; sb * hs];
10492            for j in 0..sb {
10493                let r = inference::rms_norm(
10494                    &rows[(pos + j) * hs..(pos + j + 1) * hs],
10495                    &self.weights.final_norm,
10496                    self.rms_eps,
10497                    self.norm_style,
10498                );
10499                normed[j * hs..(j + 1) * hs].copy_from_slice(&r);
10500            }
10501            let mut logits = vec![0f32; sb * vrows];
10502            self.weights
10503                .lm_head
10504                .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10505            for j in 0..sb {
10506                let lg = &mut logits[j * vrows..j * vrows + vs];
10507                if let Some(mu) = self.logit_multiplier {
10508                    lg.iter_mut().for_each(|v| *v *= mu);
10509                }
10510                if let Some(c) = self.final_softcap {
10511                    softcap_in_place(lg, c, self.pool.as_deref());
10512                }
10513                let (mut max, mut top) = (f32::NEG_INFINITY, 0usize);
10514                for (i, &v) in lg.iter().enumerate() {
10515                    if v > max {
10516                        (max, top) = (v, i);
10517                    }
10518                }
10519                let lse = lg
10520                    .iter()
10521                    .map(|&v| ((v - max) as f64).exp())
10522                    .sum::<f64>()
10523                    .ln()
10524                    + max as f64;
10525                out.push((lse - lg[ids[pos + j + 1] as usize] as f64, top));
10526            }
10527            pos = end;
10528        }
10529        out
10530    }
10531
10532    /// Diagnostic (`CMF_PREFILL_PARITY_O1=k` on `cortiq ppl --o1 …
10533    /// --o1-prefill P`): how much the O(1) seal depends on where its exact
10534    /// prefix ran. Every arm continues on the batch graph from the boundary
10535    /// in k-row chunks:
10536    /// - OH: prefix on the host chunked prefill; seal from the host's
10537    ///   queries and K/V; recurrent state seeded from the host.
10538    /// - OG: prefix on the batch graph (recurrent state stays on the
10539    ///   device); seal from OH's host queries and host K/V.
10540    /// - OD: prefix on the batch graph; seal from OH's queries and the
10541    ///   graph's own K/V rows (read back).
10542    ///
10543    /// OH vs OG isolates the recurrent-state hand-off, OG vs OD the seal's
10544    /// response to the host/graph K/V difference. X is the exact graph.
10545    pub fn o1_seal_parity_probe(
10546        &mut self,
10547        ids: &[u32],
10548        cfg: crate::nystrom::O1Cfg,
10549        requested: usize,
10550        bk: usize,
10551    ) -> Result<String, String> {
10552        use std::fmt::Write as _;
10553        let (n, hs) = (ids.len(), self.hidden_size);
10554        self.set_o1(Some(cfg));
10555        let split = self
10556            .o1_effective_boundary(requested)
10557            .ok_or("o1 parity: no O(1) configuration")?;
10558        if split + 1 >= n || bk == 0 || !self.can_prefill_batched() {
10559            return Err(format!(
10560                "o1 parity: needs the boundary {split} inside {n} tokens, k > 0 and a batched host prefill"
10561            ));
10562        }
10563        let o1_layers: Vec<usize> = (0..self.num_layers)
10564            .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
10565            .collect();
10566        let graph_rows = |p: &mut Self, from: usize, to: usize, rows: &mut [f32]| {
10567            let mut pos = from;
10568            while pos < to {
10569                let end = (pos + bk).min(to);
10570                let k = end - pos;
10571                let mut hid = vec![0f32; k * hs];
10572                for (j, &id) in ids[pos..end].iter().enumerate() {
10573                    hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10574                }
10575                let positions: Vec<usize> = (pos..end).collect();
10576                let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10577                if outcome != crate::gpu::BatchGraphOutcome::Completed {
10578                    return Err(format!(
10579                        "o1 parity: batch graph {outcome:?} at position {pos}"
10580                    ));
10581                }
10582                rows[pos * hs..end * hs].copy_from_slice(&hid);
10583                pos = end;
10584            }
10585            Ok(())
10586        };
10587        // Seal every O(1) layer from the given rows; `trace` = (q, k, v) per
10588        // O(1) layer, position-major.
10589        let seal_from = |p: &mut Self, trace: &[(Vec<f32>, Vec<f32>, Vec<f32>)]| {
10590            for (&li, (q, k, v)) in o1_layers.iter().zip(trace) {
10591                let c = &mut p.kv_cache.layers[li];
10592                let (row, qrow) = (k.len() / split, q.len() / split);
10593                for pos in 0..split {
10594                    c.append(
10595                        &k[pos * row..(pos + 1) * row],
10596                        &v[pos * row..(pos + 1) * row],
10597                        &[],
10598                    );
10599                    c.o1_push_q(&q[pos * qrow..(pos + 1) * qrow]);
10600                }
10601            }
10602            p.o1_seal_checked()
10603        };
10604        // Greedy continuation (`CMF_PARITY_GREEDY` tokens, default 40) from
10605        // the last prompt row, one batch-graph row per step.
10606        let steps: usize = std::env::var("CMF_PARITY_GREEDY")
10607            .ok()
10608            .and_then(|v| v.parse().ok())
10609            .unwrap_or(40);
10610        // `CMF_PARITY_DECODE=token`: the steps through `decode_step_logits`
10611        // (the token graph generation decodes on) instead of the batch graph.
10612        let token_decode = std::env::var("CMF_PARITY_DECODE").as_deref() == Ok("token");
10613        let greedy = |p: &mut Self, rows: &[f32]| -> Result<Vec<u32>, String> {
10614            let argmax = |lg: &[f32], vs: usize| {
10615                (0..vs.min(lg.len())).fold(0, |b, i| if lg[i] > lg[b] { i } else { b }) as u32
10616            };
10617            let normed = inference::rms_norm(
10618                &rows[(n - 1) * hs..n * hs],
10619                &p.weights.final_norm,
10620                p.rms_eps,
10621                p.norm_style,
10622            );
10623            let mut out = vec![argmax(&p.lm_head_forward(&normed), p.vocab_size)];
10624            for s in 1..steps {
10625                let (tok, at) = (out[s - 1], n + s - 1);
10626                let lg = if token_decode {
10627                    p.decode_step_logits(tok, at)
10628                } else {
10629                    let mut hid = p.embed_single(tok);
10630                    let outcome = p.try_batch_graph_wgpu(&mut hid, &[at], 1, None);
10631                    if outcome != crate::gpu::BatchGraphOutcome::Completed {
10632                        return Err(format!("o1 parity: greedy step {s} {outcome:?}"));
10633                    }
10634                    let normed =
10635                        inference::rms_norm(&hid, &p.weights.final_norm, p.rms_eps, p.norm_style);
10636                    p.lm_head_forward(&normed)
10637                };
10638                out.push(argmax(&lg, p.vocab_size));
10639            }
10640            Ok(out)
10641        };
10642
10643        // OW first, in a fresh process as `run` starts: the host prefix
10644        // exactly as generation runs it — boundary at `split`, the
10645        // pipeline's prefill chunk, the layers sealing after the chunk that
10646        // reaches the boundary (`CMF_O1_WALK_BOUNDARY=1`: that chunk walks
10647        // its rows one at a time, each layer sealing after the boundary row).
10648        self.clear_sequence_state();
10649        self.o1_begin_with_prefix(Some(requested));
10650        let walk_chunk = self.prefill_chunk().max(1);
10651        let walk_from = (split - 1) / walk_chunk * walk_chunk;
10652        let mut ow = vec![0f32; n * hs];
10653        let mut pos = 0;
10654        while pos < split {
10655            let end = (pos + walk_chunk).min(split);
10656            let hb = self.prefill_batch(&ids[pos..end], pos);
10657            ow[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10658            pos = end;
10659        }
10660        self.o1_seal_checked()?;
10661        let ow_run = graph_rows(self, split, n, &mut ow);
10662        let ow_text = ow_run.clone().and_then(|_| greedy(self, &ow));
10663
10664        // OH. Collect one row past the boundary's natural barrier so the
10665        // host walk does not seal inside the last chunk, copy the trace,
10666        // then seal at `split` exactly.
10667        let t = std::time::Instant::now();
10668        self.clear_sequence_state();
10669        self.o1_begin_with_prefix(Some(split + 1));
10670        let mut oh = vec![0f32; n * hs];
10671        let mut pos = 0;
10672        while pos < split {
10673            let end = (pos + 128).min(split);
10674            let hb = self.prefill_batch(&ids[pos..end], pos);
10675            oh[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10676            pos = end;
10677        }
10678        let mut host = Vec::with_capacity(o1_layers.len());
10679        for &li in &o1_layers {
10680            let c = &self.kv_cache.layers[li];
10681            let q = c
10682                .o1_q_trace()
10683                .ok_or("o1 parity: host walk sealed early")?
10684                .to_vec();
10685            let (nkv, hd) = (c.num_kv_heads, c.head_dim);
10686            let (mut k, mut v) = (vec![0f32; split * nkv * hd], vec![0f32; split * nkv * hd]);
10687            for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
10688                for p in 0..split {
10689                    let d = (p * nkv + h) * hd;
10690                    k[d..d + hd].copy_from_slice(&kh[p * hd..(p + 1) * hd]);
10691                    v[d..d + hd].copy_from_slice(&vh[p * hd..(p + 1) * hd]);
10692                }
10693            }
10694            host.push((q, k, v));
10695        }
10696        for &li in &o1_layers {
10697            self.kv_cache.layers[li].o1_set_boundary(split);
10698        }
10699        self.o1_seal_checked()?;
10700        let oh_run = graph_rows(self, split, n, &mut oh);
10701        let t_oh = t.elapsed().as_secs_f64();
10702        let oh_text = oh_run.clone().and_then(|_| greedy(self, &oh));
10703
10704        // OG: graph prefix, host seal inputs. `CMF_PARITY_PERTURB=eps`
10705        // scales every host K/V value by (1 ± eps), signs from a fixed
10706        // xorshift stream: the seal's sensitivity to rounding-size noise.
10707        self.clear_sequence_state();
10708        self.o1_begin_with_prefix(Some(requested));
10709        let mut og = vec![0f32; n * hs];
10710        graph_rows(self, 0, split, &mut og)?;
10711        let perturb: f32 = std::env::var("CMF_PARITY_PERTURB")
10712            .ok()
10713            .and_then(|v| v.parse().ok())
10714            .unwrap_or(0.0);
10715        if perturb > 0.0 {
10716            let mut seed = 0x9e37_79b9_7f4a_7c15u64;
10717            let noisy: Vec<_> = host
10718                .iter()
10719                .map(|(q, k, v)| {
10720                    let mut jitter = |x: &f32| {
10721                        seed ^= seed << 13;
10722                        seed ^= seed >> 7;
10723                        seed ^= seed << 17;
10724                        x * if seed & 1 == 0 {
10725                            1.0 + perturb
10726                        } else {
10727                            1.0 - perturb
10728                        }
10729                    };
10730                    let k: Vec<f32> = k.iter().map(&mut jitter).collect();
10731                    let v: Vec<f32> = v.iter().map(&mut jitter).collect();
10732                    (q.clone(), k, v)
10733                })
10734                .collect();
10735            seal_from(self, &noisy)?;
10736        } else {
10737            seal_from(self, &host)?;
10738        }
10739        let og_run = graph_rows(self, split, n, &mut og);
10740        let og_text = og_run.clone().and_then(|_| greedy(self, &og));
10741
10742        // OD: graph prefix, the graph's own K/V with the host queries.
10743        self.clear_sequence_state();
10744        self.o1_begin_with_prefix(Some(requested));
10745        let mut od = vec![0f32; n * hs];
10746        graph_rows(self, 0, split, &mut od)?;
10747        let mut dev = Vec::with_capacity(o1_layers.len());
10748        let mut kv_rel = Vec::with_capacity(o1_layers.len());
10749        for (&li, (q, hk, hv)) in o1_layers.iter().zip(&host) {
10750            let c = &self.kv_cache.layers[li];
10751            let (k, v) = crate::gpu::graph_kv_read_rows(
10752                self.graph_kv_id,
10753                &[(li, 0, split)],
10754                c.num_kv_heads,
10755                c.head_dim,
10756            )
10757            .and_then(|mut rows| rows.pop())
10758            .ok_or_else(|| format!("o1 parity: no graph K/V rows for layer {li}"))?;
10759            kv_rel.push((li, rel_l2(hk, &k), rel_l2(hv, &v)));
10760            dev.push((q.clone(), k, v));
10761        }
10762        seal_from(self, &dev)?;
10763        let od_run = graph_rows(self, split, n, &mut od);
10764        let od_text = od_run.clone().and_then(|_| greedy(self, &od));
10765
10766        // X: exact batch graph.
10767        self.clear_sequence_state();
10768        self.set_o1(None);
10769        let mut xx = vec![0f32; n * hs];
10770        let x_run = graph_rows(self, 0, n, &mut xx);
10771        let x_text = x_run.clone().and_then(|_| greedy(self, &xx));
10772        self.clear_sequence_state();
10773        oh_run?;
10774        ow_run?;
10775        og_run?;
10776        od_run?;
10777        x_run?;
10778
10779        let sx = self.parity_score(ids, &xx, split);
10780        let arms = [
10781            ("OH", self.parity_score(ids, &oh, split)),
10782            ("OG", self.parity_score(ids, &og, split)),
10783            ("OD", self.parity_score(ids, &od, split)),
10784            ("OW", self.parity_score(ids, &ow, split)),
10785        ];
10786        let mut r = String::new();
10787        let _ = writeln!(
10788            r,
10789            "o1 seal parity: {n} tokens, boundary {split}, graph k {bk} (OH {t_oh:.1} s), \
10790             OW chunk {walk_chunk}, boundary chunk {walk_from}..{split}"
10791        );
10792        let _ = write!(r, "  seal K/V rel host vs graph:");
10793        for (li, rk, rv) in &kv_rel {
10794            let _ = write!(r, " L{li} {rk:.0e}/{rv:.0e}");
10795        }
10796        // Post-stack prefix rows against the exact graph: the batched rows,
10797        // then the rows OW walks one at a time.
10798        let worst = |a: &[f32], p0: usize, p1: usize| {
10799            (p0..p1)
10800                .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &xx[p * hs..(p + 1) * hs]))
10801                .fold(0f64, f64::max)
10802        };
10803        let _ = writeln!(
10804            r,
10805            "\n  prefix rows rel vs exact graph, [0,{walk_from}) / [{walk_from},{split}): \
10806             OH {:.1e} / {:.1e}, OW {:.1e} / {:.1e}",
10807            worst(&oh, 0, walk_from),
10808            worst(&oh, walk_from, split),
10809            worst(&ow, 0, walk_from),
10810            worst(&ow, walk_from, split),
10811        );
10812        let _ = writeln!(
10813            r,
10814            "  bin          | NLL exact    OH     OG     OD     OW | top1=exact  OH    OG    OD    OW"
10815        );
10816        let mean = |s: &[(f64, usize)], b0: usize, b1: usize| {
10817            (b0..b1).map(|p| s[p - split].0).sum::<f64>() / (b1 - b0).max(1) as f64
10818        };
10819        let agree = |s: &[(f64, usize)], b0: usize, b1: usize| {
10820            let same = (b0..b1)
10821                .filter(|&p| s[p - split].1 == sx[p - split].1)
10822                .count();
10823            100.0 * same as f64 / (b1 - b0).max(1) as f64
10824        };
10825        let mut b0 = split;
10826        let mut total = [0f64; 5];
10827        while b0 + 1 < n {
10828            let b1 = ((b0 / 256 + 1) * 256).min(n - 1);
10829            let _ = writeln!(
10830                r,
10831                "  {b0:5}..{b1:<5} | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | {:>12.1} {:>5.1} {:>5.1} {:>5.1}",
10832                mean(&sx, b0, b1),
10833                mean(&arms[0].1, b0, b1),
10834                mean(&arms[1].1, b0, b1),
10835                mean(&arms[2].1, b0, b1),
10836                mean(&arms[3].1, b0, b1),
10837                agree(&arms[0].1, b0, b1),
10838                agree(&arms[1].1, b0, b1),
10839                agree(&arms[2].1, b0, b1),
10840                agree(&arms[3].1, b0, b1),
10841            );
10842            b0 = b1;
10843        }
10844        for (i, s) in std::iter::once(&sx)
10845            .chain(arms.iter().map(|a| &a.1))
10846            .enumerate()
10847        {
10848            total[i] = s.iter().map(|x| x.0).sum::<f64>() / s.len().max(1) as f64;
10849        }
10850        let _ = writeln!(
10851            r,
10852            "  all          | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | ppl ratio vs exact OH ×{:.3} OG ×{:.3} OD ×{:.3} OW ×{:.3}",
10853            total[0],
10854            total[1],
10855            total[2],
10856            total[3],
10857            total[4],
10858            (total[1] - total[0]).exp(),
10859            (total[2] - total[0]).exp(),
10860            (total[3] - total[0]).exp(),
10861            (total[4] - total[0]).exp(),
10862        );
10863        for (name, text) in [
10864            ("X ", x_text),
10865            ("OH", oh_text),
10866            ("OG", og_text),
10867            ("OD", od_text),
10868            ("OW", ow_text),
10869        ] {
10870            let s = text.map_or_else(|e| e, |t| self.tokenizer.decode(&t));
10871            let _ = writeln!(r, "  greedy {name}: {:?}", s);
10872        }
10873        Ok(r)
10874    }
10875
10876    fn nll_ids_inner(
10877        &mut self,
10878        ids: &[u32],
10879        start: usize,
10880        task_mask: Option<&TaskMask>,
10881    ) -> Result<(f64, usize), String> {
10882        self.nll_begin()?;
10883        // Block diffusion on Apple silicon: the Metal executor's causal path.
10884        #[cfg(target_os = "macos")]
10885        if task_mask.is_none() && self.is_block_diffusion() {
10886            if let Some(r) = self.clover_metal_nll(ids, start) {
10887                self.nll_end();
10888                return r;
10889            }
10890        }
10891        let result: Result<(f64, usize), String> = (|| {
10892            let mut nll = 0f64;
10893            let mut cnt = 0usize;
10894            // An unmasked quality run with the resident wgpu graph must score
10895            // the same stateful path used by generation.  The layer-major
10896            // GEMM prefill below is a valid CPU/GEMM oracle, but it seeds
10897            // neither the graph's device GDN state nor its device KV mirrors;
10898            // using it here would silently score a different execution.  Keep
10899            // masked scoring on the exact per-position path as before, and
10900            // let the serial arm below drive the graph-aware scorer.
10901            // Only native Metal has a fused graph lm_head contract.  Vulkan
10902            // and other graph backends may expose hidden state without the
10903            // optional logits side channel; preserve their established CPU
10904            // norm/head fallback instead of turning that valid route into a
10905            // hard missing-logits error.
10906            let (graph_quality, fused_head_quality) = nll_graph_policy(
10907                task_mask.is_none(),
10908                self.graph_prefill_preferred(),
10909                crate::gpu::q1_force(),
10910            );
10911            self.graph_head_required = fused_head_quality;
10912            self.graph_want_logits = fused_head_quality;
10913            #[cfg(target_os = "macos")]
10914            if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
10915                match self.nll_batch_metal(ids, start) {
10916                    MetalBatchNllOutcome::Completed(nll, count) => {
10917                        return Ok((nll, count));
10918                    }
10919                    MetalBatchNllOutcome::Declined => {}
10920                    MetalBatchNllOutcome::Failed(err) => return Err(err),
10921                }
10922            }
10923            // CMF_NLL_SERIAL=1 scores position by position through the
10924            // decode path (the wgpu token graph where it admits the model)
10925            // — the perplexity gate of a decode kernel, which the batched
10926            // prefill arm below never runs.
10927            let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
10928            if self.can_prefill_batched() && !graph_quality && !force_serial {
10929                // prefill-GEMM: layer-major position chunks, lm_head batched
10930                // (254MB lm_head read once per chunk, not per position).
10931                // The layer chunk is large (grouping positions by MoE experts
10932                // wins with size), lm_head in sub-blocks (logit buffer
10933                // 32×vocab ≈ 32MB instead of 128×).
10934                // CMF_NLL_CHUNK: positions per prefill step (default 128).
10935                // A device scorer pays one round trip per projection per
10936                // step, so a 512-position step quarters them.
10937                let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
10938                    .ok()
10939                    .and_then(|v| v.parse().ok())
10940                    .filter(|&c: &usize| c > 0)
10941                    .unwrap_or(128);
10942                const LM_SUB: usize = 32;
10943                let n = ids.len().saturating_sub(1);
10944                let hs = self.hidden_size;
10945                let rows = self.weights.lm_head.rows();
10946                let mut pos = 0usize;
10947                let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
10948                while pos < n {
10949                    let end = (pos + chunk_len).min(n);
10950                    let bsz = end - pos;
10951                    let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
10952                    self.nll_check_graph("batched prefill", pos)?;
10953                    if state_trace && end % 256 == 0 {
10954                        self.trace_recurrent_state(end);
10955                    }
10956                    let mut k0 = 0usize;
10957                    while k0 < bsz {
10958                        let k1 = (k0 + LM_SUB).min(bsz);
10959                        let sb = k1 - k0;
10960                        // Sub-block entirely below the scored range: the KV
10961                        // it just built is all this pass needed from it.
10962                        if pos + k1 <= start {
10963                            k0 = k1;
10964                            continue;
10965                        }
10966                        let mut normed = vec![0.0f32; sb * hs];
10967                        for k in 0..sb {
10968                            let r = inference::rms_norm(
10969                                &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
10970                                &self.weights.final_norm,
10971                                self.rms_eps,
10972                                self.norm_style,
10973                            );
10974                            normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
10975                        }
10976                        let mut logits = vec![0.0f32; sb * rows];
10977                        self.weights
10978                            .lm_head
10979                            .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10980                        for k in 0..sb {
10981                            if pos + k0 + k < start {
10982                                continue;
10983                            }
10984                            self.nll_check_graph("batched score row", pos + k0 + k)?;
10985                            let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
10986                            if let Some(mu) = self.logit_multiplier {
10987                                for v in lg.iter_mut() {
10988                                    *v *= mu;
10989                                }
10990                            }
10991                            // Gemma-class final-logit soft-capping: the
10992                            // decode paths apply it; scoring must too, or
10993                            // the uncapped softmax misprices every token.
10994                            if let Some(c) = self.final_softcap {
10995                                softcap_in_place(lg, c, self.pool.as_deref());
10996                            }
10997                            // Cortiq Embryo hierarchical head: same correction
10998                            // the decode path applies (lm_head_forward).
10999                            if let Some(cm) = self.head_clusters.clone() {
11000                                self.hierarchical_head_logprobs(
11001                                    &normed[k * hs..(k + 1) * hs],
11002                                    &cm,
11003                                    lg,
11004                                );
11005                            }
11006                            let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
11007                            let target = ids[pos + k0 + k + 1] as usize;
11008                            let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11009                            let lse: f64 = lg
11010                                .iter()
11011                                .map(|&v| ((v - max) as f64).exp())
11012                                .sum::<f64>()
11013                                .ln()
11014                                + max as f64;
11015                            nll += lse - lg[target] as f64;
11016                            cnt += 1;
11017                            if std::env::var("CMF_PPL_TRACE").is_ok() {
11018                                let top = lg
11019                                    .iter()
11020                                    .enumerate()
11021                                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11022                                    .map(|(i, _)| i)
11023                                    .unwrap_or(0);
11024                                eprintln!(
11025                                    "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
11026                                    pos + k0 + k,
11027                                    target,
11028                                    lse - lg[target] as f64,
11029                                    top,
11030                                    lg[target],
11031                                    lg[top]
11032                                );
11033                            }
11034                        }
11035                        k0 = k1;
11036                    }
11037                    pos = end;
11038                }
11039                return Ok((nll, cnt));
11040            }
11041            for pos in 0..ids.len().saturating_sub(1) {
11042                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
11043                self.nll_check_graph("serial forward", pos)?;
11044                // Architectures whose head lives inside their own stack return
11045                // the logits out of band and a zero hidden — DeepSeek-V4 folds
11046                // its hyper-connection copies between the last layer and the
11047                // norm, so it cannot hand back a vector this loop could use.
11048                // Scoring the zeros gave a perplexity of exactly the vocabulary
11049                // size, which is a uniform distribution reported as a
11050                // measurement. `generate` already reads this channel.
11051                let out_of_band = self.graph_logits.take();
11052                if self.graph_head_required && out_of_band.is_none() {
11053                    METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
11054                    return Err(format!(
11055                        "fused Metal graph head did not complete at NLL position {pos}"
11056                    ));
11057                }
11058                if pos < start {
11059                    continue;
11060                }
11061                let logits = match out_of_band {
11062                    Some(lg) => lg,
11063                    None => {
11064                        let normed = inference::rms_norm(
11065                            &hidden,
11066                            &self.weights.final_norm,
11067                            self.rms_eps,
11068                            self.norm_style,
11069                        );
11070                        // lm_head_forward applies the final-logit softcap itself
11071                        // — capping again here double-squashed gemma-class
11072                        // logits (tanh∘tanh) and reported a flattered ppl.
11073                        self.lm_head_forward(&normed)
11074                    }
11075                };
11076                let target = ids[pos + 1] as usize;
11077                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11078                let lse: f64 = logits
11079                    .iter()
11080                    .map(|&v| ((v - max) as f64).exp())
11081                    .sum::<f64>()
11082                    .ln()
11083                    + max as f64;
11084                let tok_nll = lse - logits[target] as f64;
11085                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11086                    let top = logits
11087                        .iter()
11088                        .enumerate()
11089                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11090                        .map(|(i, _)| i)
11091                        .unwrap_or(0);
11092                    eprintln!(
11093                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11094                        logits[target], logits[top]
11095                    );
11096                }
11097                nll += tok_nll;
11098                cnt += 1;
11099            }
11100            Ok((nll, cnt))
11101        })();
11102        self.nll_end();
11103        result
11104    }
11105
11106    /// Score one post-layer hidden with the same final norm/head path used by
11107    /// decode. Keeping this in one helper is important for the production
11108    /// batch scorer: its rows stop before the final norm, just like the
11109    /// per-position O(1) path below.
11110    fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
11111        let normed = inference::rms_norm(
11112            hidden,
11113            &self.weights.final_norm,
11114            self.rms_eps,
11115            self.norm_style,
11116        );
11117        // lm_head_forward applies the final-logit softcap itself — capping
11118        // again here double-squashed gemma-class logits in earlier scorers.
11119        let mut logits = self.lm_head_forward(&normed);
11120        let target = target as usize;
11121        let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11122        let lse: f64 = logits
11123            .iter()
11124            .map(|&v| ((v - max) as f64).exp())
11125            .sum::<f64>()
11126            .ln()
11127            + max as f64;
11128        let tok_nll = lse - logits[target] as f64;
11129        if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11130            let top = logits
11131                .iter()
11132                .enumerate()
11133                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11134                .map(|(i, _)| i)
11135                .unwrap_or(0);
11136            eprintln!(
11137                "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11138                logits[target], logits[top]
11139            );
11140        }
11141        attention::recycle_buf(&mut logits);
11142        tok_nll
11143    }
11144
11145    /// `CMF_STATE_TRACE`: per-layer magnitude of the recurrent record at
11146    /// position `pos` — the whole `linear_state` (vmf: S then the conv
11147    /// ring; GDN: conv ring then S), its recurrent S part alone, the
11148    /// bounded ring, and the last per-position KV row count.  The tool
11149    /// that separated the ~4k perplexity cliff of the 500-step exports
11150    /// (a state that keeps climbing past the trained window) from a
11151    /// runtime boundary; one line per layer, `STATE pos=… layer=…`.
11152    fn trace_recurrent_state(&self, pos: usize) {
11153        let stats = |v: &[f32]| -> (f64, f64) {
11154            if v.is_empty() {
11155                return (0.0, 0.0);
11156            }
11157            let (mut ss, mut mx) = (0f64, 0f64);
11158            for &x in v {
11159                ss += (x as f64) * (x as f64);
11160                mx = mx.max((x as f64).abs());
11161            }
11162            ((ss / v.len() as f64).sqrt(), mx)
11163        };
11164        for (li, l) in self.kv_cache.layers.iter().enumerate() {
11165            let lw = &self.weights.layers[self.phys_layer(li)];
11166            let (kind, s_len) = match &lw.attn {
11167                AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
11168                AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
11169                AttnKind::Bounded(_) => ("bounded", 0),
11170                AttnKind::Full { .. } => ("full", 0),
11171                _ => ("other", 0),
11172            };
11173            let (rms, max) = stats(&l.linear_state);
11174            let s_part = if kind == "vmf" {
11175                &l.linear_state[..s_len.min(l.linear_state.len())]
11176            } else if kind == "gdn" {
11177                let ring = l
11178                    .linear_state
11179                    .len()
11180                    .saturating_sub(s_len)
11181                    .min(l.linear_state.len());
11182                &l.linear_state[ring..]
11183            } else {
11184                &l.linear_state[..0]
11185            };
11186            let (s_rms, s_max) = stats(s_part);
11187            let (ring_rms, ring_len) = match &l.bounded {
11188                Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
11189                None => (0.0, 0),
11190            };
11191            eprintln!(
11192                "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
11193                 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
11194                l.linear_state.len(),
11195                l.seq_len
11196            );
11197        }
11198    }
11199
11200    /// Teacher-forced NLL of the CONVERTED model: the O(1) Nyström path
11201    /// is ACTIVE over the scored positions. Returns `Ok((nll sum, scored
11202    /// count))` over `prefill..len-1` and surfaces a post-mutation batch
11203    /// failure instead of returning a partial score.
11204    ///
11205    /// Runtime discipline, deliberately NOT the matrix probe's: the
11206    /// requested prefix plus any required deferred lead-in run the exact
11207    /// prompt pass — that pass is what freezes the landmarks and M — and
11208    /// every post-seal scored position goes through `NystromState::step()`,
11209    /// the same code decode runs.
11210    /// So the landmarks are PREFILL-frozen (what ships), not
11211    /// full-sequence oracles (what the published probe measured). When the
11212    /// requested prefix is shorter than the bounded transition, rows in the
11213    /// exact lead-in are still scored so the shifted target range is stable.
11214    ///
11215    /// Pair with `nll_ids_from(ids, prefill)` for the exact baseline
11216    /// over the identical token set — that ratio is the honest one.
11217    pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
11218        // This scorer consumes host hiddens, so never request the optional
11219        // token-graph lm_head side channel. `nll_begin` also consumes a
11220        // prior graph failure and clears only the cancel bit that failure
11221        // raised, leaving a caller-owned cancellation observable.
11222        self.nll_begin()?;
11223        let requested_prefix = (prefill > 0).then_some(prefill);
11224        self.o1_begin_with_prefix(requested_prefix);
11225        let n = ids.len().saturating_sub(1);
11226        let requested_start = prefill.min(n);
11227        // The exact prefix must reach the deferred boundary before a
11228        // collecting layer can convert. Rows between the requested start and
11229        // that boundary remain part of the public NLL range and are scored
11230        // from the same hidden pass below.
11231        let exact_end = if self.o1_active() {
11232            match requested_prefix {
11233                Some(requested) => self.o1_effective_boundary(requested),
11234                None => self
11235                    .o1_cfg
11236                    .as_ref()
11237                    .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
11238            }
11239            .unwrap_or(requested_start)
11240            .min(n)
11241        } else {
11242            requested_start
11243        };
11244        let mut nll = 0f64;
11245        let mut cnt = 0usize;
11246
11247        // Exact prompt pass over ids[..exact_end]: the seal consumes its
11248        // q/k/v. Rows at or after requested_start are scored here when the
11249        // bounded lead-in is longer than the caller's requested prefix.
11250        let mut pos = 0usize;
11251        if self.can_prefill_batched() {
11252            const CHUNK: usize = 128;
11253            while pos < exact_end {
11254                let end = (pos + CHUNK).min(exact_end);
11255                let hiddens = self.prefill_batch(&ids[pos..end], pos);
11256                if self
11257                    .graph_failed
11258                    .swap(false, std::sync::atomic::Ordering::Relaxed)
11259                {
11260                    self.cancel
11261                        .store(false, std::sync::atomic::Ordering::Relaxed);
11262                    self.nll_end();
11263                    return Err("GPU graph failed during O(1) NLL prefix".into());
11264                }
11265                for row in 0..end - pos {
11266                    let score_pos = pos + row;
11267                    if score_pos >= requested_start && score_pos < n {
11268                        nll += self.nll_from_hidden(
11269                            &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
11270                            ids[score_pos + 1],
11271                            score_pos,
11272                        );
11273                        cnt += 1;
11274                    }
11275                }
11276                pos = end;
11277            }
11278        } else {
11279            while pos < exact_end {
11280                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11281                if self
11282                    .graph_failed
11283                    .swap(false, std::sync::atomic::Ordering::Relaxed)
11284                {
11285                    self.cancel
11286                        .store(false, std::sync::atomic::Ordering::Relaxed);
11287                    self.nll_end();
11288                    return Err("GPU graph failed during O(1) NLL prefix".into());
11289                }
11290                if pos >= requested_start && pos < n {
11291                    nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11292                    cnt += 1;
11293                }
11294                pos += 1;
11295            }
11296        }
11297        self.o1_seal_checked().map_err(|err| {
11298            self.nll_end();
11299            err
11300        })?;
11301
11302        // Reuse the production whole-token batch graph for the post-seal
11303        // suffix when the caller explicitly enabled both routes. This is a
11304        // teacher-forced scorer, so every row is ids[pos] and its target is
11305        // ids[pos + 1]; no speculative tail or rollback state is involved.
11306        // A first Declined is safe to handle with the established serial O(1)
11307        // path. Once a chunk completes, however, the device recurrent state
11308        // owns the sequence and a later decline must be terminal rather than
11309        // falling back to stale CPU state.
11310        let batch_k = std::env::var("CMF_BATCH_K")
11311            .ok()
11312            .and_then(|v| v.parse::<usize>().ok())
11313            .unwrap_or(0);
11314        let batch_admitted = batch_k > 0
11315            && self.can_prefill_batched()
11316            && self.o1_active()
11317            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
11318            && (0..self.num_layers).all(|li| {
11319                let cache = &self.kv_cache.layers[self.phys_layer(li)];
11320                cache.o1.is_none() || cache.o1_views().is_some()
11321            });
11322        if std::env::var("CMF_GRAPH_PROF").is_ok() {
11323            eprintln!(
11324                "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
11325                batch_admitted,
11326                batch_k,
11327                n.saturating_sub(exact_end),
11328            );
11329        }
11330        let mut batch_completed = false;
11331        if batch_admitted && exact_end < n {
11332            let hs = self.hidden_size;
11333            let mut batch_pos = exact_end;
11334            while batch_pos < n {
11335                let end = (batch_pos + batch_k).min(n);
11336                let bk = end - batch_pos;
11337                let mut hiddens = vec![0.0f32; bk * hs];
11338                for (row, &id) in ids[batch_pos..end].iter().enumerate() {
11339                    hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
11340                }
11341                let positions: Vec<usize> = (batch_pos..end).collect();
11342                let t_batch = std::time::Instant::now();
11343                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
11344                if std::env::var("CMF_GRAPH_PROF").is_ok() {
11345                    let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
11346                    eprintln!(
11347                        "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
11348                        batch_pos,
11349                        end.saturating_sub(1),
11350                        bk as f64 / (ms / 1000.0),
11351                    );
11352                }
11353                if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
11354                    self.nll_end();
11355                    return Err(err);
11356                }
11357                match outcome {
11358                    crate::gpu::BatchGraphOutcome::Completed => {
11359                        batch_completed = true;
11360                        for row in 0..bk {
11361                            nll += self.nll_from_hidden(
11362                                &hiddens[row * hs..(row + 1) * hs],
11363                                ids[batch_pos + row + 1],
11364                                batch_pos + row,
11365                            );
11366                            cnt += 1;
11367                        }
11368                        batch_pos = end;
11369                    }
11370                    crate::gpu::BatchGraphOutcome::Declined => {
11371                        if batch_completed {
11372                            self.nll_end();
11373                            return Err(format!(
11374                                "O(1) NLL batch declined after completed chunk at position {batch_pos}"
11375                            ));
11376                        }
11377                        break;
11378                    }
11379                    crate::gpu::BatchGraphOutcome::Failed => {
11380                        self.nll_end();
11381                        return Err(format!(
11382                            "O(1) NLL batch graph failed after admission at position {batch_pos}"
11383                        ));
11384                    }
11385                }
11386            }
11387            if batch_completed && cnt == n.saturating_sub(requested_start) {
11388                self.nll_end();
11389                return Ok((nll, cnt));
11390            }
11391        }
11392
11393        // Serial O(1) fallback/reference. It is intentionally retained when
11394        // batch admission declines before mutation; callers must label this
11395        // CMF_BATCH_K=0/per-position path separately from the production
11396        // whole-token batch route.
11397        for pos in exact_end..n {
11398            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11399            if self
11400                .graph_failed
11401                .swap(false, std::sync::atomic::Ordering::Relaxed)
11402            {
11403                self.cancel
11404                    .store(false, std::sync::atomic::Ordering::Relaxed);
11405                self.nll_end();
11406                return Err(format!(
11407                    "GPU graph failed during O(1) NLL serial scoring at position {pos}"
11408                ));
11409            }
11410            nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11411            cnt += 1;
11412        }
11413        self.nll_end();
11414        Ok((nll, cnt))
11415    }
11416
11417    /// Teacher-forced calibration data (B1): for each position, whether the
11418    /// argmax equals the actual next token, and the top-1 softmax prob
11419    /// (top-1 probability) under EACH temperature in `temps` — all from ONE forward
11420    /// pass (argmax/correctness are temperature-invariant; only p_max
11421    /// reshapes). Feeds `cortiq calibrate` (reliability/ECE + temperature
11422    /// fit): is the model's confidence a true property, or does it need a
11423    /// measured scaling?
11424    pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
11425        self.clear_sequence_state();
11426        let n = ids.len().saturating_sub(1);
11427        let mut correct = Vec::with_capacity(n);
11428        let mut pmax = Vec::with_capacity(n);
11429        for pos in 0..n {
11430            let emb = self.embed_single(ids[pos]);
11431            let hidden = self.forward_layers(&emb, pos, None);
11432            let logits = if let Some(logits) = self.graph_logits.take() {
11433                logits
11434            } else {
11435                let normed = inference::rms_norm(
11436                    &hidden,
11437                    &self.weights.final_norm,
11438                    self.rms_eps,
11439                    self.norm_style,
11440                );
11441                // lm_head_forward applies the final-logit softcap itself —
11442                // capping again here double-squashed gemma-class logits
11443                // (tanh∘tanh) and reported a flattered ppl.
11444                self.lm_head_forward(&normed)
11445            };
11446            let target = ids[pos + 1] as usize;
11447            let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
11448            for (i, &v) in logits.iter().enumerate() {
11449                if v > mval {
11450                    mval = v;
11451                    amax = i;
11452                }
11453            }
11454            correct.push(amax == target);
11455            let row: Vec<f32> = temps
11456                .iter()
11457                .map(|&t| {
11458                    let tt = t.max(1e-3);
11459                    let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
11460                    1.0 / s.max(1e-12) // numerator at the max is exp(0)=1
11461                })
11462                .collect();
11463            pmax.push(row);
11464        }
11465        self.clear_sequence_state();
11466        (correct, pmax)
11467    }
11468
11469    /// Teacher-forced PPL with the dynamic router driving per-window
11470    /// skill switches (VMF experiment №2 measurement). Sequential (φ
11471    /// must update per token), returns (ppl, switch_count). The router
11472    /// must be enabled (`enable_dynamic_routing`); else this equals
11473    /// plain `ppl_ids`. The active skill when scoring token t shapes the
11474    /// logits for t+1 — on-policy over the held-out text itself.
11475    pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
11476        if self.dyn_router.is_none() {
11477            return Ok((self.ppl_ids(ids)?, 0));
11478        }
11479        self.nll_begin()?;
11480        let saved_active = self.dyn_active;
11481        let mut router = self
11482            .dyn_router
11483            .take()
11484            .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
11485        router.reset();
11486        self.dyn_phi_seen = 0;
11487        let _ = self.set_active_skill(None);
11488
11489        let result: Result<(f64, usize), String> = (|| {
11490            let mut nll = 0f64;
11491            let mut cnt = 0usize;
11492            for pos in 0..ids.len().saturating_sub(1) {
11493                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11494                self.nll_check_graph("dynamic serial forward", pos)?;
11495                let out_of_band = self.graph_logits.take();
11496                let mut logits = match out_of_band {
11497                    Some(lg) => lg,
11498                    None => {
11499                        let normed = inference::rms_norm(
11500                            &hidden,
11501                            &self.weights.final_norm,
11502                            self.rms_eps,
11503                            self.norm_style,
11504                        );
11505                        // lm_head_forward applies the final-logit softcap itself —
11506                        // capping again here double-squashed gemma-class logits
11507                        // and reported a flattered ppl.
11508                        self.lm_head_forward(&normed)
11509                    }
11510                };
11511                let target = ids[pos + 1] as usize;
11512                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11513                let lse: f64 = logits
11514                    .iter()
11515                    .map(|&v| ((v - max) as f64).exp())
11516                    .sum::<f64>()
11517                    .ln()
11518                    + max as f64;
11519                let tok_nll = lse - logits[target] as f64;
11520                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11521                    let top = logits
11522                        .iter()
11523                        .enumerate()
11524                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11525                        .map(|(i, _)| i)
11526                        .unwrap_or(0);
11527                    eprintln!(
11528                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11529                        logits[target], logits[top]
11530                    );
11531                }
11532                nll += tok_nll;
11533                cnt += 1;
11534                attention::recycle_buf(&mut logits);
11535                // Route on the evolving phi (drives the NEXT token's skill).
11536                let phi = self.dyn_phi_ema.clone();
11537                if let Some(new_active) = router.step(&phi, pos) {
11538                    let _ = self.set_active_skill(new_active);
11539                }
11540            }
11541            Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
11542        })();
11543
11544        // Restore the detached router and the active overlay on both success
11545        // and failure. The scoring state is cleared independently below.
11546        let _ = self.set_active_skill(saved_active);
11547        self.dyn_router = Some(router);
11548        self.nll_end();
11549        result
11550    }
11551
11552    /// Routing probe φ (spec §9): mean-pooled hidden after `layer`.
11553    pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
11554        self.clear_sequence_state();
11555        let mut acc = vec![0f32; self.hidden_size];
11556        for (pos, &id) in ids.iter().enumerate() {
11557            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11558            for (a, v) in acc.iter_mut().zip(&h) {
11559                *a += v;
11560            }
11561        }
11562        let n = ids.len().max(1) as f32;
11563        for a in acc.iter_mut() {
11564            *a /= n;
11565        }
11566        self.clear_sequence_state();
11567        acc
11568    }
11569
11570    /// Router-v2 φ probe (spec §9.4, `phi.pool = "span_mean"`): the hidden
11571    /// AFTER `layer` — the same per-position walk and the same quantity as
11572    /// [`Self::probe_phi`] — averaged over the positions in `span` only
11573    /// (the user text between the template's prefix and suffix ids), NOT
11574    /// unit-normalized (the decision normalizes). The walk stops at
11575    /// `span.end`: causality makes the later positions irrelevant, so the
11576    /// result is bit-identical to probing `ids[..span.end]`. An empty span
11577    /// gives the zero vector, which the decision treats as degenerate.
11578    ///
11579    /// Every sequence state is reset before and after — the host KV/ring/
11580    /// recurrent state, the reuse keys (`kv_history`, `kv_prefix`) and the
11581    /// device graph's sequence — so run it on a pipeline that does not
11582    /// also serve a conversation (its prefix reuse would be lost).
11583    pub fn probe_phi_span(
11584        &mut self,
11585        ids: &[u32],
11586        layer: usize,
11587        span: std::ops::Range<usize>,
11588    ) -> Vec<f32> {
11589        #[cfg(target_os = "macos")]
11590        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11591        let end = span.end.min(ids.len());
11592        let start = span.start.min(end);
11593        let reset = |p: &mut Self| p.clear_sequence_state();
11594        reset(self);
11595        let mut acc = vec![0f32; self.hidden_size];
11596        for (pos, &id) in ids[..end].iter().enumerate() {
11597            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11598            if pos >= start {
11599                for (a, v) in acc.iter_mut().zip(&h) {
11600                    *a += v;
11601                }
11602            }
11603        }
11604        let n = end - start;
11605        if n > 0 {
11606            let n = n as f32;
11607            for a in acc.iter_mut() {
11608                *a /= n;
11609            }
11610        }
11611        reset(self);
11612        acc
11613    }
11614
11615    /// One decode step of the current sequence: forward `token` at
11616    /// `position` (the cache holds positions `[0, position)`, e.g. after
11617    /// [`Self::forward_ids`]) and return the next-token logits — the same
11618    /// forward and head the generation loop runs (resident-graph logits
11619    /// when the graph ran, final norm + lm_head otherwise). The logit-dump
11620    /// tools drive greedy decoding with it so every position is observable.
11621    pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
11622        #[cfg(target_os = "macos")]
11623        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11624        self.graph_logits = None;
11625        let hidden = self.forward_layers(&self.embed_single(token), position, None);
11626        if let Some(logits) = self.graph_logits.take() {
11627            return logits;
11628        }
11629        inference::rms_norm_into(
11630            &hidden,
11631            &self.weights.final_norm,
11632            self.rms_eps,
11633            self.norm_style,
11634            &mut self.ws.n1,
11635        );
11636        self.lm_head_forward(&self.ws.n1)
11637    }
11638
11639    /// Layer-major batched prefill (prefill-GEMM): full-attention —
11640    /// per-position with the existing operators (KV grows naturally,
11641    /// causality preserved), GDN projections / FFN / MoE — batched
11642    /// (a weight row is read from DRAM once per chunk, not per
11643    /// position). Returns the hidden of all positions [b × hidden].
11644    fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
11645        self.prefill_batch_masked(ids, start_pos, None)
11646    }
11647
11648    /// `prefill_batch` with a task mask honored on the dense-FFN panels
11649    /// (the masked-inference fast path: full fused compute, mask lands on
11650    /// the activations). The whole-chunk GPU graph is skipped for masked
11651    /// layers by the callers' arms; the per-GEMM device paths stay in
11652    /// play because the zeroing happens on the host between them.
11653    fn prefill_batch_masked(
11654        &mut self,
11655        ids: &[u32],
11656        start_pos: usize,
11657        task_mask: Option<&TaskMask>,
11658    ) -> Vec<f32> {
11659        self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
11660    }
11661
11662    /// One prompt chunk through the whole stack, post-stack rows out (no
11663    /// final norm) — the ingest generation uses, shared by scoring and
11664    /// `forward_ids` so they measure the same execution: the batched wgpu
11665    /// graph's device prefix plus the host's batched walk for the rest when
11666    /// `batch_prefix_prefill` holds and the graph admits the chunk, else
11667    /// the host's chunked prefill. Err only when a graph that had mutated
11668    /// device state failed.
11669    fn prefill_rows(
11670        &mut self,
11671        ids: &[u32],
11672        pos: usize,
11673        task_mask: Option<&TaskMask>,
11674    ) -> Result<Vec<f32>, String> {
11675        self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
11676    }
11677
11678    fn prefill_input_rows(
11679        &mut self,
11680        input: PrefillIn<'_>,
11681        pos: usize,
11682        task_mask: Option<&TaskMask>,
11683    ) -> Result<Vec<f32>, String> {
11684        self.mimo_moe_prepare();
11685        let hs = self.hidden_size;
11686        let bk = match input {
11687            PrefillIn::Ids(ids) => ids.len(),
11688            PrefillIn::Hidden(rows) => rows.len() / hs,
11689        };
11690        #[cfg(not(target_os = "macos"))]
11691        if task_mask.is_none()
11692            && !self.o1_active()
11693            && bk > 1
11694            && (self.batch_prefix_prefill()
11695                // An embedded prompt (vision rows) on the graph route takes
11696                // the batched graph like an id prompt does.
11697                || (matches!(input, PrefillIn::Hidden(_)) && self.generation_batch_k() > 0)
11698                || (self.verify_exact_moe
11699                    && crate::gpu::enabled_here()
11700                    && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
11701        {
11702            let mut hiddens = match input {
11703                PrefillIn::Hidden(rows) => rows.to_vec(),
11704                PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
11705            };
11706            let positions: Vec<usize> = (pos..pos + bk).collect();
11707            let mut run = 0usize;
11708            match self.try_batch_graph_wgpu_prefix(
11709                &mut hiddens,
11710                &positions,
11711                bk,
11712                None,
11713                Some(&mut run),
11714            ) {
11715                crate::gpu::BatchGraphOutcome::Completed => {
11716                    let out = if run < self.num_layers {
11717                        self.prefill_batch_span(
11718                            PrefillIn::Hidden(&hiddens),
11719                            pos,
11720                            None,
11721                            run,
11722                            self.num_layers,
11723                        )
11724                    } else {
11725                        hiddens
11726                    };
11727                    return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11728                        Err("MiMo attention graph failed after admission".into())
11729                    } else {
11730                        Ok(out)
11731                    };
11732                }
11733                crate::gpu::BatchGraphOutcome::Failed => {
11734                    return Err("batched prefix prefill failed after admission".into());
11735                }
11736                crate::gpu::BatchGraphOutcome::Declined => {
11737                    // Rows an earlier chunk left on the device only.
11738                    #[cfg(feature = "gpu")]
11739                    self.pull_lagging_host_kv(0, self.num_layers, pos);
11740                }
11741            }
11742        }
11743        let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
11744        if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11745            Err("batch tail graph failed after admission".into())
11746        } else {
11747            Ok(out)
11748        }
11749    }
11750
11751    /// The layer-major batched walk over a layer span [from..upto_excl):
11752    /// the whole prefill machinery (chunk graph, batched attends, GEMM
11753    /// panels) for a PARTIAL stack — the network split's prefill rides
11754    /// the same canon as the local one. Input is token ids (embeds
11755    /// itself, coordinator side) or ready boundary hiddens (worker side).
11756    fn prefill_batch_span(
11757        &mut self,
11758        input: PrefillIn<'_>,
11759        start_pos: usize,
11760        task_mask: Option<&TaskMask>,
11761        from: usize,
11762        upto_excl: usize,
11763    ) -> Vec<f32> {
11764        let hs = self.hidden_size;
11765        let b = match input {
11766            PrefillIn::Ids(ids) => ids.len(),
11767            PrefillIn::Hidden(hb) => hb.len() / hs,
11768        };
11769        let upto_excl = upto_excl.min(self.num_layers);
11770        // The CPU embed is deferred: when the chunk graph takes the run
11771        // from layer 0 it gathers the embeddings on the device instead.
11772        // A hidden input is ready by definition.
11773        let mut h: Vec<f32>;
11774        let mut h_ready;
11775        match input {
11776            PrefillIn::Ids(_) => {
11777                h = vec![0.0; b * hs];
11778                h_ready = false;
11779            }
11780            PrefillIn::Hidden(hb) => {
11781                h = hb.to_vec();
11782                h_ready = true;
11783            }
11784        }
11785        let fill_h = |h: &mut Vec<f32>, me: &Self| {
11786            if let PrefillIn::Ids(ids) = input {
11787                for (bi, &id) in ids.iter().enumerate() {
11788                    let e = me.embed_single(id);
11789                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
11790                }
11791                if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11792                    if let Ok(t) = tp.parse::<usize>() {
11793                        if t >= start_pos && t < start_pos + ids.len() {
11794                            let bi = t - start_pos;
11795                            let row = &h[bi * hs..(bi + 1) * hs];
11796                            let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11797                            eprintln!(
11798                                "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
11799                                ids[bi],
11800                                row[0],
11801                                row[1],
11802                                ids.len(),
11803                                &ids[..ids.len().min(8)]
11804                            );
11805                        }
11806                    }
11807                }
11808            }
11809        };
11810        let (_nkv, _hd, _rd, eps) = (
11811            self.num_kv_heads,
11812            self.head_dim,
11813            self.rotary_dim,
11814            self.rms_eps,
11815        );
11816        let pool = self.pool.clone();
11817        let norm_style = self.norm_style;
11818        self.mimo_moe_prepare();
11819        let automatic_gpu_prefix = self.automatic_gpu_prefix();
11820
11821        #[cfg(target_os = "macos")]
11822        let mut chunk_skip_until = 0usize;
11823        // SenseNova-Loop: the chunk rows' mHC streams.
11824        let mut mhc_rs: Vec<f32> = Vec::new();
11825        for li in from..upto_excl {
11826            let _capacity_tail = automatic_gpu_prefix
11827                .filter(|&prefix| {
11828                    li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
11829                })
11830                .map(|_| crate::gpu::enter_cpu_scope());
11831            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU
11832            // Spark-X2.5: the layer's int8 matrix-unit GEMMs take the
11833            // `zi_mm` tile, bit-identical to the 64x64 kernel and about
11834            // three times its rate (`gpu_wgpu::zi_gemm_route`).
11835            #[cfg(feature = "gpu")]
11836            let _fast_gemm = self
11837                .proj_gate_sigmoid
11838                .then(crate::gpu::enter_prefill_fast_gemm);
11839            // GPU chunk graph (default-on under CMF_GPU=1): a run of
11840            // consecutive eligible layers for the whole chunk in ONE
11841            // Metal submission — norm, QKV, RoPE with fused mirror
11842            // append, causal attend, O, FFN, hidden device-resident
11843            // across the run. Any refusal falls through to the CPU path.
11844            #[cfg(target_os = "macos")]
11845            if task_mask.is_none() {
11846                if li < chunk_skip_until {
11847                    continue;
11848                }
11849                // Device-side embedding needs a q8_row embedding matrix;
11850                // with any other layout the CPU fills `h` first and the
11851                // graph starts from a ready hidden (refusing the whole
11852                // run over the embedding alone kept q4t models — the
11853                // whole Nanbeige/Bonsai class — on the CPU prefill).
11854                if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
11855                    fill_h(&mut h, self);
11856                    h_ready = true;
11857                }
11858                let ids_for_embed = match input {
11859                    PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
11860                    PrefillIn::Hidden(_) => None,
11861                };
11862                let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
11863                if end > li {
11864                    h_ready = true;
11865                    chunk_skip_until = end;
11866                    // Looped Transformer: the graph stopped at a loop
11867                    // boundary — apply final norm before the next iteration.
11868                    if self.is_loop_end(end - 1) && end < self.num_layers {
11869                        for bi in 0..b {
11870                            let normed = inference::rms_norm(
11871                                &h[bi * hs..(bi + 1) * hs],
11872                                &self.weights.final_norm,
11873                                eps,
11874                                norm_style,
11875                            );
11876                            h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11877                        }
11878                    }
11879                    continue;
11880                }
11881            }
11882            if !h_ready {
11883                fill_h(&mut h, self);
11884                h_ready = true;
11885            }
11886            if task_mask.is_none() && self.verify_exact_moe {
11887                let positions: Vec<_> = (start_pos..start_pos + b).collect();
11888                match self.mimo_graph_layer_rows(li, &mut h, &positions) {
11889                    crate::gpu::BatchGraphOutcome::Completed => continue,
11890                    crate::gpu::BatchGraphOutcome::Failed => return h,
11891                    crate::gpu::BatchGraphOutcome::Declined => {}
11892                }
11893            }
11894            #[cfg(feature = "gpu")]
11895            self.pull_lagging_host_kv(li, li + 1, start_pos);
11896            // SenseNova-Loop mHC: every row's streams route into `h`; the
11897            // attention below adds onto that branch, so its output is the
11898            // difference from the copy kept here.
11899            let mhc_attn = self.mhc_layer(li).then(|| {
11900                let m = self.mhc.as_deref().expect("mhc layer");
11901                let p = self.phys_layer(li);
11902                let n = m.s.streams * hs;
11903                mhc_rs.resize(b * n, 0.0);
11904                let coefs: Vec<_> = (0..b)
11905                    .map(|bi| {
11906                        let r = &mut mhc_rs[bi * n..(bi + 1) * n];
11907                        if m.enters(p) {
11908                            m.enter(r, &h[bi * hs..(bi + 1) * hs]);
11909                        }
11910                        m.route(p, false, r, &mut h[bi * hs..(bi + 1) * hs])
11911                    })
11912                    .collect();
11913                (h.clone(), coefs)
11914            });
11915            let lw = &self.weights.layers[self.phys_layer(li)];
11916            let t_attn = std::time::Instant::now();
11917            // ── attention ──
11918            match &lw.attn {
11919                AttnKind::Kda(w) => {
11920                    // Projections batched, recurrence sequential.
11921                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
11922                    let mut normed = vec![0.0f32; b * hs];
11923                    for bi in 0..b {
11924                        inference::rms_norm_into(
11925                            &h[bi * hs..(bi + 1) * hs],
11926                            &lw.input_norm,
11927                            eps,
11928                            norm_style,
11929                            &mut normed[bi * hs..(bi + 1) * hs],
11930                        );
11931                    }
11932                    let attn = crate::linear_core::kda_forward_batch(
11933                        &normed,
11934                        b,
11935                        w,
11936                        &cfg,
11937                        &mut self.kv_cache.layers[li].linear_state,
11938                        pool.as_deref(),
11939                    );
11940                    for (dst, &a) in h.iter_mut().zip(&attn) {
11941                        *dst += a;
11942                    }
11943                }
11944                AttnKind::LinearGdn(w) => {
11945                    // Projections batched, recurrence sequential.
11946                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
11947                    let mut normed = vec![0.0f32; b * hs];
11948                    for bi in 0..b {
11949                        let r = inference::rms_norm(
11950                            &h[bi * hs..(bi + 1) * hs],
11951                            &lw.input_norm,
11952                            eps,
11953                            norm_style,
11954                        );
11955                        normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11956                    }
11957                    let attn = crate::linear_core::gdn_forward_batch(
11958                        &normed,
11959                        b,
11960                        w,
11961                        &cfg,
11962                        &mut self.kv_cache.layers[li].linear_state,
11963                        pool.as_deref(),
11964                    );
11965                    for (dst, &a) in h.iter_mut().zip(&attn) {
11966                        *dst += a;
11967                    }
11968                }
11969                AttnKind::ShortConv(w) => {
11970                    // Projections batched over the chunk; the conv walks the
11971                    // contiguous positions in order (same ring as decode).
11972                    let cfg = self
11973                        .short_conv_cfg
11974                        .expect("short-conv layer without short_conv_cfg");
11975                    let mut normed = vec![0.0f32; b * hs];
11976                    for bi in 0..b {
11977                        inference::rms_norm_into(
11978                            &h[bi * hs..(bi + 1) * hs],
11979                            &lw.input_norm,
11980                            eps,
11981                            norm_style,
11982                            &mut normed[bi * hs..(bi + 1) * hs],
11983                        );
11984                    }
11985                    let mut attn = short_conv_forward_batch(
11986                        &normed,
11987                        b,
11988                        w,
11989                        &cfg,
11990                        &mut self.kv_cache.layers[li].linear_state,
11991                        pool.as_deref(),
11992                    );
11993                    // LFM3 sandwich: the mixer output is normalized before
11994                    // the residual, as on the decode path.
11995                    if let Some(wn) = &lw.attn_out_norm {
11996                        for bi in 0..b {
11997                            inference::rms_norm_into(
11998                                &attn[bi * hs..(bi + 1) * hs],
11999                                wn,
12000                                eps,
12001                                norm_style,
12002                                &mut normed[bi * hs..(bi + 1) * hs],
12003                            );
12004                        }
12005                        attn.copy_from_slice(&normed);
12006                    }
12007                    for (dst, &a) in h.iter_mut().zip(&attn) {
12008                        *dst += a;
12009                    }
12010                }
12011                AttnKind::Mla(w) => {
12012                    // Per-position prefill (correctness first; latent
12013                    // batching is a later optimization).
12014                    let inv_freq_l = self.layer_inv_freq(li);
12015                    let rs = self.layer_rope_scale(li);
12016                    let mut normed = vec![0.0f32; hs];
12017                    for bi in 0..b {
12018                        inference::rms_norm_into(
12019                            &h[bi * hs..(bi + 1) * hs],
12020                            &lw.input_norm,
12021                            eps,
12022                            norm_style,
12023                            &mut normed,
12024                        );
12025                        let ao = mla_attention(
12026                            w,
12027                            &normed,
12028                            &mut self.kv_cache.layers[li],
12029                            start_pos + bi,
12030                            &inv_freq_l,
12031                            rs,
12032                            eps,
12033                            pool.as_deref(),
12034                        );
12035                        for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
12036                            *dst += a;
12037                        }
12038                    }
12039                }
12040                AttnKind::Full {
12041                    wq,
12042                    wk,
12043                    wv,
12044                    wo,
12045                    q_norm,
12046                    k_norm,
12047                    output_gate,
12048                    softplus_gate,
12049                    bias,
12050                } => {
12051                    // Chunk-GEMM QKV/O; per-position causal attention
12052                    // inside (roadmap §3 P0 — full-attention prefill no
12053                    // longer re-reads the projection weights b times).
12054                    let mut normed = vec![0.0f32; b * hs];
12055                    for bi in 0..b {
12056                        inference::rms_norm_into(
12057                            &h[bi * hs..(bi + 1) * hs],
12058                            &lw.input_norm,
12059                            eps,
12060                            norm_style,
12061                            &mut normed[bi * hs..(bi + 1) * hs],
12062                        );
12063                    }
12064                    let inv_freq_l = self.layer_inv_freq(li);
12065                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
12066                    let cfg = QwenAttnCfg {
12067                        mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
12068                        num_heads: self.layer_num_heads(li),
12069                        num_kv_heads: nkv_l,
12070                        head_dim: hd_l,
12071                        hidden_size: hs,
12072                        position: start_pos,
12073                        inv_freq: &inv_freq_l,
12074                        rotary_dim: rd_l,
12075                        scale: self.attn_scale,
12076                        softcap: self.attn_softcap,
12077                        window: self.layer_window(li),
12078                        v_norm: self.attn_v_norm,
12079                        qk_norm_after_rope: self.qk_norm_after_rope,
12080                        gate_sigmoid: self.proj_gate_sigmoid,
12081                        q_norm: q_norm.as_deref(),
12082                        k_norm: k_norm.as_deref(),
12083                        output_gate: *output_gate,
12084                        softplus_gate: softplus_gate
12085                            .as_ref()
12086                            .map(|(gate, per_head)| (gate, *per_head)),
12087                        rope_scale: self.layer_rope_scale(li),
12088                        bias: bias
12089                            .as_ref()
12090                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12091                        rms_eps: eps,
12092                        norm_style,
12093                        pool: pool.as_deref(),
12094                        v_head_dim: self.layer_v_dim(li),
12095                    };
12096                    #[cfg(feature = "gpu")]
12097                    let _mirror = self
12098                        .prefill_mirror_target(li)
12099                        .map(crate::gpu::enter_prefill_mirror);
12100                    let mut attn = attention::qwen_attention_batch(
12101                        &normed,
12102                        b,
12103                        wq,
12104                        wk,
12105                        wv,
12106                        wo,
12107                        &mut self.kv_cache.layers[li],
12108                        &cfg,
12109                    );
12110                    if let Some(w) = &lw.attn_out_norm {
12111                        for bi in 0..b {
12112                            inference::rms_norm_into(
12113                                &attn[bi * hs..(bi + 1) * hs],
12114                                w,
12115                                eps,
12116                                norm_style,
12117                                &mut normed[bi * hs..(bi + 1) * hs],
12118                            );
12119                        }
12120                        attn.copy_from_slice(&normed);
12121                    }
12122                    for (dst, &a) in h.iter_mut().zip(&attn) {
12123                        *dst += a;
12124                    }
12125                }
12126                AttnKind::Bounded(w) => {
12127                    // Chunk-GEMM projections, the bounded operator per
12128                    // position over ring + chunk — never a growing KV.
12129                    let mut normed = vec![0.0f32; b * hs];
12130                    for bi in 0..b {
12131                        inference::rms_norm_into(
12132                            &h[bi * hs..(bi + 1) * hs],
12133                            &lw.input_norm,
12134                            eps,
12135                            norm_style,
12136                            &mut normed[bi * hs..(bi + 1) * hs],
12137                        );
12138                    }
12139                    let rope = self
12140                        .bounded_rope
12141                        .clone()
12142                        .expect("bounded layer without an installed rotation table");
12143                    let cfg = crate::bounded::BoundedAttnCfg {
12144                        num_heads: self.num_heads,
12145                        num_kv_heads: self.num_kv_heads,
12146                        head_dim: self.head_dim,
12147                        hidden_size: hs,
12148                        scale: self.attn_scale,
12149                        rope: &rope,
12150                        pool: pool.as_deref(),
12151                    };
12152                    let mut attn = crate::bounded::bounded_attention_batch(
12153                        &normed,
12154                        b,
12155                        w,
12156                        &mut self.kv_cache.layers[li],
12157                        &cfg,
12158                    );
12159                    if let Some(wn) = &lw.attn_out_norm {
12160                        for bi in 0..b {
12161                            inference::rms_norm_into(
12162                                &attn[bi * hs..(bi + 1) * hs],
12163                                wn,
12164                                eps,
12165                                norm_style,
12166                                &mut normed[bi * hs..(bi + 1) * hs],
12167                            );
12168                        }
12169                        attn.copy_from_slice(&normed);
12170                    }
12171                    for (dst, &a) in h.iter_mut().zip(&attn) {
12172                        *dst += a;
12173                    }
12174                    attention::recycle_buf(&mut attn);
12175                }
12176                AttnKind::Linear(w) => {
12177                    for bi in 0..b {
12178                        let normed = inference::rms_norm(
12179                            &h[bi * hs..(bi + 1) * hs],
12180                            &lw.input_norm,
12181                            eps,
12182                            norm_style,
12183                        );
12184                        vmf_phase_forward(
12185                            &normed,
12186                            w,
12187                            &self.vmf_cfg.expect("linear layer without vmf_cfg"),
12188                            &mut self.kv_cache.layers[li].linear_state,
12189                            pool.as_deref(),
12190                        )
12191                        .iter()
12192                        .enumerate()
12193                        .for_each(|(i, &a)| h[bi * hs + i] += a);
12194                    }
12195                }
12196            }
12197
12198            // mHC: fold the attention output into the streams, route the
12199            // FFN's branch.
12200            let mhc_ffn = mhc_attn.map(|(branch, coefs)| {
12201                let m = self.mhc.as_deref().expect("mhc layer");
12202                let p = self.phys_layer(li);
12203                let n = m.s.streams * hs;
12204                let mut y = vec![0f32; hs];
12205                (0..b)
12206                    .map(|bi| {
12207                        for (e, v) in y.iter_mut().enumerate() {
12208                            *v = h[bi * hs + e] - branch[bi * hs + e];
12209                        }
12210                        let r = &mut mhc_rs[bi * n..(bi + 1) * n];
12211                        m.merge(r, &y, &coefs[bi]);
12212                        m.route(p, true, r, &mut h[bi * hs..(bi + 1) * hs])
12213                    })
12214                    .collect::<Vec<_>>()
12215            });
12216            // ── FFN batched ──
12217            let lw = &self.weights.layers[self.phys_layer(li)];
12218            let mut post = vec![0.0f32; b * hs];
12219            for bi in 0..b {
12220                let r =
12221                    inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
12222                post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
12223            }
12224            // A restrictive per-visit FFN row lands on the activations
12225            // inside the dense arm; an all-open row costs nothing.
12226            let mask_row = task_mask
12227                .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
12228                .and_then(|m| m.ffn_masks.get(li))
12229                .map(|v| v.as_slice());
12230            let attn_ns = t_attn.elapsed().as_nanos() as u64;
12231            let t_ffn = std::time::Instant::now();
12232            let mut ffn = match &lw.ffn {
12233                FfnKind::Dense(d) if !d.segs.is_empty() => {
12234                    tube_ffn(d, &post, b, pool.as_deref(), mask_row)
12235                }
12236                FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
12237                FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
12238                    moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
12239                }
12240                FfnKind::Moe(m) if self.verify_exact_moe => {
12241                    moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
12242                }
12243                // Keep prompt expert panels off the projection arena and
12244                // use their routes to prime the model-wide bank.
12245                FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
12246                    let before = m.stats.borrow().clone();
12247                    let out = crate::gpu::cpu_scope(|| {
12248                        moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
12249                    });
12250                    self.mimo_moe.prime(li, m, &before);
12251                    out
12252                }
12253                FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
12254                // Dual-branch layers run per position (the expert branch
12255                // reads the raw residual — nothing to batch yet).
12256                FfnKind::DenseMoe(dm) => {
12257                    let mut out = vec![0.0f32; b * hs];
12258                    for bi in 0..b {
12259                        let r = dense_moe_ffn(
12260                            dm,
12261                            &post[bi * hs..(bi + 1) * hs],
12262                            &h[bi * hs..(bi + 1) * hs],
12263                            eps,
12264                            norm_style,
12265                            pool.as_deref(),
12266                        );
12267                        out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
12268                    }
12269                    out
12270                }
12271            };
12272            if prefill_prof_on() {
12273                PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
12274                PREFILL_SPLIT[1].fetch_add(
12275                    t_ffn.elapsed().as_nanos() as u64,
12276                    std::sync::atomic::Ordering::Relaxed,
12277                );
12278            }
12279            if let Some(w) = &lw.ffn_out_norm {
12280                for bi in 0..b {
12281                    inference::rms_norm_into(
12282                        &ffn[bi * hs..(bi + 1) * hs],
12283                        w,
12284                        eps,
12285                        norm_style,
12286                        &mut post[bi * hs..(bi + 1) * hs],
12287                    );
12288                }
12289                ffn.copy_from_slice(&post);
12290            }
12291            if let Some(coefs) = mhc_ffn {
12292                // mHC: fold the FFN output in; the range's last layer hands
12293                // the streams' mean on as the hidden.
12294                let m = self.mhc.as_deref().expect("mhc layer");
12295                let p = self.phys_layer(li);
12296                let n = m.s.streams * hs;
12297                for bi in 0..b {
12298                    let r = &mut mhc_rs[bi * n..(bi + 1) * n];
12299                    m.merge(r, &ffn[bi * hs..(bi + 1) * hs], &coefs[bi]);
12300                    if m.leaves(p) {
12301                        m.leave(r, &mut h[bi * hs..(bi + 1) * hs]);
12302                    }
12303                }
12304            } else {
12305                for (dst, &f) in h.iter_mut().zip(&ffn) {
12306                    *dst += f;
12307                }
12308            }
12309            if let Some(sc) = lw.layer_scale {
12310                for v in h.iter_mut() {
12311                    *v *= sc;
12312                }
12313            }
12314            // CMF_LAYER_DUMP: every position's hidden after layer li.
12315            if self.layer_dump.is_some() {
12316                for bi in 0..b {
12317                    self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
12318                }
12319            }
12320            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
12321                if let Ok(t) = tp.parse::<usize>() {
12322                    if t >= start_pos && t < start_pos + b {
12323                        let bi = t - start_pos;
12324                        let row = &h[bi * hs..(bi + 1) * hs];
12325                        let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
12326                        eprintln!(
12327                            "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
12328                            row[0], row[1]
12329                        );
12330                    }
12331                }
12332            }
12333            // CMF_DEBUG_LAYERS=1: per-layer hidden-state health of the
12334            // LAST prompt position — the knife for "which layer type
12335            // breaks first" on a new architecture.
12336            if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
12337                let row = &h[(b - 1) * hs..b * hs];
12338                let rms =
12339                    (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
12340                let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
12341                eprintln!(
12342                    "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
12343                    match &self.weights.layers[self.phys_layer(li)].attn {
12344                        AttnKind::LinearGdn(_) => "gdn",
12345                        AttnKind::Linear(_) => "vmf",
12346                        AttnKind::ShortConv(_) => "conv",
12347                        _ => "attn",
12348                    },
12349                    match &lw.ffn {
12350                        FfnKind::Moe(_) => "moe",
12351                        FfnKind::Dense(_) => "dense",
12352                        FfnKind::DenseMoe(_) => "dense+moe",
12353                    },
12354                );
12355            }
12356            // Looped Transformer: apply final norm at the end of each loop iteration.
12357            if self.is_loop_end(li) && li + 1 < self.num_layers {
12358                for bi in 0..b {
12359                    let normed = inference::rms_norm(
12360                        &h[bi * hs..(bi + 1) * hs],
12361                        &self.weights.final_norm,
12362                        eps,
12363                        norm_style,
12364                    );
12365                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
12366                }
12367            }
12368            if std::env::var("CMF_TRACE_H").is_ok() {
12369                let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
12370                let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
12371                eprintln!(
12372                    "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
12373                    lw.layer_scale
12374                );
12375            }
12376        }
12377        crate::gpu::set_layer(-1); // lm_head/final ops outside layer-split
12378        if prefill_prof_on() {
12379            let ms = |a: &std::sync::atomic::AtomicU64| {
12380                a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
12381            };
12382            let sp = &attention::ATTN_SPLIT;
12383            let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
12384            eprintln!(
12385                "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
12386                 host gate {:.1}], host loop {:.1}, attend {:.1} \
12387                 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
12388                 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
12389                ms(&PREFILL_SPLIT[0]),
12390                ms(&sp[0]),
12391                qkv_enq,
12392                qkv_wait,
12393                ms(&sp[6]),
12394                ms(&sp[1]),
12395                ms(&sp[2]),
12396                ms(&sp[4]),
12397                ms(&sp[5]),
12398                ca_wait,
12399                ms(&sp[3]),
12400                ms(&PREFILL_SPLIT[1]),
12401            );
12402            let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
12403            eprintln!(
12404                "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
12405                 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
12406                 fallbacks (cumulative)",
12407                attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
12408            );
12409        }
12410        // A batched span owns a complete set of positions. Publish any
12411        // collecting→sealed transition only after every layer has finished;
12412        // callers that cross into serial/device work must see the new epoch
12413        // before this function returns.
12414        self.o1_progress();
12415        // Every layer of the chunk has appended and attended its rows.
12416        self.swa_trim_tails();
12417        h
12418    }
12419
12420    /// Embed a single token.
12421    fn embed_single(&self, id: u32) -> Vec<f32> {
12422        let mut out = vec![0.0f32; self.hidden_size];
12423        if (id as usize) < self.weights.embed_tokens.rows() {
12424            self.weights.embed_tokens.row_f32(id as usize, &mut out);
12425        }
12426        if self.embed_multiplier != 1.0 {
12427            for v in out.iter_mut() {
12428                *v *= self.embed_multiplier;
12429            }
12430        }
12431        // DeepSeek-V4's hash layers route by TOKEN ID, so the id has to
12432        // reach the forward. It rides in slot 0 (the forward re-reads the
12433        // real embedding itself from the table).
12434        if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
12435            let mut v = vec![0.0f32; self.hidden_size.max(1)];
12436            v[0] = id as f32;
12437            return v;
12438        }
12439        // Gemma-3n: the per-layer-embedding half needs the token ID, so
12440        // it rides appended to the embedding; the g3n forward splits it.
12441        // Gemma-4 E-series PLE: same contract, its own table.
12442        if let Some(b) = &self.g4e {
12443            return b.0.extend_embedding(id, &out, self.pool.as_deref());
12444        }
12445        if let Some(b) = &self.g3n {
12446            return b.0.extend_embedding(id, &out, self.pool.as_deref());
12447        }
12448        out
12449    }
12450
12451    /// A run of consecutive prefill layers on the GPU for the whole
12452    /// chunk (default-on under CMF_GPU=1; CMF_GPU_CHUNK=0 disables).
12453    /// Eligibility per layer: q8_row weights, plain full attention
12454    /// (no output gate), F32 KV, no o1/masks/gemma extras. Returns the
12455    /// first layer index NOT processed (== `li0` when the run is empty).
12456    #[cfg(target_os = "macos")]
12457    fn chunk_run_gpu(
12458        &mut self,
12459        li0: usize,
12460        h: &mut [f32],
12461        b: usize,
12462        pos0: usize,
12463        embed_ids: Option<&[u32]>,
12464        cap: usize,
12465    ) -> usize {
12466        // (The old streaming attend needed a depth bound at ~1k; the
12467        // GEMM attention scales like the CPU path and lifted it.)
12468        // CMF_GPU_CHUNK=0 disables the graph.
12469        // Gemma-4 (`metal_gemma4`) brings its own kernels for the global
12470        // layers, V norm, attention scale and sandwich (`ChunkGemma4`).
12471        // `CMF_METAL_GEMMA4_CHUNK=0` keeps its prompt on the host walk.
12472        let gemma4 =
12473            self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
12474        if !crate::gpu::enabled_here()
12475            || std::env::var("CMF_GPU_CHUNK")
12476                .map(|v| v == "0")
12477                .unwrap_or(false)
12478            || b < self.chunk_min_rows
12479            // MRoPE rows rotate on the host (`attention::MropeView`).
12480            || self.prompt_mrope.is_some()
12481            // Sliding windows with per-layer RoPE ride the chunk graph
12482            // (`metal_graph_swa`: causal_softmax_win, per-layer tables).
12483            || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
12484            || (self.global_attn.is_some() && !gemma4)
12485            // Chunk attention has no value-less learned-sink column. q1
12486            // decode can carry it through ordinary `gqa_attend`, but prompt
12487            // chunks must stay on the exact host attention path.
12488            || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
12489            // Per-layer KV heads and narrow V remain outside this chunk
12490            // graph (the q1 sliding-native door checks the same geometry).
12491            || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
12492            // Collection owns the exact Q trace and boundary conversion;
12493            // this chunk graph appends dense KV without feeding that trace.
12494            || self.o1_active()
12495            || (self.attn_v_norm && !gemma4)
12496            || !self.metal_rope_scale_ok()
12497            || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
12498        {
12499            return li0;
12500        }
12501        let Some(model) = self.model.clone() else {
12502            return li0;
12503        };
12504        let (nh, nkv, hd, hs) = (
12505            self.num_heads,
12506            self.num_kv_heads,
12507            self.head_dim,
12508            self.hidden_size,
12509        );
12510        // Collect the longest run of consecutive eligible layers.
12511        // Looped Transformer: stop at the loop boundary so the CPU can
12512        // apply loop_final_norm between iterations.
12513        let loop_end = if self.loop_final_norm {
12514            ((li0 / self.physical_layers) + 1) * self.physical_layers
12515        } else {
12516            self.num_layers
12517        };
12518        let run_end = self.num_layers.min(loop_end).min(cap);
12519        // Each layer's own RoPE table (Spark-X2.5: sliding and full layers
12520        // differ); the global one for every model without sliding layers.
12521        // Declared before `layers`, which borrows it (and drops a routing
12522        // closure on MoE layers).
12523        let tables: Vec<std::sync::Arc<Vec<f32>>> =
12524            (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
12525        let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
12526        let mut stored_at: Vec<usize> = Vec::new();
12527        for li in li0..run_end {
12528            if self.mhc_layer(li) {
12529                break;
12530            }
12531            let lw = &self.weights.layers[self.phys_layer(li)];
12532            if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
12533                && !gemma4
12534            {
12535                break;
12536            }
12537            let AttnKind::Full {
12538                wq,
12539                wk,
12540                wv,
12541                wo,
12542                q_norm,
12543                k_norm,
12544                output_gate: false,
12545                softplus_gate,
12546                bias,
12547            } = &lw.attn
12548            else {
12549                break;
12550            };
12551            // Spark-X2.5's per-head sigmoid g_proj gate, from f32 rows.
12552            let head_gate = match softplus_gate {
12553                None => None,
12554                Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
12555                    Some((d, r, c)) if r == nh && c == hs => Some(d),
12556                    _ => break,
12557                },
12558                Some(_) => break,
12559            };
12560            // The FFN: a dense SiLU/GELU trio, or routed q4tp experts
12561            // (Mellum2.1: softmax router, no shared expert) whose first
12562            // trio stands in for the shape checks.
12563            let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
12564                FfnKind::Dense(d)
12565                    if (matches!(d.act, Act::Silu | Act::Gelu)
12566                        || (gemma4 && d.act == Act::GeluTanh))
12567                        && d.segs.is_empty() =>
12568                {
12569                    (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
12570                }
12571                FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
12572                    Some((router, experts, q8)) => {
12573                        let e0 = &m.experts[0];
12574                        let route = Box::new(move |lg: &[f32]| {
12575                            // `moe_ffn_batch`'s routing and weights, row by row.
12576                            let (idx, p, wsum) = moe_route(lg, m, None);
12577                            let mut st = m.stats.borrow_mut();
12578                            if st.len() < m.experts.len() {
12579                                st.resize(m.experts.len(), 0);
12580                            }
12581                            for &e in &idx {
12582                                st[e] += 1;
12583                            }
12584                            let w = idx.iter().map(|&e| p[e] / wsum).collect();
12585                            (idx, w)
12586                        });
12587                        (
12588                            &e0.gate_proj,
12589                            &e0.up_proj,
12590                            &e0.down_proj,
12591                            Act::Silu,
12592                            Some(crate::gpu_metal::ChunkMoe {
12593                                router,
12594                                experts,
12595                                q8,
12596                                route,
12597                            }),
12598                        )
12599                    }
12600                    None => break,
12601                },
12602                _ => break,
12603            };
12604            // q8_row (row_scale populated), q8_2f (plus its input field),
12605            // or q4_tiled / q4tp (row_scale empty — their scales are in the
12606            // payload). Mixing across the seven projections of one layer is
12607            // fine; the encoder branches per weight on the tensor's dtype.
12608            // Anything else refuses.
12609            fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
12610                t.q8_row_parts()
12611                    .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
12612                    .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12613                    .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12614            }
12615            fn cf(t: &QTensor) -> &[f32] {
12616                t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
12617            }
12618            let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
12619            let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
12620            // q8_2f projections ride the chunk graph on the MoE layers it
12621            // was measured on (Mellum2.1) and on the dense exact-GELU FFN
12622            // (Spark-X2.6-4B q8_2f on the M4: the gate/up inputs staged as
12623            // x·col, the down field applied after the activation —
12624            // `ChunkLayer::ffn_col`). A dense SiLU q8_2f stack keeps the host
12625            // walk: its down GEMM fuses the activation into the X-tile load
12626            // and has no input field (0.8.14 refused every q8_2f weight here).
12627            // Gemma-4 reads every q8_2f weight through the blob GEMM with its
12628            // own field (`ChunkGemma4`), so its dense q8_2f stack rides.
12629            if moe.is_none()
12630                && !gemma4
12631                && ffn_act != Act::Gelu
12632                && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
12633            {
12634                break;
12635            }
12636            let parts = (
12637                cw(wq),
12638                cw(wk),
12639                cw(wv),
12640                cw(wo),
12641                cw(gate_t),
12642                cw(up_t),
12643                cw(down_t),
12644            );
12645            let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
12646            else {
12647                break;
12648            };
12649            let layer = &self.kv_cache.layers[li];
12650            if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
12651                break;
12652            }
12653            // A trimmed tail holds every row the chunk's first query reads.
12654            debug_assert!(
12655                layer.base() == 0
12656                    || self
12657                        .layer_window(li)
12658                        .is_some_and(|w| layer.head_len(0) + 1 >= w)
12659            );
12660            stored_at.push(layer.head_len(0));
12661            // Per-layer geometry for Gemma-4 (512-wide global layers over
12662            // one KV head); every other model keeps the pipeline-wide one.
12663            let (nkv, hd) = if gemma4 {
12664                let (a, bb, _) = self.layer_geom(li);
12665                (a, bb)
12666            } else {
12667                (nkv, hd)
12668            };
12669            layers.push(crate::gpu_metal::ChunkLayer {
12670                model: &model,
12671                kv_id: self.graph_kv_id,
12672                layer: li,
12673                wq: pq,
12674                wk: pk,
12675                wv: pv,
12676                wo: po,
12677                gate: pg,
12678                up: pu,
12679                down: pd,
12680                input_norm: &lw.input_norm,
12681                post_norm: &lw.post_norm,
12682                bias: bias
12683                    .as_ref()
12684                    .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
12685                q_norm: q_norm.as_deref(),
12686                k_norm: k_norm.as_deref(),
12687                inv_freq: &tables[li - li0],
12688                rope_scale: self.layer_rope_scale(li),
12689                rd: self.layer_geom(li).2,
12690                nh,
12691                nkv,
12692                hd,
12693                hs,
12694                inter: gate_t.rows(),
12695                gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
12696                late_qk_norm: self.qk_norm_after_rope,
12697                eps: self.rms_eps as f32,
12698                window: self.layer_window(li),
12699                head_gate,
12700                act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
12701                g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
12702                    v_norm: self.attn_v_norm,
12703                    scale: self.attn_scale,
12704                    attn_out_norm: lw.attn_out_norm.as_deref(),
12705                    ffn_out_norm: lw.ffn_out_norm.as_deref(),
12706                    layer_scale: lw.layer_scale,
12707                }),
12708                col,
12709                // The plain dense branch alone reads it: routed experts and
12710                // Gemma-4 carry their own fields.
12711                ffn_col: if moe.is_none() && !gemma4 {
12712                    ffn_col
12713                } else {
12714                    [&[][..]; 3]
12715                },
12716                moe,
12717            });
12718        }
12719        if layers.is_empty() {
12720            return li0;
12721        }
12722        // KV row width per layer (uniform but for Gemma-4).
12723        let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
12724        let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
12725            .iter()
12726            .zip(&rows_kv)
12727            .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
12728            .collect();
12729        let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
12730        for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
12731            let li = layers[i].layer;
12732            let layer = &self.kv_cache.layers[li];
12733            let nkv = rows_kv[i].0;
12734            io.push(crate::gpu_metal::ChunkIo {
12735                cpu_stored: stored_at[i],
12736                cpu_gen: layer.generation(),
12737                cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
12738                cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
12739                out_k: ok,
12740                out_v: ov,
12741                imp: oi,
12742            });
12743        }
12744        let n_run = layers.len();
12745        let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
12746        // Device-side embedding when the run starts the model and the
12747        // embedding matrix is q8_row-mapped.
12748        let ep = embed_ids.and_then(|ids| {
12749            self.weights
12750                .embed_tokens
12751                .q8_row_parts()
12752                .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
12753                    idx,
12754                    rows,
12755                    row_scale: rs,
12756                    ids,
12757                    mult: self.embed_multiplier,
12758                })
12759        });
12760        if embed_ids.is_some() && ep.is_none() {
12761            return li0;
12762        }
12763        if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
12764            return li0;
12765        }
12766        drop(io);
12767        drop(layers);
12768        // CPU caches stay the owners of record: append the chunk rows
12769        // and bank the importance masses per layer.
12770        for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
12771            let li = li0 + i;
12772            let row = rows_kv[i].1;
12773            let layer = &mut self.kv_cache.layers[li];
12774            for bi in 0..b {
12775                layer.append(
12776                    &ok[bi * row..(bi + 1) * row],
12777                    &ov[bi * row..(bi + 1) * row],
12778                    &[],
12779                );
12780            }
12781            layer.accumulate_imp(oi);
12782        }
12783        last
12784    }
12785
12786    /// Is layer `li` a sliding-window (local-RoPE) layer? Gemma-3:
12787    /// every `pattern`-th layer is global, the rest are local.
12788    fn layer_is_local(&self, li: usize) -> bool {
12789        if let Some(layers) = &self.sliding_layers {
12790            return layers.get(li).copied().unwrap_or(false);
12791        }
12792        match self.swa {
12793            Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
12794            None => false,
12795        }
12796    }
12797
12798    /// The RoPE table for layer `li` (local layers may have their own;
12799    /// Gemma-4 global layers use the proportional padded table).
12800    fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
12801        if self.layer_is_local(li) {
12802            if let Some(f) = &self.inv_freq_local {
12803                return f.clone();
12804            }
12805        } else if let Some(f) = &self.inv_freq_global {
12806            return f.clone();
12807        }
12808        self.inv_freq.clone()
12809    }
12810
12811    /// The attend window for layer `li` (None = full context).
12812    fn layer_window(&self, li: usize) -> Option<usize> {
12813        self.swa
12814            .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
12815    }
12816
12817    /// Drop every sliding layer's rows its window can no longer read
12818    /// (`LayerKvCache::trim_window`). Call only at a safe point — after a
12819    /// complete position / pair / chunk walk, never between a layer's
12820    /// appends and its attend. One comparison per layer below the trigger.
12821    ///
12822    /// Off with an MTP head: its verify oracles and rollbacks snapshot and
12823    /// compare per-layer row counts as positions (`CMF_METAL_VERIFY_CHECK`,
12824    /// the MTP caches), which a trimmed tail would break.
12825    fn swa_trim_tails(&mut self) {
12826        let Some((slack, align)) = self.swa_trim else {
12827            return;
12828        };
12829        if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
12830            return;
12831        }
12832        for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
12833            if let Some(w) = self.layer_window(li) {
12834                self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
12835            }
12836        }
12837    }
12838
12839    fn layer_num_heads(&self, li: usize) -> usize {
12840        self.attention_heads_per_layer
12841            .as_ref()
12842            .and_then(|v| v.get(li).copied())
12843            .unwrap_or(self.num_heads)
12844    }
12845
12846    fn layer_rope_scale(&self, li: usize) -> f32 {
12847        if self.layer_is_local(li) {
12848            self.rope_scale_local
12849        } else {
12850            self.rope_scale
12851        }
12852    }
12853
12854    /// Attention geometry of layer `li`: (num_kv_heads, head_dim,
12855    /// rotary_dim). Gemma-4 global layers override all three.
12856    fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
12857        if !self.layer_is_local(li) {
12858            if let Some((ghd, gkv)) = self.global_attn {
12859                return (gkv, ghd, ghd);
12860            }
12861        }
12862        let rd = if self.layer_is_local(li) {
12863            self.rotary_dim_local.unwrap_or(self.rotary_dim)
12864        } else {
12865            self.rotary_dim
12866        };
12867        // A layer without positional encoding rotates nothing.
12868        let rd = if self
12869            .no_rope_layers
12870            .as_ref()
12871            .and_then(|v| v.get(li))
12872            .copied()
12873            == Some(true)
12874        {
12875            0
12876        } else {
12877            rd
12878        };
12879        (self.layer_num_kv_heads(li), self.head_dim, rd)
12880    }
12881
12882    /// KV heads of layer `li` (virtual index): the per-layer count when the
12883    /// model has one (MiMo-V2), else the uniform `num_kv_heads`.
12884    fn layer_num_kv_heads(&self, li: usize) -> usize {
12885        self.kv_heads_per_layer
12886            .as_ref()
12887            .and_then(|v| v.get(self.phys_layer(li)).copied())
12888            .unwrap_or(self.num_kv_heads)
12889    }
12890
12891    /// V head width of layer `li` (≤ its head_dim).
12892    fn layer_v_dim(&self, li: usize) -> usize {
12893        let (_, hd, _) = self.layer_geom(li);
12894        self.v_head_dim.unwrap_or(hd).min(hd)
12895    }
12896
12897    /// Install a per-layer KV geometry: KV heads per PHYSICAL layer and/or
12898    /// a V head width narrower than `head_dim` (MiMo-V2). Validates it and
12899    /// reshapes the caches of every layer whose KV head count differs from
12900    /// `num_kv_heads`. The loader and the tests share this one path, so a
12901    /// hand-built pipeline cannot hold a geometry the loader would refuse.
12902    /// Call before the first forward (it drops cached rows of reshaped
12903    /// layers). Refuses combinations whose paths would read it wrong:
12904    /// Gemma-4 global layers and MLA carry their own geometry.
12905    pub fn set_attn_geometry(
12906        &mut self,
12907        kv_heads_per_layer: Option<Vec<usize>>,
12908        v_head_dim: Option<usize>,
12909    ) -> Result<(), String> {
12910        if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
12911            if self.global_attn.is_some() {
12912                return Err(
12913                    "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
12914                     attention geometry"
12915                        .into(),
12916                );
12917            }
12918            if self
12919                .weights
12920                .layers
12921                .iter()
12922                .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
12923            {
12924                return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
12925            }
12926        }
12927        if let Some(vd) = v_head_dim {
12928            if vd == 0 || vd > self.head_dim {
12929                return Err(format!(
12930                    "v_head_dim {vd} must be in 1..={} (head_dim)",
12931                    self.head_dim
12932                ));
12933            }
12934        }
12935        if let Some(v) = &kv_heads_per_layer {
12936            if v.len() != self.physical_layers {
12937                return Err(format!(
12938                    "kv_heads_per_layer has {} entries, expected {} layers",
12939                    v.len(),
12940                    self.physical_layers
12941                ));
12942            }
12943            for (li, &nkv) in v.iter().enumerate() {
12944                let is_attn = matches!(
12945                    self.weights.layers.get(li).map(|lw| &lw.attn),
12946                    Some(AttnKind::Full { .. }) | None
12947                );
12948                if !is_attn {
12949                    continue;
12950                }
12951                let nh = self
12952                    .attention_heads_per_layer
12953                    .as_ref()
12954                    .and_then(|h| h.get(li).copied())
12955                    .unwrap_or(self.num_heads);
12956                if nkv == 0 || nh % nkv != 0 {
12957                    return Err(format!(
12958                        "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
12959                    ));
12960                }
12961            }
12962        }
12963        self.kv_heads_per_layer = kv_heads_per_layer;
12964        self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
12965        if self.kv_heads_per_layer.is_some() {
12966            for li in 0..self.kv_cache.layers.len() {
12967                let full = matches!(
12968                    self.weights
12969                        .layers
12970                        .get(self.phys_layer(li))
12971                        .map(|lw| &lw.attn),
12972                    Some(AttnKind::Full { .. })
12973                );
12974                let nkv = self.layer_num_kv_heads(li);
12975                let cache = &self.kv_cache.layers[li];
12976                if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
12977                    let sinks = cache.sinks.clone();
12978                    self.kv_cache.layers[li] =
12979                        crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
12980                    self.kv_cache.layers[li].sinks = sinks;
12981                }
12982            }
12983        }
12984        Ok(())
12985    }
12986
12987    /// Attach learned attention-sink logits (one per Q head) to PHYSICAL
12988    /// layer `phys` — every virtual layer that runs it. The loader calls
12989    /// this for each `model.layers.N.self_attn.sinks` tensor.
12990    pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
12991        let Some(lw) = self.weights.layers.get(phys) else {
12992            return Err(format!("sinks for layer {phys}: no such layer"));
12993        };
12994        if !matches!(lw.attn, AttnKind::Full { .. }) {
12995            return Err(format!(
12996                "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
12997            ));
12998        }
12999        let nh = self
13000            .attention_heads_per_layer
13001            .as_ref()
13002            .and_then(|h| h.get(phys).copied())
13003            .unwrap_or(self.num_heads);
13004        if sinks.len() != nh {
13005            return Err(format!(
13006                "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
13007                sinks.len()
13008            ));
13009        }
13010        if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
13011            return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
13012        }
13013        for li in 0..self.kv_cache.layers.len() {
13014            if self.phys_layer(li) == phys {
13015                self.kv_cache.layers[li].sinks = Some(sinks.clone());
13016            }
13017        }
13018        Ok(())
13019    }
13020
13021    /// Why the GPU attention graphs cannot serve this model, if they
13022    /// cannot: the wgpu whole-token and batched graphs, the greedy
13023    /// multi-burst, the q1 attention dropin and the Metal block/chunk/rows
13024    /// graphs all assume ONE (num_kv_heads, head_dim) geometry, V heads as
13025    /// wide as K, a single RoPE table, full-context attention and a plain
13026    /// softmax. A model outside that contract runs on the CPU layer walk
13027    /// (and the per-op GPU matvecs) until a graph learns it — never on a
13028    /// graph that would read it wrong. None = no attention-level reason
13029    /// (the graph builders still check weights and layer kinds).
13030    pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
13031        if self.kv_heads_per_layer.is_some() {
13032            return Some("per-layer KV head counts");
13033        }
13034        if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
13035            return Some("V heads narrower than Q/K heads");
13036        }
13037        if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
13038            return Some("learned attention sinks");
13039        }
13040        if self.swa.is_some() || self.sliding_layers.is_some() {
13041            return Some("sliding-window layers");
13042        }
13043        None
13044    }
13045
13046    /// Can the Metal kernels apply this model's RoPE amplitude? Every
13047    /// rotation kernel (`attn_rope_qkn[_b]`, `chunk_rope_kv`) multiplies the
13048    /// rotated pair by the layer's `rope_scale` (YaRN `attention_factor`,
13049    /// Mellum2.1's global layers) exactly where the CPU's
13050    /// `rope_rotate_scaled` does. With the Q/K norm AFTER RoPE the kernels'
13051    /// shared sum of squares would be the unscaled one, so that pairing
13052    /// (no known model) declines.
13053    #[cfg(target_os = "macos")]
13054    fn metal_rope_scale_ok(&self) -> bool {
13055        !(self.qk_norm_after_rope
13056            && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
13057    }
13058
13059    /// Can the Metal block graph carry this model's sliding-window layers?
13060    /// Both of its attention forms take a layer's own window, RoPE table
13061    /// and rotary width: the device attend (`AttnDeviceParams::window`, the
13062    /// layer's `inv_freq`/`rd`) and the sandwich's host attend (exactly the
13063    /// CPU path's attention). True when the sliding window is the model's
13064    /// only attention-level decline (Spark-X2.5). Learned, value-less
13065    /// sinks may accompany that geometry: q1's ordinary device GQA
13066    /// includes the sink in its softmax denominator and disables split-K.
13067    /// Per-layer KV heads, narrow V and Gemma-4 global geometry still
13068    /// decline; a YaRN amplitude per layer rides along
13069    /// (`metal_rope_scale_ok`).
13070    ///
13071    /// Per-layer Q heads (Laguna) and capped scores (Gemma-2) decline here
13072    /// too, with V norm (Gemma-4): the chunk prefill's own gate checks
13073    /// neither of the first two, and before this door opened its `swa`
13074    /// refusal kept every sliding model with them off the chunk graph.
13075    #[cfg(target_os = "macos")]
13076    fn metal_graph_swa(&self) -> bool {
13077        (self.swa.is_some() || self.sliding_layers.is_some())
13078            && self.attention_heads_per_layer.is_none()
13079            && !self.attn_v_norm
13080            && self.attn_softcap == 0.0
13081            && self.kv_heads_per_layer.is_none()
13082            && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
13083            && self.global_attn.is_none()
13084            && self.inv_freq_global.is_none()
13085            && self.metal_rope_scale_ok()
13086            && !(0..self.num_layers).any(|li| {
13087                self.layer_is_local(li)
13088                    && self.inv_freq_local.is_none()
13089                    && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13090            })
13091    }
13092
13093    /// Can the Metal graphs carry this Gemma-4 model (`gpu_metal::gemma4`)?
13094    /// Its sliding layers (window, local RoPE table, 256-wide heads)
13095    /// device-attend like Spark-X2.5's; the 512-wide global layers (one KV
13096    /// head, proportional RoPE in the zero-padded table) take the 16-slot
13097    /// kernels; the V norm, sandwich norms, layer scalar and tanh-GELU come
13098    /// from the Gemma-4 library. Dense FFNs only (no Gemma-4 MoE measured).
13099    /// `CMF_METAL_GEMMA4=0` declines it as before (CPU attention).
13100    #[cfg(target_os = "macos")]
13101    fn metal_gemma4(&self) -> bool {
13102        crate::gpu_metal::gemma4::enabled()
13103            && (self.global_attn.is_some() || self.attn_v_norm)
13104            && self.attention_heads_per_layer.is_none()
13105            && self.kv_heads_per_layer.is_none()
13106            && self.v_head_dim.is_none()
13107            && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
13108            && self.attn_softcap == 0.0
13109            && !self.qk_norm_after_rope
13110            && self.metal_rope_scale_ok()
13111            && self
13112                .weights
13113                .layers
13114                .iter()
13115                .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
13116            && !(0..self.num_layers).any(|li| {
13117                self.layer_is_local(li)
13118                    && self.inv_freq_local.is_none()
13119                    && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13120            })
13121    }
13122
13123    /// Why the WGPU graphs (whole-token, batched prefill, greedy burst)
13124    /// cannot run this model's attention, if they cannot. Per-layer KV
13125    /// heads, V narrower than K, learned sinks and sliding windows ride
13126    /// their per-layer geometry (`GraphAttnGeom`, the ATTEND_X kernels);
13127    /// what that geometry does not express keeps the decline, by name.
13128    /// None for every model with one attention geometry.
13129    pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
13130        // Keep the ordinary (uniform) fast arms off YaRN until they carry a
13131        // scale field too. A non-uniform geometry gets a `GraphAttnGeom` and
13132        // is handled below by the generic graph kernels.
13133        let geometry_reason = self.graph_attn_decline_reason();
13134        let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
13135        if has_scaled_rope && geometry_reason.is_none() {
13136            return Some("scaled RoPE without per-layer geometry");
13137        }
13138        // For Q/K norm after partial RoPE, the scale changes only the rotary
13139        // slice's RMS denominator. The generic graph deliberately declines
13140        // that uncommon combination until it carries a separate rotary sum;
13141        // Mellum has q/k norm before RoPE, so it remains on this path.
13142        if has_scaled_rope && self.qk_norm_after_rope {
13143            return Some("scaled RoPE with post-RoPE Q/K norm");
13144        }
13145        geometry_reason?;
13146        // Gemma-4: global layers 512 wide over 256-wide sliding ones
13147        // (`GraphAttnGeom::hd`), the scale-less V norm (`v_norm`), and the
13148        // sandwich norms + layer scalar (`GraphLayer`) ride the TOKEN graph.
13149        // The batch graph has none of the three and refuses them itself
13150        // (`batch_graph_gemma_decline`).
13151        if self.attention_heads_per_layer.is_some() {
13152            return Some("per-layer Q head counts with per-layer geometry");
13153        }
13154        // Sandwich norms are measured on Gemma-4 (the humanizer port) and
13155        // LFM3, both with plain x̂·w weights, against HF transformers;
13156        // Gemma-2/3's (1+w) norms keep the per-op path until they are
13157        // measured the same way, and a layer scale stays Gemma-4's.
13158        let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
13159        let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
13160        if !gemma4
13161            && self.weights.layers.iter().any(|lw| {
13162                lw.layer_scale.is_some()
13163                    || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some()))
13164            })
13165        {
13166            return Some("sandwich norms / layer scale with per-layer geometry");
13167        }
13168        // `GraphAttnGeom::rope_scale` carries YaRN's post-rotation
13169        // attention-amplitude factor to the WGPU RoPE kernel. It is not an
13170        // angular position scale, so it must not be folded into `invf`.
13171        if self.weights.layers.iter().any(|lw| {
13172            matches!(
13173                &lw.attn,
13174                AttnKind::Full {
13175                    output_gate: true,
13176                    ..
13177                }
13178            )
13179        }) && self.v_head_dim.is_some()
13180        {
13181            return Some("gated attention with V narrower than K");
13182        }
13183        if (0..self.num_layers).any(|li| {
13184            self.layer_is_local(li)
13185                && self.inv_freq_local.is_none()
13186                && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13187        }) {
13188            return Some("local rotary width without a local RoPE table");
13189        }
13190        None
13191    }
13192
13193    /// What the wgpu BATCH graph (and the attention-only singleton graphs)
13194    /// cannot run of Gemma-4, which the token graph does run: per-layer
13195    /// head widths, the V norm, sandwich norms and the layer scalar.
13196    fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
13197        // Gemma-4 rides `gemma_batch` (per-layer head width, V norm,
13198        // sandwich norms, layer scalar) unless CMF_G4_BATCH=0.
13199        if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
13200            return None;
13201        }
13202        if self.global_attn.is_some() {
13203            return Some("per-layer head width (Gemma-4 global layers)");
13204        }
13205        if self.attn_v_norm {
13206            return Some("V norm");
13207        }
13208        if self.weights.layers.iter().any(|lw| {
13209            lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
13210        }) {
13211            return Some("sandwich norms / layer scale");
13212        }
13213        None
13214    }
13215
13216    /// The wgpu graphs' attention geometry for layer `li` (virtual index):
13217    /// Some only for a model whose layers do not share one (MiMo-V2) — KV
13218    /// heads, V width, rotary width and RoPE table, window and sinks of
13219    /// THIS layer, exactly what the CPU attention reads for it.
13220    fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
13221        self.graph_attn_decline_reason()?;
13222        let (nkv, hd, rd) = self.layer_geom(li);
13223        let invf: &[f32] = if self.layer_is_local(li) {
13224            match &self.inv_freq_local {
13225                Some(f) => f.as_slice(),
13226                None => self.inv_freq.as_slice(),
13227            }
13228        } else {
13229            match &self.inv_freq_global {
13230                Some(f) => f.as_slice(),
13231                None => self.inv_freq.as_slice(),
13232            }
13233        };
13234        Some(crate::gpu::GraphAttnGeom {
13235            nkv,
13236            hd,
13237            dv: self.layer_v_dim(li),
13238            rd,
13239            invf,
13240            rope_scale: self.layer_rope_scale(li),
13241            window: self.layer_window(li),
13242            sink: self.kv_cache.layers[li].sinks.as_deref(),
13243            v_norm: self.attn_v_norm,
13244        })
13245    }
13246
13247    /// The decode graph's device K/V mirror that layer `li`'s batched
13248    /// prefill appends its chunk to and attends against (wgpu), instead
13249    /// of uploading the layer's whole K/V prefix every chunk and the whole
13250    /// cache again at the first decode token. Only where the token graph
13251    /// will read that very mirror: the decode graph on, not refused, and
13252    /// this layer's geometry what it requests (`graph_attn_geom`: KV heads,
13253    /// V as wide as K, the window as a ring, no sink).
13254    ///
13255    /// Spark-X2.5 only (`proj_gate_sigmoid`), the one architecture whose
13256    /// output was measured identical on it. `CMF_PREFILL_MIRROR=0` keeps
13257    /// the host upload (A/B).
13258    #[cfg(feature = "gpu")]
13259    fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
13260        if !self.proj_gate_sigmoid
13261            || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
13262            || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
13263            || self.graph_refused()
13264            || self.o1_active()
13265            || self.attn_softcap > 0.0
13266            || self.wgpu_graph_attn_decline().is_some()
13267        {
13268            return None;
13269        }
13270        let g = self.graph_attn_geom(li)?;
13271        let (nkv, hd, _) = self.layer_geom(li);
13272        // The token graph passes the call-wide head width (layer 0's).
13273        if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
13274            return None;
13275        }
13276        Some(crate::gpu::PrefillMirror {
13277            kv_id: self.graph_kv_id,
13278            layer: li,
13279            limit: self.kv_cache.max_seq_len,
13280        })
13281    }
13282
13283    /// Bring the host KV cache of every Full-attention layer in
13284    /// `[from, upto)` up to `position` rows from the wgpu mirrors, where a
13285    /// device graph advanced a layer that the host is about to run: a
13286    /// device prefix that shrank since the prompt (or a batched prefill
13287    /// prefix longer than the decode one). A layer whose mirror does not
13288    /// hold the missing rows is left alone. Rows a sliding layer's ring
13289    /// no longer holds come back as zeros — outside every window that
13290    /// will read them.
13291    #[cfg(feature = "gpu")]
13292    fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
13293        let kv_id = self.graph_kv_id;
13294        for li in from..upto.min(self.num_layers) {
13295            if !matches!(
13296                self.weights.layers[self.phys_layer(li)].attn,
13297                AttnKind::Full { .. }
13298            ) {
13299                continue;
13300            }
13301            // Absolute: a trimmed sliding tail holds positions base.. only.
13302            let host = self.kv_cache.layers[li].pos_len();
13303            if host >= position {
13304                continue;
13305            }
13306            let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
13307                continue;
13308            };
13309            let to = dev.min(position);
13310            if to <= host {
13311                continue;
13312            }
13313            let (nkv, hd) = {
13314                let c = &self.kv_cache.layers[li];
13315                (c.num_kv_heads, c.head_dim)
13316            };
13317            let Some((k, v, first_valid)) =
13318                crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
13319            else {
13320                continue;
13321            };
13322            // A sliding layer only ever reads its last `window` rows; a
13323            // full-context layer needs every row it did not have.
13324            let need_from = match self.layer_window(li) {
13325                Some(w) => host.max((position + 1).saturating_sub(w)),
13326                None => host,
13327            };
13328            if first_valid > need_from {
13329                tracing::warn!(
13330                    "layer {li}: device KV rows {host}..{to} no longer resident \
13331                     (from {first_valid}); host attention will miss them"
13332                );
13333            }
13334            let row = nkv * hd;
13335            let cache = &mut self.kv_cache.layers[li];
13336            for p in 0..to - host {
13337                cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
13338            }
13339        }
13340    }
13341
13342    /// Log (once per graph site and pipeline) that `site` declined for
13343    /// `reason`. The lines are kept so a caller or a test can read them.
13344    fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
13345        let mut seen = self.graph_declines.borrow_mut();
13346        if !seen.iter().any(|&(s, r)| s == site && r == reason) {
13347            tracing::warn!("{site} declined: {reason} (CPU attention path)");
13348            seen.push((site, reason));
13349        }
13350    }
13351
13352    /// The GPU-graph declines this pipeline has logged so far, as the
13353    /// logged lines.
13354    pub fn graph_declines(&self) -> Vec<String> {
13355        self.graph_declines
13356            .borrow()
13357            .iter()
13358            .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
13359            .collect()
13360    }
13361
13362    /// Does layer `li` have the plain attention geometry the historical
13363    /// head-masked f32 path (`multi_head_attention`) assumes — pipeline-wide
13364    /// KV heads / head_dim / RoPE table, full context, no sink, V as wide
13365    /// as K? Anything else runs the dense `qwen_attention` instead.
13366    /// `CMF_LAYER_DUMP` writer (see `Pipeline::layer_dump`): one position's
13367    /// hidden after layer `li` as raw little-endian f32 into
13368    /// `<dir>/p{pos:06}_l{li:02}.f32`. A failed write is reported once and
13369    /// never stops the forward — the dump is a diagnostic.
13370    fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
13371        let Some(dir) = &self.layer_dump else {
13372            return;
13373        };
13374        let mut bytes = Vec::with_capacity(row.len() * 4);
13375        for v in row {
13376            bytes.extend_from_slice(&v.to_le_bytes());
13377        }
13378        let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
13379        if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
13380            use std::sync::atomic::{AtomicBool, Ordering};
13381            static SAID: AtomicBool = AtomicBool::new(false);
13382            if !SAID.swap(true, Ordering::Relaxed) {
13383                tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
13384            }
13385        }
13386    }
13387
13388    /// Decide the MiMo-V2 expert placement once (`crate::mimo_moe`). Any
13389    /// other model turns the slot off on the first call.
13390    fn mimo_moe_prepare(&mut self) {
13391        if !self.mimo_moe.is_undecided() {
13392            return;
13393        }
13394        let slot = {
13395            let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
13396                .filter_map(
13397                    |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
13398                        FfnKind::Moe(m) => Some((li, m)),
13399                        _ => None,
13400                    },
13401                )
13402                .collect();
13403            // One bank lives on one device: an in-process multi-GPU split
13404            // keeps the whole-layer path.
13405            if layers.is_empty()
13406                || self.physical_layers != self.num_layers
13407                || self.gpu_plan.is_some()
13408            {
13409                crate::mimo_moe::Slot::Off
13410            } else {
13411                // Whether a whole-token graph could run this model's layers
13412                // (then a whole-layer prefix is one submit, not per-layer
13413                // fences).
13414                let graph_prefix =
13415                    self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
13416                crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
13417            }
13418        };
13419        self.mimo_moe = slot;
13420    }
13421
13422    #[cfg(test)]
13423    pub(crate) fn test_graph_kv_id(&self) -> u64 {
13424        self.graph_kv_id
13425    }
13426
13427    /// Dynamic MiMo layer: one device attention graph, followed by a bank
13428    /// frame. Both decode and short verification use this same attention
13429    /// path and absolute layer key; the host KV may intentionally lag.
13430    pub(crate) fn mimo_graph_layer_rows(
13431        &mut self,
13432        li: usize,
13433        h: &mut [f32],
13434        positions: &[usize],
13435    ) -> crate::gpu::BatchGraphOutcome {
13436        use crate::gpu::BatchGraphOutcome as Out;
13437        let b = positions.len();
13438        if !(1..=4).contains(&b)
13439            || h.len() != b * self.hidden_size
13440            || !self.mimo_moe.is_dynamic(li, true)
13441            || !crate::gpu::enabled_here()
13442            || !crate::gpu::wgpu_active()
13443            || self.o1_active()
13444            || self.physical_layers != self.num_layers
13445            // The pair-fusion diagnostic (and an explicit graph-off run)
13446            // rewinds only host KV. A hidden singleton attention graph here
13447            // would leave device mirrors ahead of the next host position.
13448            || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
13449            || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
13450            || self.wgpu_graph_attn_decline().is_some()
13451        {
13452            return Out::Declined;
13453        }
13454        let attn_started = std::time::Instant::now();
13455        let outcome = {
13456            let lw = &self.weights.layers[li];
13457            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13458                return Out::Declined;
13459            }
13460            let FfnKind::Moe(m) = &lw.ffn else {
13461                return Out::Declined;
13462            };
13463            let AttnKind::Full {
13464                wq,
13465                wk,
13466                wv,
13467                wo,
13468                q_norm,
13469                k_norm,
13470                output_gate,
13471                softplus_gate,
13472                bias,
13473            } = &lw.attn
13474            else {
13475                return Out::Declined;
13476            };
13477            if *output_gate || softplus_gate.is_some() {
13478                return Out::Declined;
13479            }
13480            let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
13481                m.experts
13482                    .first()?
13483                    .gate_proj
13484                    .mapped_q4tp()
13485                    .map(|(m, _)| m.clone())
13486            }) else {
13487                return Out::Declined;
13488            };
13489            fn gw<'a>(
13490                t: &'a QTensor,
13491                owner: &std::sync::Arc<cortiq_core::CmfModel>,
13492            ) -> Option<crate::gpu::GraphW<'a>> {
13493                if let Some((m, idx, kind, rs)) = t.graph_weight() {
13494                    if m.uid() != owner.uid() || t.has_prism_contract() {
13495                        return None;
13496                    }
13497                    return Some(crate::gpu::GraphW {
13498                        idx,
13499                        kind,
13500                        row_scale: rs,
13501                        data: &[],
13502                        prism: crate::gpu::GraphPrismOp::None,
13503                        affine: false,
13504                    });
13505                }
13506                t.as_f32().map(|data| crate::gpu::GraphW {
13507                    idx: 0,
13508                    kind: 4,
13509                    row_scale: &[],
13510                    data,
13511                    prism: crate::gpu::GraphPrismOp::None,
13512                    affine: false,
13513                })
13514            }
13515            let (Some(q), Some(k), Some(v), Some(o)) = (
13516                gw(wq, &model),
13517                gw(wk, &model),
13518                gw(wv, &model),
13519                gw(wo, &model),
13520            ) else {
13521                return Out::Declined;
13522            };
13523            let layer = crate::gpu::GraphLayer {
13524                mhc: None,
13525                input_norm: &lw.input_norm,
13526                post_norm: &lw.post_norm,
13527                attn_out_norm: None,
13528                ffn_out_norm: None,
13529                layer_scale: None,
13530                ffn: crate::gpu::GraphFfn::AttentionOnly,
13531                attn: crate::gpu::GraphAttn::Full {
13532                    wq: q,
13533                    wk: k,
13534                    wv: v,
13535                    wo: o,
13536                    q_norm: q_norm.as_deref(),
13537                    k_norm: k_norm.as_deref(),
13538                    late_qk_norm: self.qk_norm_after_rope,
13539                    bias: bias
13540                        .as_ref()
13541                        .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
13542                    output_gate: false,
13543                    cpu_k: self.kv_cache.layers[li].k_heads(),
13544                    cpu_v: self.kv_cache.layers[li].v_heads(),
13545                    cpu_base: self.kv_cache.layers[li].base(),
13546                    geom: self.graph_attn_geom(li),
13547                    head_gate: None,
13548                },
13549            };
13550            let (nkv, hd, rd) = self.layer_geom(li);
13551            crate::gpu::forward_batch_graph_at(
13552                &model,
13553                self.graph_kv_id,
13554                li,
13555                &[layer],
13556                &self.inv_freq,
13557                h,
13558                self.layer_num_heads(li),
13559                nkv,
13560                hd,
13561                rd,
13562                self.hidden_size,
13563                1,
13564                positions,
13565                self.kv_cache.max_seq_len,
13566                self.norm_style == cortiq_core::NormStyle::Gemma,
13567                self.rms_eps as f32,
13568                self.attn_scale,
13569                b,
13570                &[],
13571                self.o1_epoch,
13572                None,
13573                None,
13574            )
13575        };
13576        match outcome {
13577            Out::Completed => {}
13578            Out::Declined => return Out::Declined,
13579            Out::Failed => {
13580                self.graph_failed
13581                    .store(true, std::sync::atomic::Ordering::Relaxed);
13582                return Out::Failed;
13583            }
13584        }
13585        let attn_ns = attn_started.elapsed().as_nanos() as u64;
13586        let hs = self.hidden_size;
13587        let lw = &self.weights.layers[li];
13588        let FfnKind::Moe(m) = &lw.ffn else {
13589            unreachable!()
13590        };
13591        let mut post = vec![0.0; h.len()];
13592        for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
13593            inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
13594        }
13595        let mut ffn = if b == 1 {
13596            moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
13597        } else {
13598            moe_ffn_banked_rows(
13599                &mut self.mimo_moe,
13600                li,
13601                m,
13602                &post,
13603                b,
13604                hs,
13605                self.pool.as_deref(),
13606            )
13607        };
13608        for (x, &f) in h.iter_mut().zip(&ffn) {
13609            *x += f;
13610        }
13611        attention::recycle_buf(&mut ffn);
13612        if self.layer_dump.is_some() {
13613            for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
13614                self.dump_layer_row(pos, li, row);
13615            }
13616        }
13617        crate::mimo_moe::note_attention_graph(b, attn_ns);
13618        Out::Completed
13619    }
13620
13621    fn layer_attn_plain(&self, li: usize) -> bool {
13622        self.kv_heads_per_layer.is_none()
13623            && self.v_head_dim.is_none()
13624            && self.global_attn.is_none()
13625            && self.layer_window(li).is_none()
13626            && self.kv_cache.layers[li].sinks.is_none()
13627    }
13628
13629    /// Forward one position through all layers (hybrid dispatch).
13630    fn forward_layers(
13631        &mut self,
13632        hidden: &[f32],
13633        position: usize,
13634        task_mask: Option<&TaskMask>,
13635    ) -> Vec<f32> {
13636        let out = self.forward_layers_upto(hidden, position, task_mask, None);
13637        self.o1_progress();
13638        self.swa_trim_tails();
13639        out
13640    }
13641
13642    // ── Network pipeline-split building blocks (coordinator/worker) ──
13643    // A remote worker owns layers [from ..= upto] and their KV; the
13644    // coordinator owns the rest plus embed / final norm / head. Attention
13645    // causality is per-layer, so a whole prompt's boundary hiddens ship
13646    // as one batch and decode ships one vector per token.
13647
13648    /// Embed one token id (embed multiplier applied).
13649    pub fn embed_id(&self, id: u32) -> Vec<f32> {
13650        self.embed_single(id)
13651    }
13652
13653    /// Refuse the archs/modes whose forward cannot be cut at a layer
13654    /// boundary. Loud by design: a split that silently changed the math
13655    /// would be a chimera.
13656    pub fn split_supported(&self) -> Result<(), String> {
13657        if self.dsv4.is_some() {
13658            return Err(
13659                "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
13660            );
13661        }
13662        if self.dsv41.is_some() {
13663            return Err(
13664                "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
13665                    .into(),
13666            );
13667        }
13668        if self.qwen4_exp.is_some() {
13669            return Err(
13670                "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
13671            );
13672        }
13673        if self.per_token_stack() {
13674            return Err(
13675                "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
13676            );
13677        }
13678        Ok(())
13679    }
13680
13681    /// Forward `hidden` through layers [from ..= upto] at `position`,
13682    /// appending those layers' KV/state. Both split sides call this
13683    /// over their own range; a task mask applies to the span's own
13684    /// layers (each side masks what it runs).
13685    pub fn forward_span(
13686        &mut self,
13687        hidden: &[f32],
13688        position: usize,
13689        from: usize,
13690        upto: usize,
13691        task_mask: Option<&TaskMask>,
13692    ) -> Result<Vec<f32>, String> {
13693        self.split_supported()?;
13694        if from > upto || upto >= self.num_layers {
13695            return Err(format!(
13696                "forward_span: layer range {from}..={upto} outside 0..{}",
13697                self.num_layers
13698            ));
13699        }
13700        if hidden.len() != self.hidden_size {
13701            return Err(format!(
13702                "forward_span: hidden len {} ≠ hidden_size {}",
13703                hidden.len(),
13704                self.hidden_size
13705            ));
13706        }
13707        let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
13708        self.o1_progress();
13709        self.swa_trim_tails();
13710        if self
13711            .graph_failed
13712            .swap(false, std::sync::atomic::Ordering::Relaxed)
13713        {
13714            self.cancel
13715                .store(false, std::sync::atomic::Ordering::Relaxed);
13716            self.clear_sequence_state();
13717            return Err("forward_span: deferred O(1) transition failed".into());
13718        }
13719        Ok(out)
13720    }
13721
13722    /// Final norm + lm_head over a boundary hidden (the final-logit
13723    /// softcap is applied by lm_head_forward itself).
13724    pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
13725        let normed = inference::rms_norm(
13726            hidden,
13727            &self.weights.final_norm,
13728            self.rms_eps,
13729            self.norm_style,
13730        );
13731        self.lm_head_forward(&normed)
13732    }
13733
13734    /// Sample the next token with this pipeline's sampler state.
13735    pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
13736        sampler::sample_with_scratch(
13737            logits,
13738            &self.sampler_config,
13739            past_tokens,
13740            &mut self.rng,
13741            &mut self.sampler_scratch,
13742        )
13743    }
13744
13745    /// Fresh sequence: clear KV, reuse history and device mirrors.
13746    pub fn reset_session(&mut self) {
13747        self.clear_sequence_state();
13748    }
13749
13750    /// Batched span prefill from token ids (coordinator side): embed +
13751    /// layers [0 ..= upto]; returns the boundary hiddens of ALL positions
13752    /// (ids.len() × hidden). Rides the same layer-major machinery as the
13753    /// local prefill; falls back to the per-position walk under
13754    /// CMF_PREFILL=seq.
13755    pub fn prefill_span_ids(
13756        &mut self,
13757        ids: &[u32],
13758        start_pos: usize,
13759        upto: usize,
13760        task_mask: Option<&TaskMask>,
13761    ) -> Result<Vec<f32>, String> {
13762        self.split_supported()?;
13763        if upto >= self.num_layers {
13764            return Err(format!(
13765                "prefill_span_ids: upto {upto} outside 0..{}",
13766                self.num_layers
13767            ));
13768        }
13769        // Same predicate as the whole-stack prefill: a span whose GDN
13770        // state lives on the device must walk positions through the
13771        // graph, not through the batched CPU span.
13772        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13773            let out =
13774                self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
13775            self.check_o1_progress_failure("prefill_span_ids")?;
13776            Ok(out)
13777        } else {
13778            let hs = self.hidden_size;
13779            let mut out = Vec::with_capacity(ids.len() * hs);
13780            for (i, &id) in ids.iter().enumerate() {
13781                let emb = self.embed_id(id);
13782                out.extend_from_slice(&self.forward_span(
13783                    &emb,
13784                    start_pos + i,
13785                    0,
13786                    upto,
13787                    task_mask,
13788                )?);
13789            }
13790            Ok(out)
13791        }
13792    }
13793
13794    /// Batched span prefill from boundary hiddens (worker side): layers
13795    /// [from ..= upto] for every position in the batch; returns the batch.
13796    pub fn prefill_span_hidden(
13797        &mut self,
13798        hidden: &[f32],
13799        start_pos: usize,
13800        from: usize,
13801        upto: usize,
13802        task_mask: Option<&TaskMask>,
13803    ) -> Result<Vec<f32>, String> {
13804        self.split_supported()?;
13805        let hs = self.hidden_size;
13806        if hidden.is_empty() || hidden.len() % hs != 0 {
13807            return Err(format!(
13808                "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
13809                hidden.len()
13810            ));
13811        }
13812        if from > upto || upto >= self.num_layers {
13813            return Err(format!(
13814                "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
13815                self.num_layers
13816            ));
13817        }
13818        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13819            let out = self.prefill_batch_span(
13820                PrefillIn::Hidden(hidden),
13821                start_pos,
13822                task_mask,
13823                from,
13824                upto + 1,
13825            );
13826            self.check_o1_progress_failure("prefill_span_hidden")?;
13827            Ok(out)
13828        } else {
13829            let b = hidden.len() / hs;
13830            let mut out = Vec::with_capacity(hidden.len());
13831            for i in 0..b {
13832                let h = self.forward_span(
13833                    &hidden[i * hs..(i + 1) * hs],
13834                    start_pos + i,
13835                    from,
13836                    upto,
13837                    task_mask,
13838                )?;
13839                out.extend_from_slice(&h);
13840            }
13841            Ok(out)
13842        }
13843    }
13844
13845    /// Build the whole-token wgpu graph for a pure-attention q1 model (every
13846    /// layer Full q1 + dense q1 FFN, no gate/bias). Returns the post-stack
13847    /// hidden (caller does final norm + lm_head), or None to fall back.
13848    fn try_token_graph_wgpu(
13849        &self,
13850        hidden: &[f32],
13851        position: usize,
13852        logits_out: &mut Vec<f32>,
13853        layers_run: &mut usize,
13854    ) -> Option<Result<Vec<f32>, ()>> {
13855        self.try_token_graph_wgpu_steps(
13856            hidden,
13857            position,
13858            logits_out,
13859            1,
13860            None,
13861            Some(layers_run),
13862            0,
13863            self.num_layers,
13864        )
13865    }
13866
13867    /// The span twin (network split): the graph covers [from..upto_excl)
13868    /// — one submit per SEGMENT per token. lm_head folds in only when
13869    /// the span reaches the last layer.
13870    fn try_token_graph_wgpu_span(
13871        &self,
13872        hidden: &[f32],
13873        position: usize,
13874        logits_out: &mut Vec<f32>,
13875        from: usize,
13876        upto_excl: usize,
13877        layers_run: &mut usize,
13878    ) -> Option<Result<Vec<f32>, ()>> {
13879        self.try_token_graph_wgpu_steps(
13880            hidden,
13881            position,
13882            logits_out,
13883            1,
13884            None,
13885            Some(layers_run),
13886            from,
13887            upto_excl,
13888        )
13889    }
13890
13891    /// The pooled tail's device lm_head: final-norm + head as one
13892    /// layer-less graph pass, so decode stops paying the double-digit
13893    /// millisecond host matvec every token. `None` keeps the host head.
13894    #[cfg(feature = "gpu")]
13895    fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
13896        if !crate::minicpm_tail::minimax_pool_on()
13897            || !crate::gpu::enabled_here()
13898            || self.head_clusters.is_some()
13899            || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
13900            || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
13901        {
13902            return None;
13903        }
13904        let (model, i, kind, rs) = self
13905            .weights
13906            .lm_head
13907            .graph_weight()
13908            .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
13909        let name = &model.tensors[i].name;
13910        let prism = if crate::prism::is_inverse_embedding(&model, name) {
13911            crate::gpu::GraphPrismOp::InverseEmbedding
13912        } else if crate::prism::is_forward_weight(&model, name) {
13913            crate::gpu::GraphPrismOp::Forward
13914        } else {
13915            crate::gpu::GraphPrismOp::None
13916        };
13917        let gw = crate::gpu::GraphW {
13918            idx: i,
13919            kind,
13920            row_scale: rs,
13921            data: &[],
13922            prism,
13923            affine: crate::prism::is_affine_target(&model, name),
13924        };
13925        let rows = self.weights.lm_head.rows();
13926        let mut h = hidden.to_vec();
13927        let mut lg = Vec::new();
13928        let mut layers_run = 0usize;
13929        let outcome = crate::gpu::forward_token_graph(
13930            &model,
13931            self.graph_kv_id,
13932            &[],
13933            &[],
13934            self.o1_epoch,
13935            &self.inv_freq,
13936            &mut h,
13937            self.num_heads,
13938            self.num_kv_heads,
13939            self.head_dim,
13940            self.attn_scale,
13941            self.head_dim,
13942            self.hidden_size,
13943            self.intermediate_size,
13944            0,
13945            self.kv_cache.max_seq_len,
13946            self.norm_style == cortiq_core::NormStyle::Gemma,
13947            self.rms_eps as f32,
13948            Some((&gw, rows)),
13949            &self.weights.final_norm,
13950            &mut lg,
13951            &[],
13952            1,
13953            None,
13954            None,
13955            Some(&mut layers_run),
13956            0,
13957            false,
13958        );
13959        if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
13960            return None;
13961        }
13962        // The same post-steps the host head applies — keep sampling
13963        // identical whichever arm produced the logits.
13964        lg.resize(self.vocab_size, 0.0);
13965        if let Some(m) = self.logit_multiplier {
13966            for l in lg.iter_mut() {
13967                *l *= m;
13968            }
13969        }
13970        if let Some(c) = self.final_softcap {
13971            softcap_in_place(&mut lg, c, self.pool.as_deref());
13972        }
13973        Some(lg)
13974    }
13975
13976    /// Record one token-layer's winners into the route-frequency profile and
13977    /// write it out every 16 tokens when `CMF_M3_PROFILE_SAVE=<path>` is set.
13978    fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
13979        if winners.is_empty() {
13980            return;
13981        }
13982        if self.m3_n_exp == 0 {
13983            self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
13984            self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
13985        }
13986        for &e in winners {
13987            let e = e as usize;
13988            if e < self.m3_n_exp && li < self.num_layers {
13989                self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
13990            }
13991        }
13992        if li + 1 == self.num_layers {
13993            self.m3_tokens += 1;
13994            if self.m3_tokens % 16 == 0 {
13995                if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
13996                    let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
13997                    let _ = std::fs::write(&path, bytes);
13998                }
13999            }
14000        }
14001    }
14002
14003    /// Admit the most frequent (layer, expert) pairs of a saved profile into
14004    /// the bank before the first token: `CMF_M3_PROFILE=<path>` names the
14005    /// file, `CMF_M3_PREWARM_N` (default 300) bounds how many experts go in.
14006    /// The bank's own eviction stays in charge; prewarm only chooses order.
14007    fn m3_prewarm(&mut self) {
14008        let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
14009            return;
14010        };
14011        let Ok(bytes) = std::fs::read(&path) else {
14012            tracing::warn!("m3 prewarm: cannot read profile {path}");
14013            return;
14014        };
14015        let counts: Vec<u32> = bytes
14016            .chunks_exact(4)
14017            .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
14018            .collect();
14019        let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
14020        if counts.len() != self.num_layers * n_exp {
14021            tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
14022            return;
14023        }
14024        let limit: usize = std::env::var("CMF_M3_PREWARM_N")
14025            .ok()
14026            .and_then(|v| v.parse().ok())
14027            .unwrap_or(300);
14028        let mut ranked: Vec<(u32, usize, usize)> = counts
14029            .iter()
14030            .enumerate()
14031            .filter(|(_, c)| **c > 0)
14032            .map(|(i, &c)| (c, i / n_exp, i % n_exp))
14033            .collect();
14034        ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
14035        ranked.truncate(limit);
14036        let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
14037        for (_, li, e) in ranked {
14038            per_layer[li].push(e);
14039        }
14040        let mut admitted = 0usize;
14041        for (li, ids) in per_layer.iter().enumerate() {
14042            if ids.is_empty() {
14043                continue;
14044            }
14045            let lw = &self.weights.layers[self.phys_layer(li)];
14046            let FfnKind::Moe(m) = &lw.ffn else {
14047                continue;
14048            };
14049            // Arming installs the layer's layout; nothing predicted yet.
14050            let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
14051            if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
14052                admitted += ids.len();
14053            }
14054        }
14055        tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
14056    }
14057
14058    /// One MiniMax tail layer through the pooled token-graph arm: attention,
14059    /// the pre-FFN norm, device routing and the descriptor-indexed expert
14060    /// bank in ONE submit (one sync instead of host attention + a MoE
14061    /// frame), with cold experts finished on the host against the read-back
14062    /// FFN input and admitted for the next token.
14063    ///
14064    /// `Ok(true)` — the layer is done (hidden replaced). `Ok(false)` —
14065    /// decline before any device mutation; the ordinary host path owns this
14066    /// position. `Err(())` — the device failed after admission; the caller
14067    /// must stop the sequence exactly like a failed span run.
14068    #[cfg(feature = "gpu")]
14069    fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
14070        if self.o1_active() || self.attn_softcap > 0.0 {
14071            return Ok(false);
14072        }
14073        if !self.m3_prewarmed {
14074            self.m3_prewarmed = true;
14075            self.m3_prewarm();
14076        }
14077        let frame = {
14078            let lw = &self.weights.layers[self.phys_layer(li)];
14079            let FfnKind::Moe(m) = &lw.ffn else {
14080                return Ok(false);
14081            };
14082            if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
14083                return Ok(false);
14084            }
14085            let t_arm = std::time::Instant::now();
14086            if self.m3_winners.len() != self.num_layers {
14087                self.m3_winners = vec![Vec::new(); self.num_layers];
14088            }
14089            let pred = std::mem::take(&mut self.m3_winners[li]);
14090            let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
14091            if armed.is_none() {
14092                self.m3_winners[li] = pred; // declined before anything ran
14093            }
14094            M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14095            match armed {
14096                Some(frame) => frame,
14097                // First-call refusal (geometry/bank): the host layer still
14098                // runs; nothing was mutated.
14099                None => return Ok(false),
14100            }
14101        };
14102        crate::gpu_wgpu::pooled_frame_enter(frame);
14103        // The pooled call must not fold lm_head: its cold experts have to
14104        // land before any final norm. The decode loop re-arms this flag for
14105        // the next position anyway.
14106        let want_logits = self.graph_want_logits;
14107        self.graph_want_logits = false;
14108        let mut lg = Vec::new();
14109        let mut gl = 0usize;
14110        let t_span = std::time::Instant::now();
14111        let span = crate::gpu::gpu_scope(|| {
14112            self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
14113        });
14114        self.graph_want_logits = want_logits;
14115        M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14116        let hh = match span {
14117            Some(Ok(hh)) => hh,
14118            Some(Err(())) => return Err(()),
14119            None => {
14120                // Declined before submission (seed/resolve/refusal): the
14121                // device mirror has not moved, so the host path is exact.
14122                return Ok(false);
14123            }
14124        };
14125        if gl != 1 {
14126            // A single-layer run that reports no layer mutated nothing we
14127            // may build on; anything else is a contract break.
14128            return if gl == 0 { Ok(false) } else { Err(()) };
14129        }
14130        *h = hh;
14131        let winners = crate::gpu_wgpu::pooled_frame_take_winners();
14132        self.m3_note_route(li, &winners);
14133        if self.m3_winners.len() != self.num_layers {
14134            self.m3_winners = vec![Vec::new(); self.num_layers];
14135        }
14136        self.m3_winners[li] = winners;
14137        let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
14138        // A non-finite parsed weight would poison the residual for the rest
14139        // of the sequence — drop such entries loudly instead.
14140        cold.retain(|&(_, w)| {
14141            if w.is_finite() {
14142                true
14143            } else {
14144                tracing::error!("pooled tail: non-finite cold weight from expert route");
14145                false
14146            }
14147        });
14148        if cold.is_empty() {
14149            return Ok(true);
14150        }
14151        let x = crate::gpu_wgpu::pooled_frame_take_input();
14152        if x.len() != self.hidden_size {
14153            return Err(());
14154        }
14155        // Cold experts: on the device first (admit, then one prepared frame
14156        // over exactly those slots — one submit for the layer). The host
14157        // computes them only when the device declines, and then they are
14158        // admitted for the next token. `CMF_M3_COLD_CPU=1` forces the host
14159        // arm for an A/B measurement.
14160        let lw = &self.weights.layers[self.phys_layer(li)];
14161        let FfnKind::Moe(m) = &lw.ffn else {
14162            return Err(());
14163        };
14164        let t_cold = std::time::Instant::now();
14165        let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
14166        let mut out = crate::attention::take_buf(self.hidden_size);
14167        let on_device = !force_host
14168            && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
14169        if !on_device {
14170            crate::attention::recycle_buf(&mut out);
14171            let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
14172                .iter()
14173                .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
14174                .collect::<Result<_, ()>>()?;
14175            out = crate::gpu::cpu_scope(|| {
14176                crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
14177            });
14178            let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
14179            let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
14180        }
14181        M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14182        M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
14183        if out.len() != h.len() {
14184            crate::attention::recycle_buf(&mut out);
14185            return Err(());
14186        }
14187        for (dst, src) in h.iter_mut().zip(out.iter()) {
14188            *dst += src;
14189        }
14190        crate::attention::recycle_buf(&mut out);
14191        if on_device {
14192            M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
14193        }
14194        Ok(true)
14195    }
14196
14197    #[cfg(not(feature = "gpu"))]
14198    fn m3_pooled_layer(
14199        &mut self,
14200        _li: usize,
14201        _position: usize,
14202        _h: &mut Vec<f32>,
14203    ) -> Result<bool, ()> {
14204        Ok(false)
14205    }
14206
14207    /// Greedy burst: forward `t_next` and let the device pick + re-embed
14208    /// the next k−1 tokens — k frames, ONE submit, k ids back. The ZML
14209    /// trade, on wgpu. None ⇒ caller keeps the per-token path.
14210    fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
14211        if self.o1_active() || self.attn_softcap > 0.0 {
14212            return None;
14213        }
14214        // The burst builds the whole-token graph; attention the graph's
14215        // per-layer geometry cannot express keeps the per-token path.
14216        if let Some(reason) = self.wgpu_graph_attn_decline() {
14217            self.note_graph_decline("wgpu multi-burst", reason);
14218            return None;
14219        }
14220        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
14221        if !graph_on || self.graph_refused() {
14222            // Same memo as the decode site: this path builds the very
14223            // same graph, so a model it cannot build for must not be
14224            // walked again here either. Missing this guard was worth
14225            // 2.5x on an Adreno — 0.361 tok/s against 0.905 — because
14226            // the burst retried per token what decode had already given
14227            // up on.
14228            return None;
14229        }
14230        let emb = self.embed_single(t_next);
14231        let mut lg = Vec::new();
14232        let mut ids = Vec::new();
14233        match self.try_token_graph_wgpu_steps(
14234            &emb,
14235            position,
14236            &mut lg,
14237            k,
14238            Some(&mut ids),
14239            None,
14240            0,
14241            self.num_layers,
14242        ) {
14243            Some(Ok(_)) => {}
14244            Some(Err(())) => {
14245                // Preserve the backend's post-admission failure through the
14246                // Option-based burst API.  The decode caller consumes this
14247                // flag and clears the sequence instead of falling through
14248                // to a stale CPU recurrent state.
14249                self.graph_failed
14250                    .store(true, std::sync::atomic::Ordering::Relaxed);
14251                return None;
14252            }
14253            None => return None,
14254        }
14255        (ids.len() == k).then_some(ids)
14256    }
14257
14258    /// Multi-step greedy: k whole frames in ONE submit, argmax and re-embed
14259    /// on the device. `ids_out` receives the k winner ids; the hidden/logits
14260    /// outputs are NOT produced in that mode.
14261    fn try_token_graph_wgpu_steps(
14262        &self,
14263        hidden: &[f32],
14264        position: usize,
14265        logits_out: &mut Vec<f32>,
14266        steps: usize,
14267        ids_out: Option<&mut Vec<u32>>,
14268        layers_run: Option<&mut usize>,
14269        from: usize,
14270        upto_excl: usize,
14271    ) -> Option<Result<Vec<f32>, ()>> {
14272        // SenseNova-Loop: MRoPE prompt rows rotate on the host; the graph's
14273        // mHC kernels take two streams only, and a graph must not enter an
14274        // mHC range midway (its streams would be the host's).
14275        if self.prompt_mrope.is_some()
14276            || self.mhc.as_deref().is_some_and(|m| {
14277                from < self.num_layers && m.on(self.phys_layer(from)) && !m.enters(self.phys_layer(from))
14278            })
14279            || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
14280            || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
14281                && (from..upto_excl.min(self.num_layers)).any(|li| self.mhc_layer(li))
14282        {
14283            return None;
14284        }
14285        // The bank has already reserved its VRAM. Never build a second
14286        // all-expert arena across bank-owned layers (including bursts).
14287        let upto_excl = match self.mimo_moe.graph_prefix_end() {
14288            Some(end) if end < upto_excl => {
14289                if steps != 1 || layers_run.is_none() || from >= end {
14290                    return None;
14291                }
14292                end
14293            }
14294            _ => upto_excl,
14295        };
14296        // O(1) Nyström decode runs off the sealed state, not the KV cache the
14297        // graph mirrors — never take the graph while o1 is active.
14298        let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
14299        if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
14300            // Softcapped scores have no graph kernel yet — CPU owns them.
14301            // o1 rides the graph only behind CMF_O1_GPU=1 while the port
14302            // proves itself; without it the CPU path owns o1 as before.
14303            return None;
14304        }
14305        // Per-layer KV heads, narrow V, sinks and sliding windows ride
14306        // `GraphAttn::Full::geom` (the ATTEND_X kernels). Anything that
14307        // geometry cannot express declines here, by name — before the
14308        // per-layer gate existed a sliding/sink model ran the graph as
14309        // full-context attention, fluent and wrong. The caller memoizes
14310        // the refusal.
14311        if let Some(reason) = self.wgpu_graph_attn_decline() {
14312            self.note_graph_decline("wgpu token graph", reason);
14313            return None;
14314        }
14315        // Per-layer sealed o1 state for the graph. During prefill the
14316        // state is still Collecting -> views are None -> the graph
14317        // refuses below and the CPU prefill records the q trace and
14318        // seals, exactly as the o1 design requires.
14319        let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
14320            .map(|li| {
14321                if !o1_gpu {
14322                    return None;
14323                }
14324                self.kv_cache.layers[self.phys_layer(li)].o1_views()
14325            })
14326            .collect();
14327        if self.o1_active() && o1_gpu {
14328            // Any o1 layer not sealed (or degenerate exact-only) keeps the
14329            // whole token on the CPU: half-graph forwards would desync.
14330            let want: usize = (from..upto_excl)
14331                .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
14332                .count();
14333            let have = o1_views.iter().filter(|v| v.is_some()).count();
14334            if want == 0 || have != want {
14335                // The silent twin of the gpu-side o1 gates, found the
14336                // same way: a 15x decode drop with an empty log. Views
14337                // stay None until the layer's state SEALS, so `have`
14338                // lagging `want` early in a run is the o1 design working
14339                // — but it must say so, or the next reader spends a
14340                // night proving the kernels innocent.
14341                // On CHANGE, not once: the first decline is the legal
14342                // unsealed prefill, and a once-print buries the state
14343                // that matters — what the count reads AFTER the seal.
14344                use std::sync::atomic::{AtomicUsize, Ordering};
14345                static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
14346                let code = have * 1000 + want;
14347                if LAST.swap(code, Ordering::Relaxed) != code {
14348                    tracing::warn!(
14349                        "o1 graph: {have} of {want} layers sealed — per-op until all seal"
14350                    );
14351                }
14352                return None;
14353            }
14354        }
14355        let nh = self.num_heads;
14356        let (nkv, hd, rd) = self.layer_geom(0);
14357        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14358        let mut layers = Vec::with_capacity(upto_excl - from);
14359        let mut model = None;
14360        let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
14361        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14362            // `blr` rides as GRAPH_KIND_BLR (two stage dispatches).
14363            if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
14364                let name = &m.tensors[i].name;
14365                let prism = if crate::prism::is_inverse_embedding(m, name) {
14366                    crate::gpu::GraphPrismOp::InverseEmbedding
14367                } else if crate::prism::is_forward_weight(m, name) {
14368                    crate::gpu::GraphPrismOp::Forward
14369                } else {
14370                    crate::gpu::GraphPrismOp::None
14371                };
14372                return Some(crate::gpu::GraphW {
14373                    idx: i,
14374                    kind,
14375                    row_scale: rs,
14376                    data: &[],
14377                    prism,
14378                    affine: crate::prism::is_affine_target(m, name),
14379                });
14380            }
14381            // Small unquantized projections (GDN in_proj_a/b) stay f32.
14382            match t.as_f32() {
14383                Some(d) => Some(crate::gpu::GraphW {
14384                    idx: 0,
14385                    kind: 4,
14386                    row_scale: &[],
14387                    data: d,
14388                    prism: crate::gpu::GraphPrismOp::None,
14389                    affine: false,
14390                }),
14391                None => {
14392                    if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14393                        eprintln!("batch graph: weight has no graph/f32 representation");
14394                    }
14395                    None
14396                }
14397            }
14398        }
14399        for li in from..upto_excl {
14400            let lw = &self.weights.layers[self.phys_layer(li)];
14401            if dbg {
14402                let ak = match &lw.attn {
14403                    AttnKind::Mla(_) => "Mla".into(),
14404                    AttnKind::Full {
14405                        output_gate, bias, ..
14406                    } => format!("Full gate={output_gate} bias={}", bias.is_some()),
14407                    AttnKind::LinearGdn(_) => "LinearGdn".into(),
14408                    AttnKind::Kda(_) => "Kda".into(),
14409                    AttnKind::Linear(_) => "Linear".into(),
14410                    AttnKind::ShortConv(_) => "ShortConv".into(),
14411                    AttnKind::Bounded(_) => "Bounded".into(),
14412                };
14413                let fk = match &lw.ffn {
14414                    FfnKind::Dense(_) => "Dense",
14415                    FfnKind::Moe(_) => "Moe",
14416                    FfnKind::DenseMoe(_) => "DenseMoe",
14417                };
14418                eprintln!("graph L{li}: attn={ak} ffn={fk}");
14419            }
14420            let gffn = match &lw.ffn {
14421                FfnKind::DenseMoe(_) => return None, // dual branch: CPU path
14422                // A tube layer is several matrices, not one — the
14423                // whole-layer graph has no shape for it yet.
14424                FfnKind::Dense(d) if !d.segs.is_empty() => return None,
14425                FfnKind::Dense(d) => {
14426                    // An activation the graph has no kernel arm for keeps
14427                    // the CPU/per-op owner, by name — the dense graph FFN
14428                    // used to compute SiLU for whatever the model asked.
14429                    let Some(act) = d.act.wgpu_graph_act() else {
14430                        self.note_graph_decline(
14431                            "wgpu token graph",
14432                            "dense FFN activation without a graph kernel",
14433                        );
14434                        return None;
14435                    };
14436                    crate::gpu::GraphFfn::Dense {
14437                        gate: gw(&d.gate_proj)?,
14438                        up: gw(&d.up_proj)?,
14439                        down: gw(&d.down_proj)?,
14440                        act,
14441                    }
14442                }
14443                FfnKind::Moe(m) => {
14444                    // Adaptive τ and expert masks keep the CPU path, where
14445                    // they are implemented. Sigmoid routing with a selection
14446                    // bias (LFM2-MoE / DeepSeek noaux_tc), a routed scale ≠ 1
14447                    // and an UNGATED shared expert (HunYuan hy_v3: ×2.826 on
14448                    // the routed mix, the shared expert at weight 1) are all
14449                    // graphed — before, every such token fell to the per-op
14450                    // path whole (145 submits/token on Hy-MT2-30B-A3B).
14451                    if m.route_tau.is_some() || m.mask.is_some() {
14452                        return None;
14453                    }
14454                    let shared = m.shared.as_ref();
14455                    // The resident MoE kernels do not yet carry the
14456                    // descriptor-aware transform through router/shared-gate
14457                    // selection.  Refuse the complete layer instead of
14458                    // scoring with an untransformed Prism plane (the dense
14459                    // path has an explicit FWHT boundary below).
14460                    let inter = m.experts.first()?.gate_proj.rows();
14461                    // Flint's shared expert is a 1024-wide SwiGLU while its
14462                    // routed bank is 512-wide.  The resident MoE shader
14463                    // indexes a uniform-width packed bank, so keep that
14464                    // expert out of the routed slots and add it through the
14465                    // dense q4tp path after routed accumulation.  This is
14466                    // also the safe route for a mixed q2-routed/q4-shared
14467                    // artifact: neither layout nor row stride is faked.
14468                    let detached_shared = match shared {
14469                        Some((se, None))
14470                            if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14471                        {
14472                            if !matches!(se.act, Act::Silu)
14473                                || se.gate_proj.mapped_q4tp().is_none()
14474                                || se.up_proj.mapped_q4tp().is_none()
14475                                || se.down_proj.mapped_q4tp().is_none()
14476                            {
14477                                return None;
14478                            }
14479                            Some(crate::gpu::GraphMoeDetachedShared {
14480                                gate: gw(&se.gate_proj)?,
14481                                up: gw(&se.up_proj)?,
14482                                down: gw(&se.down_proj)?,
14483                                width: se.gate_proj.rows(),
14484                            })
14485                        }
14486                        // A separately shaped gated shared expert needs a
14487                        // second sigmoid gate path.  Do not turn it into an
14488                        // ungated one merely to make the graph run.
14489                        Some((se, Some(_)))
14490                            if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14491                        {
14492                            return None;
14493                        }
14494                        _ => None,
14495                    };
14496                    let has_shared = shared.is_some() && detached_shared.is_none();
14497                    let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
14498                    let sgate = match shared {
14499                        Some((_, Some(sg))) if has_shared => gw(sg)?,
14500                        // No gate, no shared expert, or a detached shared
14501                        // expert: the router only keeps the binding total;
14502                        // the selection shader does not read it.
14503                        _ => gw(&m.router)?,
14504                    };
14505                    let router = gw(&m.router)?;
14506                    // The resident MoE kernels do not yet carry the
14507                    // descriptor-aware transform through router/shared-gate
14508                    // selection.  Refuse the complete layer instead of
14509                    // scoring with an untransformed Prism plane (the dense
14510                    // path has an explicit FWHT boundary below).
14511                    if router.prism != crate::gpu::GraphPrismOp::None
14512                        || sgate.prism != crate::gpu::GraphPrismOp::None
14513                        || router.affine
14514                        || sgate.affine
14515                    {
14516                        tracing::warn!(
14517                            "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
14518                        );
14519                        return None;
14520                    }
14521                    let in_bank_shared = shared.filter(|_| has_shared).map(|(se, _)| se);
14522                    let edge = |t: &QTensor| {
14523                        t.graph_weight()
14524                            .or_else(|| t.graph_weight_descriptor())
14525                            .map(|(mm, i, _, _)| (std::sync::Arc::as_ptr(mm) as usize, i))
14526                    };
14527                    let bank_key: Option<GraphMoeKey> = match (
14528                        m.experts.first().and_then(|e| edge(&e.gate_proj)),
14529                        m.experts.last().and_then(|e| edge(&e.down_proj)),
14530                    ) {
14531                        (Some((fm, fi)), Some((_, di))) if graph_moe_bank_on() => Some((
14532                            self.phys_layer(li),
14533                            m.experts.as_ptr() as usize,
14534                            m.experts.len(),
14535                            in_bank_shared.map_or(0, |se| std::ptr::from_ref(se) as usize),
14536                            fm,
14537                            fi,
14538                            di,
14539                        )),
14540                        _ => None,
14541                    };
14542                    let cached =
14543                        bank_key.and_then(|k| self.graph_moe_banks.lock().unwrap().get(&k).cloned());
14544                    let bank = if let Some(b) = cached {
14545                        b
14546                    } else {
14547                    let mut experts = Vec::with_capacity(m.experts.len() + 1);
14548                    let mut bank_model: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14549                    // q4t or q4tp, but not both in one layer — the kernels
14550                    // are picked per layer, not per expert.
14551                    let mut q4tp: Option<bool> = None;
14552                    // The mixed 2-bit profile: q2tp gate/up over a q4tp
14553                    // down. Uniform across the layer, like `q4tp` itself.
14554                    let mut gu_q2: Option<bool> = None;
14555                    // q8_2f experts (gate, up and down): their own kernels.
14556                    let mut q82: Option<bool> = None;
14557                    // One MoE gate/up kernel serves the complete packed bank.
14558                    // Bind an activation only when every routed (and in-bank
14559                    // shared) expert owns the same exact WGPU contract.
14560                    let mut moe_act = None;
14561                    for e in m.experts.iter().chain(in_bank_shared.into_iter()) {
14562                        let Some(e_act) = e.act.wgpu_graph_act() else {
14563                            return None;
14564                        };
14565                        match moe_act {
14566                            Some(existing) if existing != e_act => return None,
14567                            None => moe_act = Some(e_act),
14568                            _ => {}
14569                        }
14570                        if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
14571                            return None;
14572                        }
14573                        // Expert tensors are packed into one resident buffer
14574                        // and the MoE kernels have no transform slot per
14575                        // expert.  Keep the CPU/per-op owner for Prism or
14576                        // affine experts rather than silently using raw bytes.
14577                        for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
14578                            let Some((em, ei, _, _)) = expert_weight
14579                                .graph_weight()
14580                                .or_else(|| expert_weight.graph_weight_descriptor())
14581                            else {
14582                                return None;
14583                            };
14584                            let name = &em.tensors[ei].name;
14585                            if crate::prism::is_forward_weight(em, name)
14586                                || crate::prism::is_inverse_embedding(em, name)
14587                                || crate::prism::is_affine_target(em, name)
14588                            {
14589                                tracing::warn!(
14590                                    "resident MoE declined: expert Prism/affine transform is not implemented"
14591                                );
14592                                return None;
14593                            }
14594                        }
14595                        let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
14596                            Some((mm, gi)) => (
14597                                mm,
14598                                gi,
14599                                e.up_proj.mapped_q4t()?.1,
14600                                e.down_proj.mapped_q4t()?.1,
14601                                false,
14602                                false,
14603                                false,
14604                            ),
14605                            None => match e.gate_proj.mapped_q2tp() {
14606                                Some((mm, gi)) => (
14607                                    mm,
14608                                    gi,
14609                                    e.up_proj.mapped_q2tp()?.1,
14610                                    e.down_proj.mapped_q4tp()?.1,
14611                                    true,
14612                                    true,
14613                                    false,
14614                                ),
14615                                None => match e.gate_proj.mapped_q4tp() {
14616                                    Some((mm, gi)) => (
14617                                        mm,
14618                                        gi,
14619                                        e.up_proj.mapped_q4tp()?.1,
14620                                        e.down_proj.mapped_q4tp()?.1,
14621                                        true,
14622                                        false,
14623                                        false,
14624                                    ),
14625                                    // q8_2f experts: all three projections,
14626                                    // each with its own column field.
14627                                    None => {
14628                                        if !crate::gpu::moe_q82_graph_on() {
14629                                            return None;
14630                                        }
14631                                        let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14632                                        (
14633                                            mm,
14634                                            gi,
14635                                            e.up_proj.mapped_q8_2f()?.1,
14636                                            e.down_proj.mapped_q8_2f()?.1,
14637                                            false,
14638                                            false,
14639                                            true,
14640                                        )
14641                                    }
14642                                },
14643                            },
14644                        };
14645                        if *q4tp.get_or_insert(is_p) != is_p
14646                            || *gu_q2.get_or_insert(is_q2) != is_q2
14647                            || *q82.get_or_insert(is_8) != is_8
14648                        {
14649                            // The shared expert rides in the same packed
14650                            // buffer as the routed ones, so a layer that
14651                            // mixes layouts cannot be indexed by one stride.
14652                            // Say so: the symptom is a whole model quietly
14653                            // running its MoE on the CPU.
14654                            tracing::warn!(
14655                                "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."
14656                            );
14657                            return None;
14658                        }
14659                        bank_model.get_or_insert_with(|| mm.clone());
14660                        experts.push((gi, ui, di));
14661                    }
14662                    let b = GraphMoeBank {
14663                        experts,
14664                        q4tp,
14665                        gu_q2,
14666                        q82,
14667                        act: moe_act,
14668                        model: bank_model,
14669                    };
14670                    if let Some(k) = bank_key {
14671                        self.graph_moe_banks.lock().unwrap().insert(k, b.clone());
14672                    }
14673                    b
14674                    };
14675                    if let Some(mm) = &bank.model {
14676                        model.get_or_insert_with(|| mm.clone());
14677                    }
14678                    crate::gpu::GraphFfn::Moe {
14679                        router,
14680                        shared_gate: sgate,
14681                        experts: bank.experts,
14682                        n_exp: m.experts.len(),
14683                        // CMF_TOPK_PROBE: timing probe only — output is WRONG.
14684                        // Fewer experts shrink the MoE arithmetic while the
14685                        // dispatch count stays identical, which is the only
14686                        // clean way to tell a launch-bound decode from a
14687                        // compute-bound one.
14688                        top_k: std::env::var("CMF_TOPK_PROBE")
14689                            .ok()
14690                            .and_then(|v| v.parse::<usize>().ok())
14691                            .filter(|k| *k > 0 && *k <= m.top_k)
14692                            .unwrap_or(m.top_k),
14693                        inter,
14694                        act: bank.act?,
14695                        norm_topk: m.norm_topk_prob,
14696                        q4tp: bank.q4tp?,
14697                        gu_q2: bank.gu_q2.unwrap_or(false),
14698                        q82: bank.q82.unwrap_or(false),
14699                        sigmoid: m.router_sigmoid,
14700                        sigmoid_denom_eps: m.sigmoid_denom_eps,
14701                        bias: m.expert_bias.as_deref(),
14702                        has_shared,
14703                        detached_shared,
14704                        shared_gated,
14705                        route_scale: m.routed_scaling,
14706                    }
14707                }
14708            };
14709            let attn = match &lw.attn {
14710                AttnKind::Full {
14711                    wq,
14712                    wk,
14713                    wv,
14714                    wo,
14715                    q_norm,
14716                    k_norm,
14717                    output_gate,
14718                    softplus_gate,
14719                    bias,
14720                } => {
14721                    if self.attention_heads_per_layer.is_some() {
14722                        return None;
14723                    }
14724                    // A projected output gate rides the graph in one form:
14725                    // Spark-X2.5's head-wise sigmoid (one g_proj row per Q
14726                    // head). Laguna's softplus gate keeps the CPU path.
14727                    let head_gate = match softplus_gate {
14728                        None => None,
14729                        Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
14730                        Some(_) => {
14731                            self.note_graph_decline(
14732                                "wgpu token graph",
14733                                "projected softplus / per-element output gate",
14734                            );
14735                            return None;
14736                        }
14737                    };
14738                    let (m, _, _, _) = wq.graph_weight_or_blr()?;
14739                    model = Some(m.clone());
14740                    crate::gpu::GraphAttn::Full {
14741                        wq: gw(wq)?,
14742                        wk: gw(wk)?,
14743                        wv: gw(wv)?,
14744                        wo: gw(wo)?,
14745                        q_norm: q_norm.as_deref(),
14746                        k_norm: k_norm.as_deref(),
14747                        late_qk_norm: self.qk_norm_after_rope,
14748                        bias: bias
14749                            .as_ref()
14750                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14751                        output_gate: *output_gate,
14752                        cpu_k: self.kv_cache.layers[li].k_heads(),
14753                        cpu_v: self.kv_cache.layers[li].v_heads(),
14754                        cpu_base: self.kv_cache.layers[li].base(),
14755                        geom: self.graph_attn_geom(li),
14756                        head_gate,
14757                    }
14758                }
14759                AttnKind::LinearGdn(w) => {
14760                    let cfg = self.gdn_cfg?;
14761                    let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
14762                    model = Some(m.clone());
14763                    crate::gpu::GraphAttn::Gdn {
14764                        qkv: gw(&w.in_proj_qkv)?,
14765                        z: gw(&w.in_proj_z)?,
14766                        a: gw(&w.in_proj_a)?,
14767                        b: gw(&w.in_proj_b)?,
14768                        out: gw(&w.out_proj)?,
14769                        conv1d: &w.conv1d,
14770                        a_log: &w.a_log,
14771                        dt_bias: &w.dt_bias,
14772                        norm: &w.norm,
14773                        nv: cfg.num_v_heads,
14774                        nk: cfg.num_k_heads,
14775                        dk: cfg.key_head_dim,
14776                        dv: cfg.value_head_dim,
14777                        kk: cfg.conv_kernel,
14778                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14779                        preview: None,
14780                    }
14781                }
14782                AttnKind::ShortConv(w) => {
14783                    let cfg = self.short_conv_cfg?;
14784                    let (m, _, _, _) = w
14785                        .in_proj
14786                        .graph_weight()
14787                        .or_else(|| w.in_proj.graph_weight_descriptor())?;
14788                    model = Some(m.clone());
14789                    crate::gpu::GraphAttn::ShortConv {
14790                        inp: gw(&w.in_proj)?,
14791                        out: gw(&w.out_proj)?,
14792                        taps: &w.conv,
14793                        kernel: cfg.kernel,
14794                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14795                    }
14796                }
14797                _ => return None,
14798            };
14799            layers.push(crate::gpu::GraphLayer {
14800                mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
14801                input_norm: &lw.input_norm,
14802                attn,
14803                post_norm: &lw.post_norm,
14804                attn_out_norm: lw.attn_out_norm.as_deref(),
14805                ffn_out_norm: lw.ffn_out_norm.as_deref(),
14806                layer_scale: lw.layer_scale,
14807                ffn: gffn,
14808            });
14809        }
14810        let model = model?;
14811        // Fold final-norm + lm_head into the graph when this call wants logits
14812        // and the lm_head is a graphable (quantized) weight — the graph then
14813        // reads back logits (into logits_out) instead of the hidden, dropping
14814        // the separate CPU/GPU lm_head op + its sync. Never the f32 fallback:
14815        // an unquantized lm_head is vocab·hidden and must not be uploaded.
14816        let lm_gw = if upto_excl == self.num_layers
14817            && self.graph_want_logits
14818            && std::env::var("CMF_GPU_LMHEAD")
14819                .map(|v| v != "0")
14820                .unwrap_or(true)
14821        {
14822            self.weights
14823                .lm_head
14824                .graph_weight()
14825                .or_else(|| self.weights.lm_head.graph_weight_descriptor())
14826                .map(|(m, i, kind, rs)| {
14827                    let name = &m.tensors[i].name;
14828                    let prism = if crate::prism::is_inverse_embedding(m, name) {
14829                        crate::gpu::GraphPrismOp::InverseEmbedding
14830                    } else if crate::prism::is_forward_weight(m, name) {
14831                        crate::gpu::GraphPrismOp::Forward
14832                    } else {
14833                        crate::gpu::GraphPrismOp::None
14834                    };
14835                    (
14836                        crate::gpu::GraphW {
14837                            idx: i,
14838                            kind,
14839                            row_scale: rs,
14840                            data: &[],
14841                            prism,
14842                            affine: crate::prism::is_affine_target(m, name),
14843                        },
14844                        self.weights.lm_head.rows(),
14845                    )
14846                })
14847        } else {
14848            None
14849        };
14850        let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
14851        // Multi-step re-embeds the winner on the device.
14852        let emb_gw = if steps > 1 {
14853            self.weights
14854                .embed_tokens
14855                .graph_weight()
14856                .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
14857                .map(|(m, i, kind, rs)| {
14858                    let name = &m.tensors[i].name;
14859                    let prism = if crate::prism::is_inverse_embedding(m, name) {
14860                        crate::gpu::GraphPrismOp::InverseEmbedding
14861                    } else if crate::prism::is_forward_weight(m, name) {
14862                        crate::gpu::GraphPrismOp::Forward
14863                    } else {
14864                        crate::gpu::GraphPrismOp::None
14865                    };
14866                    (
14867                        crate::gpu::GraphW {
14868                            idx: i,
14869                            kind,
14870                            row_scale: rs,
14871                            data: &[],
14872                            prism,
14873                            affine: crate::prism::is_affine_target(m, name),
14874                        },
14875                        self.weights.embed_tokens.rows(),
14876                        self.embed_multiplier,
14877                    )
14878                })
14879        } else {
14880            None
14881        };
14882
14883        // Loop boundaries: virtual layer indices after which final_norm is
14884        // applied (mid-stack only; the GLOBAL last layer's norm folds into
14885        // lm_head). Span-relative — the executor compares its enumerate
14886        // index. A span ending mid-stack keeps its boundary norm even when
14887        // it is the span's own last layer.
14888        let loop_norm_at: Vec<usize> = if self.loop_final_norm {
14889            (from..upto_excl.min(self.num_layers - 1))
14890                .filter(|&li| (li + 1) % self.physical_layers == 0)
14891                .map(|li| li - from)
14892                .collect()
14893        } else {
14894            Vec::new()
14895        };
14896        let mut h = hidden.to_vec();
14897        // The normal decode path only needs the fused lm-head logits.  A
14898        // CMF_LOGIT_DUMP diagnostic, however, promises a prompt-boundary
14899        // post-stack hidden alongside those logits; request the existing
14900        // second readback only for that explicit probe instead of dumping
14901        // the input copy left in `h` by a folded-head graph.
14902        let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
14903        let outcome = crate::gpu::forward_token_graph(
14904            &model,
14905            self.graph_kv_id,
14906            &layers,
14907            &o1_views,
14908            self.o1_epoch,
14909            &self.inv_freq,
14910            &mut h,
14911            nh,
14912            nkv,
14913            hd,
14914            self.attn_scale,
14915            rd,
14916            self.hidden_size,
14917            self.intermediate_size,
14918            position,
14919            self.kv_cache.max_seq_len,
14920            gemma,
14921            self.rms_eps as f32,
14922            lm,
14923            &self.weights.final_norm,
14924            logits_out,
14925            &loop_norm_at,
14926            steps,
14927            emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
14928            ids_out,
14929            layers_run,
14930            from,
14931            dump_hidden,
14932        );
14933        match outcome {
14934            crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
14935            crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
14936            crate::gpu::TokenGraphOutcome::Declined => None,
14937        }
14938    }
14939
14940    /// Batched prefill: k contiguous prompt positions through the whole wgpu
14941    /// graph in ONE submit (projections/FFN as GEMMs). `hiddens` is [k·hidden]
14942    /// in/out (embeddings in, layer output out); KV mirror / GDN state advance.
14943    /// false ⇒ unsupported → caller keeps the per-position graph.
14944    /// The b-row Metal graph plan for the whole model: every layer as a
14945    /// GDN run or a full-attention item, all-or-nothing (a layer outside the
14946    /// graph's contract → None, the caller runs plain). Shared by the
14947    /// speculative verify and the batched prefill.
14948    #[cfg(target_os = "macos")]
14949    #[allow(clippy::type_complexity)]
14950    fn metal_rows_plan(
14951        &self,
14952    ) -> Option<(
14953        Vec<MetalRowsItem<'_>>,
14954        std::sync::Arc<cortiq_core::CmfModel>,
14955        Option<crate::gpu_metal::GdnGpuCfg>,
14956    )> {
14957        use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
14958        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
14959        if !graph_force
14960            || !crate::gpu::enabled_here()
14961            || std::env::var("CMF_GPU_BLOCK")
14962                .map(|v| v == "0")
14963                .unwrap_or(false)
14964            || self.attn_softcap > 0.0
14965            || self.o1_active()
14966            || self.swa.is_some()
14967            || self.global_attn.is_some()
14968            || self.attention_heads_per_layer.is_some()
14969            // per-layer KV heads, narrow V, learned sinks (MiMo-V2)
14970            || self.graph_attn_decline_reason().is_some()
14971            || self.attn_v_norm
14972            || self.loop_final_norm
14973            || !self.metal_rope_scale_ok()
14974        {
14975            return None;
14976        }
14977        let attend_contract = self.head_dim % 4 == 0
14978            && self.head_dim <= 256
14979            && self.rotary_dim >= 2
14980            && self.rotary_dim <= self.head_dim
14981            && (self.rotary_dim / 2) % 32 == 0
14982            && self.num_kv_heads > 0
14983            && self.num_heads % self.num_kv_heads == 0;
14984        if !attend_contract {
14985            return None;
14986        }
14987        let mut plan: Vec<MetalRowsItem> = Vec::new();
14988        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14989        for li in 0..self.num_layers {
14990            let lw = &self.weights.layers[self.phys_layer(li)];
14991            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14992                return None;
14993            }
14994            let ffn = match &lw.ffn {
14995                FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
14996                    let (Some(g), Some(u), Some(dn)) = (
14997                        d.gate_proj.metal_graph_parts(),
14998                        d.up_proj.metal_graph_parts(),
14999                        d.down_proj.metal_graph_parts(),
15000                    ) else {
15001                        return None;
15002                    };
15003                    MetalFfn::Dense {
15004                        gate: g,
15005                        up: u,
15006                        down: dn,
15007                        act: 0, // SiLU only (the arm above)
15008                    }
15009                }
15010                _ => return None,
15011            };
15012            match &lw.attn {
15013                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
15014                    let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
15015                        w.in_proj_qkv.metal_graph_parts(),
15016                        w.in_proj_z.metal_graph_parts(),
15017                        w.in_proj_a.f32_parts(),
15018                        w.in_proj_b.f32_parts(),
15019                        w.out_proj.metal_graph_parts(),
15020                    ) else {
15021                        return None;
15022                    };
15023                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
15024                        model_ref.get_or_insert_with(|| model.clone());
15025                    }
15026                    let gl = GdnGpuLayer {
15027                        mhc: None,
15028                        attn_norm: &lw.input_norm,
15029                        post_norm: &lw.post_norm,
15030                        qkv,
15031                        z,
15032                        a,
15033                        b: bb,
15034                        out,
15035                        ffn,
15036                        conv1d: &w.conv1d,
15037                        a_log: &w.a_log,
15038                        dt_bias: &w.dt_bias,
15039                        gnorm: &w.norm,
15040                    };
15041                    match plan.last_mut() {
15042                        Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
15043                        _ => plan.push(MetalRowsItem::Gdn {
15044                            run: vec![gl],
15045                            first: li,
15046                        }),
15047                    }
15048                }
15049                AttnKind::Full {
15050                    wq,
15051                    wk,
15052                    wv,
15053                    wo,
15054                    q_norm,
15055                    k_norm,
15056                    output_gate,
15057                    softplus_gate: None,
15058                    bias: None,
15059                } => {
15060                    let (Some(pq), Some(pk), Some(pv), Some(po)) = (
15061                        wq.metal_graph_parts(),
15062                        wk.metal_graph_parts(),
15063                        wv.metal_graph_parts(),
15064                        wo.metal_graph_parts(),
15065                    ) else {
15066                        return None;
15067                    };
15068                    if let QTensor::Mapped { model, .. } = wq {
15069                        model_ref.get_or_insert_with(|| model.clone());
15070                    }
15071                    let cache = &self.kv_cache.layers[li];
15072                    if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
15073                        return None;
15074                    }
15075                    plan.push(MetalRowsItem::Attn {
15076                        l: AttnGpuLayer {
15077                            mhc: None,
15078                            attn_norm: &lw.input_norm,
15079                            post_norm: &lw.post_norm,
15080                            wq: pq,
15081                            wk: pk,
15082                            wv: pv,
15083                            wo: po,
15084                            ffn,
15085                            attn_out_norm: None,
15086                            ffn_out_norm: None,
15087                            layer_scale: None,
15088                        },
15089                        li,
15090                        q_norm: q_norm.as_deref(),
15091                        k_norm: k_norm.as_deref(),
15092                        output_gate: *output_gate,
15093                    });
15094                }
15095                _ => return None,
15096            }
15097        }
15098        let model = model_ref?;
15099        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
15100            nv: cfg.num_v_heads,
15101            nk: cfg.num_k_heads,
15102            dk: cfg.key_head_dim,
15103            dv: cfg.value_head_dim,
15104            kk: cfg.conv_kernel,
15105            hidden: self.hidden_size,
15106            inter: self.intermediate_size,
15107            c_dim: cfg.conv_dim(),
15108            eps: cfg.rms_eps as f32,
15109            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15110        });
15111        Some((plan, model, gcfg))
15112    }
15113
15114    /// `AttnDeviceParams` for a plan item over the CPU cache as it stands.
15115    #[cfg(target_os = "macos")]
15116    #[allow(clippy::too_many_arguments)]
15117    fn metal_attn_params<'a>(
15118        li: usize,
15119        cache: &'a crate::kv_cache::LayerKvCache,
15120        q_norm: Option<&'a [f32]>,
15121        k_norm: Option<&'a [f32]>,
15122        output_gate: bool,
15123        inv_freq: &'a [f32],
15124        rope_scale: f32,
15125        geom: (usize, usize, usize, usize),
15126        pos0: usize,
15127        kv_id: u64,
15128        scale: f32,
15129        eps: f32,
15130        gemma: bool,
15131        late_qk_norm: bool,
15132    ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
15133        let (nh, nkv, hd, rd) = geom;
15134        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15135        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15136        let cpu_stored = cpu_k[0].len() / hd;
15137        (
15138            crate::gpu_metal::AttnDeviceParams {
15139                kv_id,
15140                layer: li,
15141                nh,
15142                nkv,
15143                hd,
15144                rd,
15145                position: pos0,
15146                scale,
15147                eps,
15148                gemma,
15149                late_qk_norm,
15150                output_gate,
15151                q_norm,
15152                k_norm,
15153                inv_freq,
15154                rope_scale,
15155                cpu_k,
15156                cpu_v,
15157                cpu_stored,
15158                cpu_gen: cache.generation(),
15159                o1: None,
15160                window: None,
15161                softmax_sink: cache.sinks.as_deref(),
15162                head_gate: None,
15163                v_norm: false,
15164            },
15165            cpu_stored,
15166        )
15167    }
15168
15169    /// Run the rows plan over `hiddens` (b rows at `pos0..`): validate,
15170    /// encode every item, optionally the head, sync. Returns the graph
15171    /// (for the commit / state finish) plus the GDN layer indices and the
15172    /// attention layers with the row count they were encoded against.
15173    #[cfg(target_os = "macos")]
15174    #[allow(clippy::type_complexity)]
15175    fn metal_rows_run(
15176        &mut self,
15177        hiddens: &mut [f32],
15178        pos0: usize,
15179        b: usize,
15180        prefill: bool,
15181        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15182        // Greedy verify: (row length scored, the b argmax ids out) — the
15183        // head's argmax runs on the device and the logits plane is NOT
15184        // read back (`spec.2` stays empty).
15185        mut argmax_out: Option<(usize, &mut Vec<u32>)>,
15186    ) -> MetalRowsRun {
15187        use crate::gpu_metal::{GraphDims, VerifyGraph};
15188        // The previous round's commit may still be replaying into the
15189        // trunk GDN owners on the second queue: this graph reads them
15190        // (zero-copy wraps) and may reallocate them below — collect the
15191        // replay first. Normally already complete (the draft chain ran
15192        // in between); a failed replay is terminal like a failed commit.
15193        if !crate::gpu_metal::wait_replay() {
15194            tracing::error!("Metal rows graph: the pending async replay failed");
15195            return MetalRowsRun::Failed;
15196        }
15197        spec_stamp("v.wait");
15198        // Seed the GDN recurrent records the rows graph reads — GDN layers
15199        // ONLY (the record the CPU path would allocate anyway).  Sizing
15200        // every layer here planted a zero GDN-sized record on a bounded
15201        // anchor of a natively bounded file this path then refused, and
15202        // that record counted as recurrent state on macOS.
15203        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
15204        if want > 0 {
15205            let phys = self.physical_layers.max(1);
15206            for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
15207                let is_gdn = self
15208                    .weights
15209                    .layers
15210                    .get(li % phys)
15211                    .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
15212                if is_gdn && l.linear_state.len() != want {
15213                    l.linear_state = vec![0f32; want];
15214                }
15215            }
15216        }
15217        let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
15218            return MetalRowsRun::Declined;
15219        };
15220        spec_stamp("v.plan");
15221        let dims = GraphDims {
15222            hidden: self.hidden_size,
15223            eps: self.rms_eps as f32,
15224            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15225        };
15226        let Some(mut graph) = (if prefill {
15227            VerifyGraph::new_prefill(&model, dims, hiddens, b)
15228        } else {
15229            VerifyGraph::new(&model, dims, hiddens, b)
15230        }) else {
15231            return MetalRowsRun::Declined;
15232        };
15233        let geom = (
15234            self.num_heads,
15235            self.num_kv_heads,
15236            self.head_dim,
15237            self.rotary_dim,
15238        );
15239        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15240        let eps = self.rms_eps as f32;
15241        let kv_id = self.graph_kv_id;
15242        let inv_freq = self.inv_freq.clone();
15243        for item in &plan {
15244            let ok = match item {
15245                MetalRowsItem::Gdn { run, .. } => gcfg
15246                    .as_ref()
15247                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
15248                    .unwrap_or(false),
15249                MetalRowsItem::Attn {
15250                    l,
15251                    li,
15252                    q_norm,
15253                    k_norm,
15254                    output_gate,
15255                } => {
15256                    let (p, _) = Self::metal_attn_params(
15257                        *li,
15258                        &self.kv_cache.layers[*li],
15259                        *q_norm,
15260                        *k_norm,
15261                        *output_gate,
15262                        &inv_freq,
15263                        self.layer_rope_scale(*li),
15264                        geom,
15265                        pos0,
15266                        kv_id,
15267                        self.attn_scale,
15268                        eps,
15269                        gemma,
15270                        self.qk_norm_after_rope,
15271                    );
15272                    graph.attn_ok(l, &p)
15273                }
15274            };
15275            if !ok {
15276                use std::sync::atomic::{AtomicBool, Ordering};
15277                static SAID: AtomicBool = AtomicBool::new(false);
15278                if !SAID.swap(true, Ordering::Relaxed) {
15279                    tracing::warn!("metal rows graph: a layer failed preflight — declining");
15280                }
15281                return MetalRowsRun::Declined;
15282            }
15283        }
15284        let lm = match &spec {
15285            Some((lm, _, _)) => {
15286                if !graph.lm_head_ok(*lm) {
15287                    return MetalRowsRun::Declined;
15288                }
15289                Some(*lm)
15290            }
15291            None => None,
15292        };
15293        let mut gdn_layers = Vec::new();
15294        let mut attn_layers = Vec::new();
15295        for item in &plan {
15296            match item {
15297                MetalRowsItem::Gdn { run, first } => {
15298                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
15299                        .iter()
15300                        .map(|l| l.linear_state.as_slice())
15301                        .collect();
15302                    if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
15303                        return MetalRowsRun::Declined;
15304                    }
15305                    gdn_layers.extend(*first..*first + run.len());
15306                }
15307                MetalRowsItem::Attn {
15308                    l,
15309                    li,
15310                    q_norm,
15311                    k_norm,
15312                    output_gate,
15313                } => {
15314                    let (p, cpu_stored) = Self::metal_attn_params(
15315                        *li,
15316                        &self.kv_cache.layers[*li],
15317                        *q_norm,
15318                        *k_norm,
15319                        *output_gate,
15320                        &inv_freq,
15321                        self.layer_rope_scale(*li),
15322                        geom,
15323                        pos0,
15324                        kv_id,
15325                        self.attn_scale,
15326                        eps,
15327                        gemma,
15328                        self.qk_norm_after_rope,
15329                    );
15330                    if !graph.encode_attn_b(l, &p) {
15331                        return MetalRowsRun::Declined;
15332                    }
15333                    attn_layers.push((*li, cpu_stored));
15334                }
15335            }
15336        }
15337        if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
15338            if !graph.encode_lm_head_b(final_norm, lm) {
15339                return MetalRowsRun::Declined;
15340            }
15341            // The device argmax is an OPTIMISATION, never a reason to
15342            // decline the round: if it will not encode, drop it and read
15343            // the logits plane back the old way (the head is encoded
15344            // either way, so the rows are there).
15345            if let Some((n, _)) = argmax_out.as_ref() {
15346                if !graph.encode_argmax_b(*n) {
15347                    argmax_out = None;
15348                }
15349            }
15350        }
15351        spec_stamp("v.enc");
15352        if !graph.sync() {
15353            return MetalRowsRun::Failed;
15354        }
15355        spec_stamp("v.gpu");
15356        match (spec, argmax_out) {
15357            (Some(_), Some((_, ids))) => {
15358                ids.resize(b, 0);
15359                if !graph.read_argmax(ids) {
15360                    return MetalRowsRun::Failed;
15361                }
15362                spec_stamp("v.am");
15363            }
15364            (Some((lm, _, logits)), None) => {
15365                logits.resize(b * lm.1, 0.0);
15366                if !graph.read_logits(logits) {
15367                    return MetalRowsRun::Failed;
15368                }
15369                spec_stamp("v.lg");
15370            }
15371            (None, _) => {}
15372        }
15373        if !graph.read_hidden(hiddens) {
15374            return MetalRowsRun::Failed;
15375        }
15376        spec_stamp("v.hid");
15377        MetalRowsRun::Completed(MetalVerifyPending {
15378            graph,
15379            gdn_layers,
15380            attn_layers,
15381        })
15382    }
15383
15384    /// Native-Metal twin of `try_batch_graph_wgpu`: the b rows through the
15385    /// whole model on the `VerifyGraph` (one submit), the head folded in
15386    /// when `spec` asks; `hiddens` come back as the last layer's output
15387    /// rows, `spec.2` as `[b][lm_rows]` logits. The graph is parked in
15388    /// `metal_verify` for `metal_verify_commit`.
15389    #[cfg(target_os = "macos")]
15390    fn try_batch_graph_metal(
15391        &mut self,
15392        hiddens: &mut [f32],
15393        positions: &[usize],
15394        b: usize,
15395        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15396        argmax_out: Option<(usize, &mut Vec<u32>)>,
15397    ) -> crate::gpu::BatchGraphOutcome {
15398        let _t0 = std::time::Instant::now();
15399        if positions.len() != b
15400            || self.mhc.is_some()
15401            || self.prompt_mrope.is_some()
15402            || positions.windows(2).any(|w| w[1] != w[0] + 1)
15403            || hiddens.len() != b * self.hidden_size
15404        {
15405            return crate::gpu::BatchGraphOutcome::Declined;
15406        }
15407        let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
15408            MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
15409            MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
15410            MetalRowsRun::Completed(pending) => pending,
15411        };
15412        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15413            eprintln!(
15414                "metal-verify: {:.1} ms | b={b}",
15415                _t0.elapsed().as_secs_f64() * 1e3
15416            );
15417        }
15418        self.metal_verify = Some(pending);
15419        crate::gpu::BatchGraphOutcome::Completed
15420    }
15421
15422    /// Batched prefill on the Metal rows graph: `ids` (≤ 512) at
15423    /// `start_pos..`, states written in place, K/V rows appended to the
15424    /// CPU caches; optional final norm/head logits are returned in `spec`.
15425    /// Declined means no command buffer was admitted; Failed is terminal.
15426    #[cfg(target_os = "macos")]
15427    fn prefill_rows_metal(
15428        &mut self,
15429        ids: &[u32],
15430        start_pos: usize,
15431        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15432    ) -> MetalPrefillOutcome {
15433        let b = ids.len();
15434        if b == 0 || b > 512 {
15435            return MetalPrefillOutcome::Declined;
15436        }
15437        METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15438        let with_head = spec.is_some();
15439        let hs = self.hidden_size;
15440        let mut hiddens = vec![0f32; b * hs];
15441        for (j, &id) in ids.iter().enumerate() {
15442            let e = self.embed_single(id);
15443            hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
15444        }
15445        let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
15446            MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
15447            MetalRowsRun::Failed => {
15448                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15449                return MetalPrefillOutcome::Failed;
15450            }
15451            MetalRowsRun::Completed(pending) => pending,
15452        };
15453        // states are final: copy them to the owners
15454        let idxs = pending.gdn_layers.clone();
15455        let mut outs: Vec<&mut [f32]> = self
15456            .kv_cache
15457            .layers
15458            .iter_mut()
15459            .enumerate()
15460            .filter(|(i, _)| idxs.binary_search(i).is_ok())
15461            .map(|(_, l)| l.linear_state.as_mut_slice())
15462            .collect();
15463        if !pending.graph.finish_states(&mut outs) {
15464            METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15465            return MetalPrefillOutcome::Failed;
15466        }
15467        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15468        // Read every layer before mutating any CPU cache.  A missing mirror
15469        // row is a terminal graph failure, not a reason to append a partial
15470        // prefix and replay the remainder serially.
15471        let mut rows = Vec::with_capacity(pending.attn_layers.len());
15472        for (li, cpu_stored) in &pending.attn_layers {
15473            let mut kbuf = vec![0f32; b * nkv * hd];
15474            let mut vbuf = vec![0f32; b * nkv * hd];
15475            if !crate::gpu_metal::kv_mirror_read_rows(
15476                self.graph_kv_id,
15477                *li,
15478                nkv,
15479                hd,
15480                *cpu_stored,
15481                b,
15482                &mut kbuf,
15483                &mut vbuf,
15484            ) {
15485                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15486                return MetalPrefillOutcome::Failed;
15487            }
15488            rows.push((*li, *cpu_stored, kbuf, vbuf));
15489        }
15490        for (li, cpu_stored, kbuf, vbuf) in rows {
15491            let cache = &mut self.kv_cache.layers[li];
15492            for r in 0..b {
15493                cache.append(
15494                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15495                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15496                    &[],
15497                );
15498            }
15499            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
15500        }
15501        METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15502        if with_head {
15503            METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15504        }
15505        MetalPrefillOutcome::Completed(hiddens)
15506    }
15507
15508    #[cfg(target_os = "macos")]
15509    fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
15510        self.prefill_rows_metal(ids, start_pos, None)
15511    }
15512
15513    /// Exact teacher-forced NLL through the ordinary Metal rows graph.  This
15514    /// is intentionally separate from the serial TokenGraph scorer: every
15515    /// chunk owns a real b-row graph/head completion and the recurrent/KV
15516    /// handoff is committed before the next chunk begins.
15517    #[cfg(target_os = "macos")]
15518    fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
15519        if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
15520            return MetalBatchNllOutcome::Declined;
15521        }
15522        let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
15523            return MetalBatchNllOutcome::Declined;
15524        };
15525        let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
15526            .ok()
15527            .and_then(|v| v.parse::<usize>().ok())
15528            .filter(|&v| (1..=512).contains(&v))
15529            .unwrap_or(32);
15530        let final_norm = self.weights.final_norm.clone();
15531        let mut nll = 0.0f64;
15532        let mut count = 0usize;
15533        let mut pos = 0usize;
15534        let mut completed = 0usize;
15535        while pos < ids.len() {
15536            let end = (pos + chunk).min(ids.len());
15537            let mut logits = Vec::new();
15538            let outcome =
15539                self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
15540            match outcome {
15541                MetalPrefillOutcome::Declined => {
15542                    return if completed == 0 {
15543                        MetalBatchNllOutcome::Declined
15544                    } else {
15545                        MetalBatchNllOutcome::Failed(format!(
15546                            "ordinary Metal NLL batch declined after {completed} chunks"
15547                        ))
15548                    };
15549                }
15550                MetalPrefillOutcome::Failed => {
15551                    return MetalBatchNllOutcome::Failed(
15552                        "ordinary Metal NLL batch failed after admission".to_string(),
15553                    );
15554                }
15555                MetalPrefillOutcome::Completed(_) => {}
15556            }
15557            completed += 1;
15558            let vocab = self.vocab_size.min(lm.1);
15559            if logits.len() != (end - pos) * lm.1 || vocab == 0 {
15560                return MetalBatchNllOutcome::Failed(
15561                    "ordinary Metal NLL head returned an invalid shape".to_string(),
15562                );
15563            }
15564            for row in 0..(end - pos) {
15565                let absolute = pos + row;
15566                if absolute < start || absolute + 1 >= ids.len() {
15567                    continue;
15568                }
15569                let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
15570                if let Some(mu) = self.logit_multiplier {
15571                    for v in lg.iter_mut() {
15572                        *v *= mu;
15573                    }
15574                }
15575                if let Some(c) = self.final_softcap {
15576                    softcap_in_place(lg, c, self.pool.as_deref());
15577                }
15578                let target = ids[absolute + 1] as usize;
15579                if target >= vocab {
15580                    return MetalBatchNllOutcome::Failed(format!(
15581                        "target token {target} exceeds Metal head rows {vocab}"
15582                    ));
15583                }
15584                let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
15585                let lse: f64 = lg
15586                    .iter()
15587                    .map(|&v| ((v - max) as f64).exp())
15588                    .sum::<f64>()
15589                    .ln()
15590                    + max as f64;
15591                nll += lse - lg[target] as f64;
15592                count += 1;
15593            }
15594            pos = end;
15595        }
15596        MetalBatchNllOutcome::Completed(nll, count)
15597    }
15598
15599    /// Commit a Metal verify round: replay the GDN recurrences over the
15600    /// `a + 1` accepted positions into the CPU states, append the accepted
15601    /// K/V rows from the mirrors to the CPU caches, re-point the mirrors.
15602    #[cfg(target_os = "macos")]
15603    fn metal_verify_commit(&mut self, a: usize) -> bool {
15604        let Some(mut pending) = self.metal_verify.take() else {
15605            return false;
15606        };
15607        let n = a + 1;
15608        // encode order == ascending layer order (the plan walks 0..layers)
15609        let idxs = pending.gdn_layers.clone();
15610        let mut outs: Vec<&mut [f32]> = self
15611            .kv_cache
15612            .layers
15613            .iter_mut()
15614            .enumerate()
15615            .filter(|(i, _)| idxs.binary_search(i).is_ok())
15616            .map(|(_, l)| l.linear_state.as_mut_slice())
15617            .collect();
15618        if !pending.graph.commit(n, &mut outs) {
15619            return false;
15620        }
15621        spec_stamp("c.replay");
15622        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15623        // Read every layer before mutating any CPU cache.  Missing rows are
15624        // terminal after the replay has executed; never append a partial KV
15625        // prefix and continue on a serial path.
15626        let mut rows = Vec::with_capacity(pending.attn_layers.len());
15627        for (li, cpu_stored) in &pending.attn_layers {
15628            let mut kbuf = vec![0f32; n * nkv * hd];
15629            let mut vbuf = vec![0f32; n * nkv * hd];
15630            if !crate::gpu_metal::kv_mirror_read_rows(
15631                self.graph_kv_id,
15632                *li,
15633                nkv,
15634                hd,
15635                *cpu_stored,
15636                n,
15637                &mut kbuf,
15638                &mut vbuf,
15639            ) {
15640                return false;
15641            }
15642            rows.push((*li, *cpu_stored, kbuf, vbuf));
15643        }
15644        for (li, cpu_stored, kbuf, vbuf) in rows {
15645            let cache = &mut self.kv_cache.layers[li];
15646            for r in 0..n {
15647                cache.append(
15648                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15649                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15650                    &[],
15651                );
15652            }
15653            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
15654        }
15655        spec_stamp("c.kv");
15656        true
15657    }
15658
15659    /// The round's warm-ups as ONE b-row graph run over the MTP block on
15660    /// Metal: `pairs` = (trunk hidden, next token) at consecutive positions
15661    /// from `first_pos`; the block's input projection is folded in. This
15662    /// half encodes and SUBMITS (no wait); `mtp_warm_batch_finish` waits
15663    /// and pulls the appended K/V rows into the CPU MTP cache. None = the
15664    /// graph declined (nothing submitted, nothing appended).
15665    #[cfg(target_os = "macos")]
15666    fn mtp_warm_batch_submit(
15667        &mut self,
15668        m: &mut MtpModule,
15669        pairs: &[(&[f32], u32)],
15670        first_pos: usize,
15671    ) -> Option<MetalWarmPending> {
15672        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
15673        let b = pairs.len();
15674        if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
15675            return None;
15676        }
15677        let AttnKind::Full {
15678            wq,
15679            wk,
15680            wv,
15681            wo,
15682            q_norm,
15683            k_norm,
15684            output_gate,
15685            softplus_gate: None,
15686            bias: None,
15687        } = &m.layer.attn
15688        else {
15689            return None;
15690        };
15691        let FfnKind::Dense(d) = &m.layer.ffn else {
15692            return None;
15693        };
15694        if !d.segs.is_empty() {
15695            return None;
15696        }
15697        let (Some(pq), Some(pk), Some(pv), Some(po)) =
15698            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
15699        else {
15700            return None;
15701        };
15702        let (Some(g), Some(u), Some(dn)) = (
15703            d.gate_proj.q1_parts(),
15704            d.up_proj.q1_parts(),
15705            d.down_proj.q1_parts(),
15706        ) else {
15707            return None;
15708        };
15709        let Some(eh) = m.eh_proj.q1_parts() else {
15710            return None;
15711        };
15712        let QTensor::Mapped { model, .. } = wq else {
15713            return None;
15714        };
15715        let model = model.clone();
15716        let hs = self.hidden_size;
15717        // [enorm(embed(tok)); hnorm(hidden)] rows
15718        let mut cat = vec![0f32; b * 2 * hs];
15719        for (j, (h, tok)) in pairs.iter().enumerate() {
15720            let e = self.embed_single(*tok);
15721            let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
15722            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
15723            inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
15724        }
15725        let dims = GraphDims {
15726            hidden: hs,
15727            eps: self.rms_eps as f32,
15728            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15729        };
15730        spec_stamp("w.cat");
15731        let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
15732            return None;
15733        };
15734        spec_stamp("w.new");
15735        let l = AttnGpuLayer {
15736            mhc: None,
15737            attn_norm: &m.layer.input_norm,
15738            post_norm: &m.layer.post_norm,
15739            wq: pq,
15740            wk: pk,
15741            wv: pv,
15742            wo: po,
15743            ffn: MetalFfn::Dense {
15744                gate: g,
15745                up: u,
15746                down: dn,
15747                act: (d.act == Act::Gelu) as u8,
15748            },
15749            attn_out_norm: None,
15750            ffn_out_norm: None,
15751            layer_scale: None,
15752        };
15753        let (nh, nkv, hd, rd) = (
15754            self.num_heads,
15755            self.num_kv_heads,
15756            self.head_dim,
15757            self.rotary_dim,
15758        );
15759        let inv_freq = self.inv_freq.clone();
15760        let cpu_stored;
15761        {
15762            let cache = &m.kv;
15763            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15764            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15765            cpu_stored = cpu_k[0].len() / hd;
15766            // The cache may LAG the position (rows nobody warmed): the
15767            // pairs land at cpu_stored.. with their true RoPE positions
15768            // first_pos.., exactly what the one-by-one warm does. A cache
15769            // AHEAD of the position is a real inconsistency.
15770            if cpu_stored > first_pos {
15771                spec_stamp("w.decl");
15772                return None;
15773            }
15774            let p = AttnDeviceParams {
15775                kv_id: self.mtp_kv_id(),
15776                layer: Self::MTP_LAYER_BASE,
15777                nh,
15778                nkv,
15779                hd,
15780                rd,
15781                position: first_pos,
15782                scale: self.attn_scale,
15783                eps: self.rms_eps as f32,
15784                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15785                late_qk_norm: self.qk_norm_after_rope,
15786                output_gate: *output_gate,
15787                q_norm: q_norm.as_deref(),
15788                k_norm: k_norm.as_deref(),
15789                inv_freq: &inv_freq,
15790                rope_scale: self.rope_scale,
15791                cpu_k,
15792                cpu_v,
15793                cpu_stored,
15794                cpu_gen: cache.generation(),
15795                o1: None,
15796                window: None,
15797                softmax_sink: None,
15798                head_gate: None,
15799                v_norm: false,
15800            };
15801            if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
15802                return None;
15803            }
15804        }
15805        spec_stamp("w.enc");
15806        if !graph.submit() {
15807            return None;
15808        }
15809        spec_stamp("w.sub");
15810        Some(MetalWarmPending {
15811            graph,
15812            cpu_stored,
15813            b,
15814        })
15815    }
15816
15817    /// Submit and finish in one call (the prefill's MTP warm-up, where
15818    /// nothing runs in between).
15819    #[cfg(target_os = "macos")]
15820    fn mtp_warm_batch_metal(
15821        &mut self,
15822        m: &mut MtpModule,
15823        pairs: &[(&[f32], u32)],
15824        first_pos: usize,
15825    ) -> bool {
15826        match self.mtp_warm_batch_submit(m, pairs, first_pos) {
15827            Some(p) => self.mtp_warm_batch_finish(m, p),
15828            None => false,
15829        }
15830    }
15831
15832    /// Second half of the batched warm-up: wait for the submitted graph,
15833    /// pull its b appended K/V rows into the CPU MTP cache, re-point the
15834    /// mirror. False = the command buffer failed or the rows are missing
15835    /// (nothing appended; the caller falls back to the one-by-one warm).
15836    #[cfg(target_os = "macos")]
15837    fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
15838        let MetalWarmPending {
15839            mut graph,
15840            cpu_stored,
15841            b,
15842        } = pending;
15843        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15844        if !graph.sync() {
15845            return false;
15846        }
15847        spec_stamp("w.gpu");
15848        let mut kbuf = vec![0f32; b * nkv * hd];
15849        let mut vbuf = vec![0f32; b * nkv * hd];
15850        if !crate::gpu_metal::kv_mirror_read_rows(
15851            self.mtp_kv_id(),
15852            Self::MTP_LAYER_BASE,
15853            nkv,
15854            hd,
15855            cpu_stored,
15856            b,
15857            &mut kbuf,
15858            &mut vbuf,
15859        ) {
15860            return false;
15861        }
15862        for r in 0..b {
15863            m.kv.append(
15864                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15865                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15866                &[],
15867            );
15868        }
15869        crate::gpu_metal::kv_mirror_set_stored(
15870            self.mtp_kv_id(),
15871            Self::MTP_LAYER_BASE,
15872            cpu_stored + b,
15873        );
15874        spec_stamp("w.kv");
15875        true
15876    }
15877
15878    /// A committed token id from the high table (Cyrillic, CJK and the
15879    /// like sit above 131072 in Qwen's vocabulary; Latin subwords past
15880    /// the 65536 cut are rare enough to lose as rejected drafts) switches
15881    /// the draft to the full head for the next 16 tokens; other ids count
15882    /// down. On an M4 the full 660 MB head costs 5.5 ms a draft step
15883    /// against 1.4 for the shortlist, so the streak is kept short.
15884    pub(crate) fn note_draft_id(&mut self, id: u32) {
15885        let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
15886        if (id as usize) >= cut {
15887            self.draft_full_streak = 16;
15888        } else {
15889            self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
15890        }
15891    }
15892
15893    /// The draft head's rows for the next step: the shortlist, or the full
15894    /// head while `draft_full_streak` runs.
15895    fn draft_head_rows(&self, head_rows: usize) -> usize {
15896        if self.draft_full_streak > 0 {
15897            head_rows
15898        } else {
15899            Self::draft_vocab_rows(head_rows)
15900        }
15901    }
15902
15903    /// Draft-head shortlist size: `CMF_DRAFT_VOCAB` rows (default 65536,
15904    /// capped at the head; 0 = full head).
15905    fn draft_vocab_rows(head_rows: usize) -> usize {
15906        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
15907        let n = *N.get_or_init(|| {
15908            std::env::var("CMF_DRAFT_VOCAB")
15909                .ok()
15910                .and_then(|v| v.parse().ok())
15911                .unwrap_or(65536)
15912        });
15913        if n == 0 { head_rows } else { n.min(head_rows) }
15914    }
15915
15916    /// One MTP block step on the native Metal token graph: block input on
15917    /// the host, the attention layer + FFN device-resident over the MTP
15918    /// mirror, the head folded in when `want_logits`. The appended K/V row
15919    /// is pulled into the CPU MTP cache (owner of record) after the sync.
15920    #[cfg(target_os = "macos")]
15921    fn mtp_step_metal(
15922        &mut self,
15923        m: &mut MtpModule,
15924        hidden: &[f32],
15925        next_token: u32,
15926        position: usize,
15927        want_logits: bool,
15928    ) -> Option<(Vec<f32>, Vec<f32>)> {
15929        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
15930        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
15931            || !crate::gpu::q1_force()
15932            || !crate::gpu::enabled_here()
15933            || self.attn_softcap > 0.0
15934            || self.attention_heads_per_layer.is_some()
15935            || m.kv.mode != crate::kv_cache::KvMode::F32
15936            || m.kv.o1.is_some()
15937        {
15938            return None;
15939        }
15940        let AttnKind::Full {
15941            wq,
15942            wk,
15943            wv,
15944            wo,
15945            q_norm,
15946            k_norm,
15947            output_gate,
15948            softplus_gate: None,
15949            bias: None,
15950        } = &m.layer.attn
15951        else {
15952            return None;
15953        };
15954        let FfnKind::Dense(d) = &m.layer.ffn else {
15955            return None;
15956        };
15957        if d.act != Act::Silu || !d.segs.is_empty() {
15958            return None;
15959        }
15960        let (pq, pk, pv, po) = (
15961            wq.q1_parts()?,
15962            wk.q1_parts()?,
15963            wv.q1_parts()?,
15964            wo.q1_parts()?,
15965        );
15966        let (g, u, dn) = (
15967            d.gate_proj.q1_parts()?,
15968            d.up_proj.q1_parts()?,
15969            d.down_proj.q1_parts()?,
15970        );
15971        let QTensor::Mapped { model, .. } = wq else {
15972            return None;
15973        };
15974        let model = model.clone();
15975        let lm = if want_logits {
15976            Some(self.weights.lm_head.q1_parts()?)
15977        } else {
15978            None
15979        };
15980        let dims = GraphDims {
15981            hidden: self.hidden_size,
15982            eps: self.rms_eps as f32,
15983            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15984        };
15985        // The block input `eh_proj · [enorm(e); hnorm(h)]` rides in the
15986        // graph (one submit a step); the host per-op matvec if it cannot.
15987        let hs = self.hidden_size;
15988        let mut x = vec![0f32; hs];
15989        // Let an eligible q4tp MTP draft use the measured masked-nibble
15990        // matvec.  Unlike fused epilogues, this reduces the draft's
15991        // projection work without changing its residual-update schedule;
15992        // the target verifier remains the authority over emitted tokens.
15993        // The guard keeps `CMF_METAL_MVFAST=0` as an immediate rollback
15994        // switch and restores the caller's setting on every early return.
15995        let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
15996            crate::gpu_metal::DENSE_MV,
15997        );
15998        let mut graph = TokenGraph::new(&model, dims, &x)?;
15999        let mut folded = false;
16000        if let Some(eh) = m.eh_proj.q1_parts() {
16001            let e = self.embed_single(next_token);
16002            let mut cat = vec![0.0f32; 2 * hs];
16003            let (cat_e, cat_h) = cat.split_at_mut(hs);
16004            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
16005            inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
16006            folded = graph.encode_input_proj(eh, &cat);
16007        }
16008        if !folded {
16009            x = self.mtp_block_input(m, hidden, next_token);
16010            graph = TokenGraph::new(&model, dims, &x)?;
16011        }
16012        spec_stamp("d.in");
16013        let l = AttnGpuLayer {
16014            mhc: None,
16015            attn_norm: &m.layer.input_norm,
16016            post_norm: &m.layer.post_norm,
16017            wq: pq,
16018            wk: pk,
16019            wv: pv,
16020            wo: po,
16021            ffn: MetalFfn::Dense {
16022                gate: g,
16023                up: u,
16024                down: dn,
16025                act: (d.act == Act::Gelu) as u8,
16026            },
16027            attn_out_norm: None,
16028            ffn_out_norm: None,
16029            layer_scale: None,
16030        };
16031        let (nh, nkv, hd, rd) = (
16032            self.num_heads,
16033            self.num_kv_heads,
16034            self.head_dim,
16035            self.rotary_dim,
16036        );
16037        let inv_freq = self.inv_freq.clone();
16038        {
16039            let cache = &m.kv;
16040            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
16041            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
16042            let cpu_stored = cpu_k[0].len() / hd;
16043            let p = AttnDeviceParams {
16044                kv_id: self.mtp_kv_id(),
16045                layer: Self::MTP_LAYER_BASE,
16046                nh,
16047                nkv,
16048                hd,
16049                rd,
16050                position,
16051                scale: self.attn_scale,
16052                eps: self.rms_eps as f32,
16053                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16054                late_qk_norm: self.qk_norm_after_rope,
16055                output_gate: *output_gate,
16056                q_norm: q_norm.as_deref(),
16057                k_norm: k_norm.as_deref(),
16058                inv_freq: &inv_freq,
16059                rope_scale: self.rope_scale,
16060                cpu_k,
16061                cpu_v,
16062                cpu_stored,
16063                cpu_gen: cache.generation(),
16064                o1: None,
16065                window: None,
16066                softmax_sink: None,
16067                head_gate: None,
16068                v_norm: false,
16069            };
16070            if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
16071                return None;
16072            }
16073        }
16074        // The draft's head over a vocabulary SHORTLIST (the first
16075        // CMF_DRAFT_VOCAB rows — BPE ids run roughly by merge rank, so the
16076        // low ids carry the mass): the verify keeps the full head, so a true
16077        // token past the cut is only a rejected draft, never a wrong token.
16078        // 662 MB a step on Qwen3.8 becomes 170 MB at 65536.
16079        let draft_rows = if let Some(lm) = lm {
16080            self.draft_head_rows(lm.1)
16081        } else {
16082            0
16083        };
16084        if let Some(lm) = lm {
16085            if !graph.lm_head_ok(lm) {
16086                return None;
16087            }
16088            if draft_rows < lm.1 {
16089                if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
16090                    return None;
16091                }
16092            } else {
16093                graph.encode_lm_head(&m.final_norm, lm);
16094            }
16095        }
16096        spec_stamp("d.enc");
16097        if graph.sync_checked().is_err() {
16098            return None;
16099        }
16100        spec_stamp("d.gpu");
16101        let mut logits = Vec::new();
16102        if let Some(lm) = lm {
16103            let n_read = draft_rows.min(lm.1).min(self.vocab_size);
16104            logits = attention::take_buf(n_read);
16105            graph.read_logits(&mut logits);
16106            // ids past the shortlist: never drafted (−∞ in every chain)
16107            pad_mtp_draft_logits(&mut logits, self.vocab_size);
16108        }
16109        graph.finish(&mut x);
16110        let mut krow = attention::take_buf(nkv * hd);
16111        let mut vrow = attention::take_buf(nkv * hd);
16112        if crate::gpu_metal::kv_mirror_read_last(
16113            self.mtp_kv_id(),
16114            Self::MTP_LAYER_BASE,
16115            nkv,
16116            hd,
16117            &mut krow,
16118            &mut vrow,
16119        ) {
16120            m.kv.append(&krow, &vrow, &[]);
16121        }
16122        attention::recycle_buf(&mut krow);
16123        attention::recycle_buf(&mut vrow);
16124        spec_stamp("d.rd");
16125        Some((logits, x))
16126    }
16127
16128    /// `CMF_MTP_CHAIN=0` keeps the per-step draft (one submit and one
16129    /// host round trip per MTP step); the default drafts the whole chain
16130    /// in one command buffer when the round is plain greedy.
16131    ///
16132    /// Measured on an M4 (24 GB), Qwen3.8-27B q4tp, P3 at 160 tokens,
16133    /// k=7, six runs per arm alternating inside one lock window — the
16134    /// round's draft phase (median over the 34 rounds of a run) is
16135    /// 34.5 ms per round old against 30.1 new, i.e. 4.93 → 4.31 ms per
16136    /// draft step. That is the whole prize: the 7 submits cost ~0.6 ms
16137    /// each in host and submit latency and nothing else changes —
16138    /// acceptance (3.41 of 7) and tokens per round (4.41) are identical,
16139    /// and the round is 289 → 285 ms, decode 13.8 → 14.0 tok/s.
16140    fn mtp_chain_on() -> bool {
16141        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16142        *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
16143    }
16144
16145    /// The round's k greedy drafts as ONE command buffer on Metal: the MTP
16146    /// block k times back to back, each step's token embedding gathered
16147    /// on the device from the argmax the step before it wrote, the head
16148    /// over the round's shortlist (or the full head during a full-head
16149    /// streak — decided once, before the chain, exactly as the per-step
16150    /// path decides it per step, since `draft_full_streak` only moves on
16151    /// a commit). One wait, then the k ids and the k appended K/V rows
16152    /// come back; the CPU MTP cache ends where k `mtp_step_metal` calls
16153    /// would have left it. `Err(false)` = declined before anything was
16154    /// committed (the per-step path takes the round); `Err(true)` = the
16155    /// command buffer failed after commit.
16156    #[cfg(target_os = "macos")]
16157    fn mtp_draft_chain_metal(
16158        &mut self,
16159        m: &mut MtpModule,
16160        hidden: &[f32],
16161        t_next: u32,
16162        position: usize,
16163        k: usize,
16164    ) -> Result<Vec<u32>, bool> {
16165        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
16166        if k == 0
16167            || k > 64
16168            || !Self::mtp_chain_on()
16169            || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
16170            || !crate::gpu::q1_force()
16171            || !crate::gpu::enabled_here()
16172            || self.attn_softcap > 0.0
16173            || self.attention_heads_per_layer.is_some()
16174            || m.kv.mode != crate::kv_cache::KvMode::F32
16175            || m.kv.o1.is_some()
16176            // the chain gathers embeddings itself: only the plain table
16177            || self.dsv4.is_some()
16178            || self.dsv41.is_some()
16179            || self.qwen4_exp.is_some()
16180            || self.per_token_stack()
16181        {
16182            return Err(false);
16183        }
16184        let AttnKind::Full {
16185            wq,
16186            wk,
16187            wv,
16188            wo,
16189            q_norm,
16190            k_norm,
16191            output_gate,
16192            softplus_gate: None,
16193            bias: None,
16194        } = &m.layer.attn
16195        else {
16196            return Err(false);
16197        };
16198        let FfnKind::Dense(d) = &m.layer.ffn else {
16199            return Err(false);
16200        };
16201        if d.act != Act::Silu || !d.segs.is_empty() {
16202            return Err(false);
16203        }
16204        let (Some(pq), Some(pk), Some(pv), Some(po)) =
16205            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
16206        else {
16207            return Err(false);
16208        };
16209        let (Some(g), Some(u), Some(dn)) = (
16210            d.gate_proj.q1_parts(),
16211            d.up_proj.q1_parts(),
16212            d.down_proj.q1_parts(),
16213        ) else {
16214            return Err(false);
16215        };
16216        let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
16217            return Err(false);
16218        };
16219        let QTensor::Mapped { model, .. } = wq else {
16220            return Err(false);
16221        };
16222        let model = model.clone();
16223        // the embedding table: a q4tp tensor of the SAME blob, no Prism
16224        // inverse-embedding post-pass
16225        let QTensor::Mapped {
16226            model: em,
16227            idx: eidx,
16228            dtype: cortiq_core::TensorDtype::Q4TiledP,
16229            ..
16230        } = &self.weights.embed_tokens
16231        else {
16232            return Err(false);
16233        };
16234        if !std::sync::Arc::ptr_eq(em, &model)
16235            || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
16236        {
16237            return Err(false);
16238        }
16239        let embed = (
16240            *eidx,
16241            self.weights.embed_tokens.rows(),
16242            self.weights.embed_tokens.cols(),
16243        );
16244        if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
16245            return Err(false);
16246        }
16247        let dims = GraphDims {
16248            hidden: self.hidden_size,
16249            eps: self.rms_eps as f32,
16250            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16251        };
16252        // Keep the chained MTP draft on the masked-nibble Metal matvec
16253        // path.  This scope covers every encode in the chain; the guard
16254        // restores its predecessor when this round returns or declines.
16255        let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
16256            crate::gpu_metal::DENSE_MV,
16257        );
16258        let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
16259            return Err(false);
16260        };
16261        if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
16262            return Err(false);
16263        }
16264        let l = AttnGpuLayer {
16265            mhc: None,
16266            attn_norm: &m.layer.input_norm,
16267            post_norm: &m.layer.post_norm,
16268            wq: pq,
16269            wk: pk,
16270            wv: pv,
16271            wo: po,
16272            ffn: MetalFfn::Dense {
16273                gate: g,
16274                up: u,
16275                down: dn,
16276                act: (d.act == Act::Gelu) as u8,
16277            },
16278            attn_out_norm: None,
16279            ffn_out_norm: None,
16280            layer_scale: None,
16281        };
16282        let (nh, nkv, hd, rd) = (
16283            self.num_heads,
16284            self.num_kv_heads,
16285            self.head_dim,
16286            self.rotary_dim,
16287        );
16288        let inv_freq = self.inv_freq.clone();
16289        let draft_rows = self.draft_head_rows(lm.1);
16290        let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
16291        if n_arg == 0 {
16292            return Err(false);
16293        }
16294        // `CMF_MTP_CHAIN_SPLIT=1` commits each step as it is encoded, so
16295        // the GPU starts on step 0 while the host is still encoding step
16296        // 1 — a probe for whether the host encode is on the critical
16297        // path. It is not: three runs each, draft 30.0 ms per round split
16298        // against 30.1 whole, and the whole chain's host encode measures
16299        // 0.3 ms against a 29.7 ms wait. Kept as a probe, off by default.
16300        let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
16301        let t_chain = std::time::Instant::now();
16302        graph.chain_ids_init(t_next, k);
16303        let cpu_stored;
16304        {
16305            let cache = &m.kv;
16306            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
16307            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
16308            cpu_stored = cpu_k[0].len() / hd;
16309            for j in 0..k {
16310                if !graph.encode_chain_input(
16311                    embed,
16312                    j as u32,
16313                    &m.enorm,
16314                    &m.hnorm,
16315                    self.embed_multiplier,
16316                    eh,
16317                ) {
16318                    return Err(false);
16319                }
16320                // step j's mirror row: the mirror is re-pointed at the CPU
16321                // rows before step 0 and advances by one per step; its
16322                // resync (never taken past step 0) reads the CPU rows
16323                let p = AttnDeviceParams {
16324                    kv_id: self.mtp_kv_id(),
16325                    layer: Self::MTP_LAYER_BASE,
16326                    nh,
16327                    nkv,
16328                    hd,
16329                    rd,
16330                    position: position + j,
16331                    scale: self.attn_scale,
16332                    eps: self.rms_eps as f32,
16333                    gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16334                    late_qk_norm: self.qk_norm_after_rope,
16335                    output_gate: *output_gate,
16336                    q_norm: q_norm.as_deref(),
16337                    k_norm: k_norm.as_deref(),
16338                    inv_freq: &inv_freq,
16339                    rope_scale: self.rope_scale,
16340                    cpu_k: cpu_k.clone(),
16341                    cpu_v: cpu_v.clone(),
16342                    cpu_stored: cpu_stored + j,
16343                    cpu_gen: cache.generation(),
16344                    o1: None,
16345                    window: None,
16346                    softmax_sink: None,
16347                    head_gate: None,
16348                    v_norm: false,
16349                };
16350                if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
16351                    return Err(false);
16352                }
16353                if draft_rows < lm.1 {
16354                    if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
16355                        return Err(false);
16356                    }
16357                } else {
16358                    graph.encode_lm_head(&m.final_norm, lm);
16359                }
16360                if !graph.encode_argmax(n_arg, j as u32 + 1) {
16361                    return Err(false);
16362                }
16363                if split {
16364                    // CMF_MTP_CHAIN_SPLIT=1: commit every step so the GPU
16365                    // starts on step 0 while the host encodes the rest
16366                    graph.commit();
16367                }
16368            }
16369        }
16370        let t_enc = t_chain.elapsed();
16371        if graph.sync_checked().is_err() {
16372            return Err(true);
16373        }
16374        if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
16375            eprintln!(
16376                "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
16377                t_enc.as_secs_f64() * 1e3,
16378                (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
16379                if split { ", split" } else { "" }
16380            );
16381        }
16382        let mut ids = vec![0u32; k];
16383        if !graph.chain_ids_read(&mut ids) {
16384            return Err(true);
16385        }
16386        let mut kbuf = vec![0f32; k * nkv * hd];
16387        let mut vbuf = vec![0f32; k * nkv * hd];
16388        if !crate::gpu_metal::kv_mirror_read_rows(
16389            self.mtp_kv_id(),
16390            Self::MTP_LAYER_BASE,
16391            nkv,
16392            hd,
16393            cpu_stored,
16394            k,
16395            &mut kbuf,
16396            &mut vbuf,
16397        ) {
16398            return Err(true);
16399        }
16400        for r in 0..k {
16401            m.kv.append(
16402                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
16403                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
16404                &[],
16405            );
16406        }
16407        Ok(ids)
16408    }
16409
16410    fn try_batch_graph_wgpu(
16411        &self,
16412        hiddens: &mut [f32],
16413        positions: &[usize],
16414        k: usize,
16415        spec: Option<crate::gpu::SpecTail<'_>>,
16416    ) -> crate::gpu::BatchGraphOutcome {
16417        self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
16418    }
16419
16420    /// `try_batch_graph_wgpu` with the device-prefix mode: `layers_run`
16421    /// Some lets a stack that does not fit run its leading layers (the
16422    /// token graph's prefix rule) and reports how many; `hiddens` then
16423    /// holds the boundary rows and the caller runs the rest on the host.
16424    fn try_batch_graph_wgpu_prefix(
16425        &self,
16426        hiddens: &mut [f32],
16427        positions: &[usize],
16428        k: usize,
16429        spec: Option<crate::gpu::SpecTail<'_>>,
16430        layers_run: Option<&mut usize>,
16431    ) -> crate::gpu::BatchGraphOutcome {
16432        // SenseNova-Loop: the mHC kernels take two streams (the
16433        // CMF_MHC_GRAPH=0 knob keeps the host); MRoPE prompt rows ride the
16434        // per-row attention loop with their own angle tables.
16435        if self.mhc.as_ref().is_some_and(|m| {
16436                m.s.streams != 2 || m.s.fixed_identity || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
16437            })
16438        {
16439            return crate::gpu::BatchGraphOutcome::Declined;
16440        }
16441        let graph_end = match self.mimo_moe.graph_prefix_end() {
16442            Some(end) if end < self.num_layers => {
16443                if layers_run.is_none() || spec.is_some() || end == 0 {
16444                    return crate::gpu::BatchGraphOutcome::Declined;
16445                }
16446                end
16447            }
16448            _ => self.num_layers,
16449        };
16450        let _tb = std::time::Instant::now();
16451        let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
16452        if self.attn_softcap > 0.0 {
16453            return crate::gpu::BatchGraphOutcome::Declined; // capped scores: no graph kernel — CPU path
16454        }
16455        // Same attention contract as the token graph: per-layer geometry
16456        // rides `geom`, anything it cannot express declines by name.
16457        if let Some(reason) = self
16458            .wgpu_graph_attn_decline()
16459            .or_else(|| self.batch_graph_gemma_decline())
16460        {
16461            self.note_graph_decline("wgpu batch graph", reason);
16462            return crate::gpu::BatchGraphOutcome::Declined;
16463        }
16464        let nh = self.num_heads;
16465        let (nkv, hd, rd) = self.layer_geom(0);
16466        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16467        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
16468            if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
16469            {
16470                let name = &m.tensors[i].name;
16471                let prism = if crate::prism::is_inverse_embedding(m, name) {
16472                    crate::gpu::GraphPrismOp::InverseEmbedding
16473                } else if crate::prism::is_forward_weight(m, name) {
16474                    crate::gpu::GraphPrismOp::Forward
16475                } else {
16476                    crate::gpu::GraphPrismOp::None
16477                };
16478                return Some(crate::gpu::GraphW {
16479                    idx: i,
16480                    kind,
16481                    row_scale: rs,
16482                    data: &[],
16483                    prism,
16484                    affine: crate::prism::is_affine_target(m, name),
16485                });
16486            }
16487            if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
16488                eprintln!(
16489                    "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
16490                    t.rows(),
16491                    t.cols()
16492                );
16493            }
16494            t.as_f32().map(|d| crate::gpu::GraphW {
16495                idx: 0,
16496                kind: 4,
16497                row_scale: &[],
16498                data: d,
16499                prism: crate::gpu::GraphPrismOp::None,
16500                affine: false,
16501            })
16502        }
16503        let built: Option<(
16504            Vec<crate::gpu::GraphLayer<'_>>,
16505            std::sync::Arc<cortiq_core::CmfModel>,
16506        )> = (|| {
16507            let mut layers = Vec::with_capacity(graph_end);
16508            let mut model = None;
16509            for li in 0..graph_end {
16510                let lw = &self.weights.layers[self.phys_layer(li)];
16511                // MoE routes per token, so its experts are encoded token by
16512                // token inside the batched submit while attention and the
16513                // projections stay GEMMs. Refusing MoE here is what left
16514                // prefill running one position at a time: 33 tok/s against
16515                // 54 on decode, i.e. reading the prompt was slower than
16516                // writing the answer.
16517                let gffn = match &lw.ffn {
16518                    FfnKind::Dense(d) if !d.segs.is_empty() => {
16519                        if batch_debug {
16520                            eprintln!("batch graph: dense segmented FFN at layer {li}");
16521                        }
16522                        return None;
16523                    }
16524                    FfnKind::Dense(d) => {
16525                        let Some(act) = d.act.wgpu_graph_act() else {
16526                            if batch_debug {
16527                                eprintln!(
16528                                    "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
16529                                    d.act
16530                                );
16531                            }
16532                            return None;
16533                        };
16534                        crate::gpu::GraphFfn::Dense {
16535                            gate: gw(&d.gate_proj)?,
16536                            up: gw(&d.up_proj)?,
16537                            down: gw(&d.down_proj)?,
16538                            act,
16539                        }
16540                    }
16541                    FfnKind::Moe(m) => {
16542                        // Adaptive τ and expert masks stay on the CPU path.
16543                        // Sigmoid scores, the selection bias, a routed scale
16544                        // ≠ 1 and an ungated shared expert (hy_v3) ride the
16545                        // same flags word as the token graph — before, this
16546                        // refusal sent every Hy-MT2-30B prompt to the chunked
16547                        // fallback (8 tok/s of ingest against 53 of decode).
16548                        if m.route_tau.is_some() || m.mask.is_some() {
16549                            return None;
16550                        }
16551                        // A shared expert rides as slot top_k (gated or
16552                        // not is a flag on the select kernel); without one
16553                        // (MiMo-V2, LFM2-MoE) the kernels run top_k slots.
16554                        let shared = m.shared.as_ref();
16555                        let has_shared = shared.is_some();
16556                        let shared_gated = matches!(shared, Some((_, Some(_))));
16557                        let sgate = match shared {
16558                            Some((_, Some(sg))) => gw(sg)?,
16559                            // Ungated or absent: the router plane stands in
16560                            // so the plumbing stays total; the kernel pins
16561                            // weight 1 or never reads it.
16562                            _ => gw(&m.router)?,
16563                        };
16564                        let router = gw(&m.router)?;
16565                        // The batch MoE kernels still consume raw per-token
16566                        // rows and do not carry the descriptor-aware Prism
16567                        // transform/affine bit for router or shared-gate
16568                        // planes.  Refuse rather than route an untransformed
16569                        // source activation.
16570                        if router.prism != crate::gpu::GraphPrismOp::None
16571                            || router.affine
16572                            || sgate.prism != crate::gpu::GraphPrismOp::None
16573                            || sgate.affine
16574                        {
16575                            return None;
16576                        }
16577                        let inter = m.experts.first()?.gate_proj.rows();
16578                        let mut experts = Vec::with_capacity(m.experts.len() + 1);
16579                        let mut q4tp: Option<bool> = None;
16580                        let mut gu_q2: Option<bool> = None;
16581                        let mut q82: Option<bool> = None;
16582                        // The packed MoE graph has one gate/up program per
16583                        // layer, so accept only one exact activation contract
16584                        // across routed and in-bank shared experts.
16585                        let mut moe_act = None;
16586                        for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
16587                            let Some(e_act) = e.act.wgpu_graph_act() else {
16588                                return None;
16589                            };
16590                            match moe_act {
16591                                Some(existing) if existing != e_act => return None,
16592                                None => moe_act = Some(e_act),
16593                                _ => {}
16594                            }
16595                            if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
16596                                return None;
16597                            }
16598                            // Same ladder as the token graph: q4t → q2tp
16599                            // (mixed profile: 2-bit gate/up over a q4tp
16600                            // down) → q4tp → q8_2f. Uniform across the layer.
16601                            let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
16602                            {
16603                                Some((mm, gi)) => (
16604                                    mm,
16605                                    gi,
16606                                    e.up_proj.mapped_q4t()?.1,
16607                                    e.down_proj.mapped_q4t()?.1,
16608                                    false,
16609                                    false,
16610                                    false,
16611                                ),
16612                                None => match e.gate_proj.mapped_q2tp() {
16613                                    Some((mm, gi)) => (
16614                                        mm,
16615                                        gi,
16616                                        e.up_proj.mapped_q2tp()?.1,
16617                                        e.down_proj.mapped_q4tp()?.1,
16618                                        true,
16619                                        true,
16620                                        false,
16621                                    ),
16622                                    None => match e.gate_proj.mapped_q4tp() {
16623                                        Some((mm, gi)) => (
16624                                            mm,
16625                                            gi,
16626                                            e.up_proj.mapped_q4tp()?.1,
16627                                            e.down_proj.mapped_q4tp()?.1,
16628                                            true,
16629                                            false,
16630                                            false,
16631                                        ),
16632                                        None => {
16633                                            if !crate::gpu::moe_q82_graph_on() {
16634                                                return None;
16635                                            }
16636                                            let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
16637                                            (
16638                                                mm,
16639                                                gi,
16640                                                e.up_proj.mapped_q8_2f()?.1,
16641                                                e.down_proj.mapped_q8_2f()?.1,
16642                                                false,
16643                                                false,
16644                                                true,
16645                                            )
16646                                        }
16647                                    },
16648                                },
16649                            };
16650                            if *q4tp.get_or_insert(is_p) != is_p
16651                                || *gu_q2.get_or_insert(is_q2) != is_q2
16652                                || *q82.get_or_insert(is_8) != is_8
16653                            {
16654                                return None;
16655                            }
16656                            if [gi, ui, di].into_iter().any(|idx| {
16657                                mm.tensors.get(idx).is_some_and(|t| {
16658                                    crate::prism::is_forward_weight(mm, &t.name)
16659                                        || crate::prism::is_affine_target(mm, &t.name)
16660                                })
16661                            }) {
16662                                return None;
16663                            }
16664                            model.get_or_insert_with(|| mm.clone());
16665                            experts.push((gi, ui, di));
16666                        }
16667                        crate::gpu::GraphFfn::Moe {
16668                            router,
16669                            shared_gate: sgate,
16670                            experts,
16671                            n_exp: m.experts.len(),
16672                            top_k: m.top_k,
16673                            inter,
16674                            act: moe_act?,
16675                            norm_topk: m.norm_topk_prob,
16676                            q4tp: q4tp?,
16677                            gu_q2: gu_q2.unwrap_or(false),
16678                            q82: q82.unwrap_or(false),
16679                            sigmoid: m.router_sigmoid,
16680                            sigmoid_denom_eps: m.sigmoid_denom_eps,
16681                            bias: m.expert_bias.as_deref(),
16682                            has_shared,
16683                            detached_shared: None,
16684                            shared_gated,
16685                            route_scale: m.routed_scaling,
16686                        }
16687                    }
16688                    _ => return None,
16689                };
16690                let attn = match &lw.attn {
16691                    AttnKind::Full {
16692                        wq,
16693                        wk,
16694                        wv,
16695                        wo,
16696                        q_norm,
16697                        k_norm,
16698                        output_gate,
16699                        softplus_gate,
16700                        bias,
16701                    } => {
16702                        if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
16703                            if batch_debug {
16704                                eprintln!(
16705                                    "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
16706                                    softplus_gate.is_some(),
16707                                    self.attention_heads_per_layer.is_some()
16708                                );
16709                            }
16710                            return None;
16711                        }
16712                        let (m, _, _, _) =
16713                            wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
16714                        model = Some(m.clone());
16715                        crate::gpu::GraphAttn::Full {
16716                            wq: gw(wq)?,
16717                            wk: gw(wk)?,
16718                            wv: gw(wv)?,
16719                            wo: gw(wo)?,
16720                            q_norm: q_norm.as_deref(),
16721                            k_norm: k_norm.as_deref(),
16722                            late_qk_norm: self.qk_norm_after_rope,
16723                            bias: bias
16724                                .as_ref()
16725                                .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16726                            output_gate: *output_gate,
16727                            cpu_k: self.kv_cache.layers[li].k_heads(),
16728                            cpu_v: self.kv_cache.layers[li].v_heads(),
16729                            cpu_base: self.kv_cache.layers[li].base(),
16730                            geom: self.graph_attn_geom(li),
16731                            // The batched graph has no head-gate arm; a
16732                            // gated layer is refused above.
16733                            head_gate: None,
16734                        }
16735                    }
16736                    AttnKind::LinearGdn(w) => {
16737                        let Some(cfg) = self.gdn_cfg else {
16738                            if batch_debug {
16739                                eprintln!("batch graph: no GDN config at layer {li}");
16740                            }
16741                            return None;
16742                        };
16743                        let (m, _, _, _) = w
16744                            .in_proj_qkv
16745                            .graph_weight()
16746                            .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
16747                        model = Some(m.clone());
16748                        crate::gpu::GraphAttn::Gdn {
16749                            qkv: gw(&w.in_proj_qkv)?,
16750                            z: gw(&w.in_proj_z)?,
16751                            a: gw(&w.in_proj_a)?,
16752                            b: gw(&w.in_proj_b)?,
16753                            out: gw(&w.out_proj)?,
16754                            conv1d: &w.conv1d,
16755                            a_log: &w.a_log,
16756                            dt_bias: &w.dt_bias,
16757                            norm: &w.norm,
16758                            nv: cfg.num_v_heads,
16759                            nk: cfg.num_k_heads,
16760                            dk: cfg.key_head_dim,
16761                            dv: cfg.value_head_dim,
16762                            kk: cfg.conv_kernel,
16763                            cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
16764                            preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
16765                        }
16766                    }
16767                    _ => return None,
16768                };
16769                // Gemma-4's sandwich norms and layer scalar (`gemma_batch`);
16770                // every other model hands the graph none, as before.
16771                let g4 = self.global_attn.is_some() || self.attn_v_norm;
16772                layers.push(crate::gpu::GraphLayer {
16773                    mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
16774                    input_norm: &lw.input_norm,
16775                    attn,
16776                    post_norm: &lw.post_norm,
16777                    attn_out_norm: if g4 {
16778                        lw.attn_out_norm.as_deref()
16779                    } else {
16780                        None
16781                    },
16782                    ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
16783                    layer_scale: if g4 { lw.layer_scale } else { None },
16784                    ffn: gffn,
16785                });
16786            }
16787            Some((layers, model?))
16788        })();
16789        let Some((layers, model)) = built else {
16790            {
16791                use std::sync::atomic::{AtomicBool, Ordering};
16792                static SAID: AtomicBool = AtomicBool::new(false);
16793                if !SAID.swap(true, Ordering::Relaxed) {
16794                    tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
16795                }
16796            }
16797            return crate::gpu::BatchGraphOutcome::Declined;
16798        };
16799        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
16800            eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
16801        }
16802        // SenseNova-Loop and Mage-VL: their q8_2f prompt chunks take the
16803        // batched int8 GEMM.
16804        #[cfg(feature = "gpu")]
16805        crate::gpu_wgpu::set_batch_q82_mm(self.mhc.is_some() || self.onevision2_prefill());
16806        // SenseNova-Loop image prompt: each row's absolute MRoPE angles.
16807        #[cfg(feature = "gpu")]
16808        let _mrope = self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| {
16809            let half = rd / 2;
16810            let mut tab = vec![0f32; k * 64];
16811            for (i, &p) in positions.iter().enumerate() {
16812                for j in 0..half.min(64) {
16813                    let a = pos.get(p).map_or(p as f32, |q| q[crate::sensenova::mrope_axis(j, sections)] as f32);
16814                    tab[i * 64 + j] = a * self.inv_freq[j];
16815                }
16816            }
16817            crate::gpu_wgpu::set_batch_mrope(Some(tab));
16818            struct Clear;
16819            impl Drop for Clear {
16820                fn drop(&mut self) {
16821                    crate::gpu_wgpu::set_batch_mrope(None);
16822                }
16823            }
16824            Clear
16825        });
16826        crate::gpu::forward_batch_graph(
16827            &model,
16828            self.graph_kv_id,
16829            &layers,
16830            &self.inv_freq,
16831            hiddens,
16832            nh,
16833            nkv,
16834            hd,
16835            rd,
16836            self.hidden_size,
16837            self.intermediate_size,
16838            positions,
16839            self.kv_cache.max_seq_len,
16840            gemma,
16841            self.rms_eps as f32,
16842            self.attn_scale,
16843            k,
16844            &(0..graph_end)
16845                .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
16846                .collect::<Vec<_>>(),
16847            self.o1_epoch,
16848            spec,
16849            layers_run,
16850        )
16851    }
16852
16853    /// Same, stopping after layer `upto` inclusive (routing probe φ).
16854    /// `CMF_DSV4_DRAFT_PROBE=1` — grade the draft against what the trunk goes on
16855    /// to produce. Off by default; it runs a whole draft per decoded token.
16856    fn draft_probe() -> bool {
16857        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16858        *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
16859    }
16860
16861    /// `CMF_DSV4_DRAFT_PROBE=1`: measure how much of the draft the trunk
16862    /// would have agreed with, WITHOUT verifying or rolling anything back.
16863    ///
16864    /// The number this produces decides the whole speculation design — at
16865    /// acceptance a, a block of B positions yields 1 + a + a² + ... tokens
16866    /// per trunk pass — so it is worth measuring before any of the machinery
16867    /// that would exploit it exists. Each draft is parked with the position
16868    /// it was made at, and graded as the real tokens arrive.
16869    /// `CMF_DSV4_SPEC=1` — the DeepSeek-V4 speculative decode: draft five
16870    /// on the card, verify them in one batched trunk pass, commit the
16871    /// accepted prefix, roll the rest back.
16872    #[cfg(feature = "gpu")]
16873    fn dsv4_spec_on() -> bool {
16874        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16875        *ON.get_or_init(|| {
16876            // Test-only runtime gate: model loading still performs the same
16877            // reservation and trunk packing, which gives rollback parity a
16878            // topology-identical non-speculative control arm.
16879            if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
16880                return v != "0";
16881            }
16882            // An explicit value is a diagnostic force/escape hatch.  With no
16883            // knob, speculation is eligible only when model loading reserved
16884            // its bounded pack.  On small q4tp cards the geometric reserve
16885            // gate deliberately leaves this at zero: trying to build DSpark
16886            // after the exact trunk filled VRAM is both slower and a device
16887            // OOM (measured on A40).
16888            std::env::var("CMF_DSV4_SPEC")
16889                .map(|v| v != "0")
16890                .unwrap_or_else(|_| {
16891                    crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
16892                })
16893        })
16894    }
16895
16896    /// One speculative round at the decode tip. `t_next` is the token the
16897    /// sampler just committed for `next_pos`. Returns the EXTRA accepted
16898    /// tokens (possibly none) and the new position, with `graph_logits`
16899    /// left holding the last accepted position's logits — exactly what the
16900    /// loop top expects. `None` means "speculate not this round": nothing
16901    /// was committed, the caller forwards normally.
16902    #[cfg(feature = "gpu")]
16903    fn dsv4_spec_step(
16904        &mut self,
16905        tip_token: u32,
16906        t_next: u32,
16907        next_pos: usize,
16908        max_extra: usize,
16909        drafted: &mut usize,
16910        accepted_ctr: &mut usize,
16911    ) -> Option<(Vec<u32>, usize)> {
16912        let t_all = std::time::Instant::now();
16913        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
16914            thread_local! {
16915                static LAST: std::cell::Cell<Option<std::time::Instant>> =
16916                    const { std::cell::Cell::new(None) };
16917            }
16918            LAST.with(|l| {
16919                if let Some(prev) = l.get() {
16920                    eprintln!(
16921                        "between rounds {:.1} ms",
16922                        prev.elapsed().as_secs_f64() * 1e3
16923                    );
16924                }
16925                l.set(Some(std::time::Instant::now()));
16926            });
16927        }
16928        if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16929            eprintln!("spec_step: enter pos={next_pos}");
16930        }
16931        let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
16932        let cfg = self.dsv4.as_ref().map(|b| b.2)?;
16933        // The draft state and its capture, armed exactly as the probe does.
16934        if self.dspark.is_none() {
16935            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16936            if t.is_empty() {
16937                return None;
16938            }
16939            crate::dsv4::dspark_arm(&t, cfg.dim);
16940            self.dspark = Some(crate::dsv4::DsparkState::new(
16941                self.dsv4_mtp.len(),
16942                &cfg,
16943                t.len(),
16944            ));
16945        }
16946        let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16947        let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
16948        if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16949            eprintln!("spec_step: the pack was not built (targets {targets:?})");
16950        }
16951        let pack = pack?;
16952        let block = crate::dsv4::dspark_block();
16953        let b_box = self.dsv4.as_mut()?;
16954        let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
16955        let ds = self.dspark.as_mut()?;
16956        // The tip's captures: either this token ran on a normal path that
16957        // filled the thread-local, or the previous spec round left them.
16958        let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
16959        if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
16960            if dbg {
16961                eprintln!("spec_step: no capture");
16962            }
16963            return None;
16964        }
16965        ds.have_hidden = true;
16966        let tip_pos = next_pos.checked_sub(1)?;
16967        let draft_started = std::time::Instant::now();
16968        let mut conf = Vec::new();
16969        let props = crate::dsv4::dspark_draft_gpu(
16970            g,
16971            &self.dsv4_mtp,
16972            &cfg,
16973            ds,
16974            pack,
16975            st.kv_id,
16976            tip_token,
16977            tip_pos,
16978            self.pool.as_deref(),
16979            &mut conf,
16980        );
16981        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
16982        *drafted += block;
16983        if props.is_empty() || props[0] != t_next {
16984            if dbg {
16985                eprintln!(
16986                    "spec_step: draft {} (props0={:?} t_next={t_next})",
16987                    if props.is_empty() {
16988                        "empty"
16989                    } else {
16990                        "miss"
16991                    },
16992                    props.first()
16993                );
16994            }
16995            return None;
16996        }
16997        // `fed[0]` is `t_next`, which the outer loop has already committed;
16998        // only `fed[1..]` become additional output tokens. Cap the verify
16999        // transaction itself to the caller's remaining output budget instead
17000        // of merely truncating the returned vector: otherwise the KV/state
17001        // would advance past `max_tokens` and a 64-token request could return
17002        // 66 tokens (and poison a reused session with two invisible steps).
17003        let mut k_verify = crate::dsv4::dspark_verify_k()
17004            .min(props.len())
17005            .min(max_extra.saturating_add(1));
17006        // Adaptive depth: positions the draft itself doubts are paid for on
17007        // every verify and delivered almost never (natural-text survival
17008        // [.67 .50 .29 .08 .04]). `CMF_DSPARK_CONF_MIN=p` trims the fed
17009        // prefix at the first proposal whose confidence drops below p; on
17010        // predictable text the confidences stay high and nothing changes.
17011        let conf_min = {
17012            static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
17013            *M.get_or_init(|| {
17014                std::env::var("CMF_DSPARK_CONF_MIN")
17015                    .ok()
17016                    .and_then(|v| v.parse().ok())
17017                    .unwrap_or(0.0)
17018            })
17019        };
17020        if conf_min > 0.0 && conf.len() >= props.len() {
17021            let mut keep = 1usize;
17022            while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
17023                keep += 1;
17024            }
17025            k_verify = k_verify.min(keep.max(2));
17026        }
17027        if k_verify < 2 {
17028            return None;
17029        }
17030        let mut fed = Vec::with_capacity(k_verify);
17031        fed.push(t_next);
17032        fed.extend_from_slice(&props[1..k_verify]);
17033        let mut argmax = Vec::new();
17034        let mut logits_all = Vec::new();
17035        let mut walked = Vec::new();
17036        let txn = crate::dsv4::dsv4_verify_chunk(
17037            g,
17038            layers,
17039            &cfg,
17040            st,
17041            &fed,
17042            next_pos,
17043            &self.inv_freq,
17044            self.pool.as_deref(),
17045            &targets,
17046            &mut argmax,
17047            &mut logits_all,
17048            &mut walked,
17049        );
17050        if txn.is_none() && dbg {
17051            eprintln!("spec_step: verify declined");
17052        }
17053        let txn = txn?;
17054        let spec_gpu_end = txn.gpu_end;
17055        let b = fed.len();
17056        let mut accepted = 1usize;
17057        while accepted < b && fed[accepted] == argmax[accepted - 1] {
17058            accepted += 1;
17059        }
17060        // `CMF_DSV4_SPEC_FORCE_REJECT=1` — accept nothing beyond the known
17061        // token, every round: the pure rollback exerciser. The output must
17062        // stay byte-identical to the plain walk; anything else is a
17063        // transaction bug, isolated from the acceptance logic.
17064        if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
17065            accepted = 1;
17066        }
17067        if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
17068            eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
17069        }
17070        let t_fin = std::time::Instant::now();
17071        if !crate::dsv4::dsv4_spec_finish(
17072            g,
17073            layers,
17074            &cfg,
17075            st,
17076            txn,
17077            accepted,
17078            &fed,
17079            &self.inv_freq,
17080            self.pool.as_deref(),
17081        ) {
17082            tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
17083            return None;
17084        }
17085        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
17086            eprintln!(
17087                "finish(k={accepted}): {:.1} ms",
17088                t_fin.elapsed().as_secs_f64() * 1e3
17089            );
17090        }
17091        *accepted_ctr += accepted - 1;
17092        // Captures per accepted token: device targets photographed by the
17093        // batch, host targets from the verify's own walk. The last one
17094        // becomes the new tip's draft input; every one owes the ring an
17095        // entry for its position.
17096        let (hc, dim) = (cfg.hc_mult, cfg.dim);
17097        // Complete-chain layers are photographed by the fused submission;
17098        // partial device layers overwrite that slot after exact host cold-
17099        // expert correction.  Thus every target in the contiguous device
17100        // prefix has a valid per-token capture.
17101        let dev_caps: Vec<usize> = targets
17102            .iter()
17103            .copied()
17104            .filter(|&t| t < spec_gpu_end)
17105            .collect();
17106        let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
17107        if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
17108            return None;
17109        }
17110        for t in 0..accepted {
17111            let tip = t + 1 == accepted;
17112            for (slot, &tl) in targets.iter().enumerate() {
17113                if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
17114                    let lo = (di * b + t) * hc * dim;
17115                    crate::dsv4::dspark_capture(
17116                        &caps_all[lo..lo + hc * dim],
17117                        &cfg,
17118                        slot,
17119                        &mut ds.main_hidden,
17120                    );
17121                } else if tip
17122                    && crate::dsv4::dspark_peek_slot(slot, dim, {
17123                        let lo = slot * dim;
17124                        &mut ds.main_hidden[lo..lo + dim]
17125                    })
17126                {
17127                    // The tip's host-layer captures are the walk's own
17128                    // per-layer notes — exact. (The walk that ran last ended
17129                    // on exactly this token, on both the accept-all and the
17130                    // rollback path.)
17131                } else {
17132                    // Intermediate tokens: the post-tail state stands in for
17133                    // the per-layer capture on host targets below the last
17134                    // layer. Ring-entry quality only; the tip is exact.
17135                    crate::dsv4::dspark_capture(
17136                        &walked[t * hc * dim..(t + 1) * hc * dim],
17137                        &cfg,
17138                        slot,
17139                        &mut ds.main_hidden,
17140                    );
17141                }
17142            }
17143            crate::dsv4::dspark_ring_append(
17144                g,
17145                &self.dsv4_mtp,
17146                &cfg,
17147                ds,
17148                next_pos + t,
17149                self.pool.as_deref(),
17150            );
17151        }
17152        let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
17153        self.graph_logits = Some(row);
17154        // The speculative loop never runs the probe, so the trunk tally has
17155        // no other place to cycle. Armed only when someone asked for the
17156        // dump; the host tail is the only tallying path here, which is
17157        // precisely the population a partial pack would serve.
17158        if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
17159            crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
17160            crate::dsv4::pick_tally_arm();
17161        }
17162        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
17163            eprintln!(
17164                "spec_step total {:.1} ms (k={accepted})",
17165                t_all.elapsed().as_secs_f64() * 1e3
17166            );
17167        }
17168        Some((fed[1..accepted].to_vec(), next_pos + accepted))
17169    }
17170
17171    fn dspark_probe(&mut self, position: usize, token_id: u32) {
17172        if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
17173            return;
17174        }
17175        // What the trunk just routed to, for this token.
17176        let trunk_now = crate::dsv4::pick_tally_take();
17177        crate::dsv4::trunk_freq_note(&trunk_now);
17178        if !trunk_now.is_empty() {
17179            self.dspark_trunk_picks.push(trunk_now);
17180            let keep = crate::dsv4::dspark_block();
17181            if self.dspark_trunk_picks.len() > keep {
17182                self.dspark_trunk_picks.remove(0);
17183            }
17184        }
17185        // Grade whatever is waiting: the token just decoded sits at
17186        // `position`, so it answers the draft made at `position - 1 - i`.
17187        for p in std::mem::take(&mut self.dspark_pending) {
17188            let Some(i) = position.checked_sub(p.0 + 1) else {
17189                continue;
17190            };
17191            let mut p = p;
17192            if i < p.1.len() {
17193                if p.2 && p.1[i] == token_id {
17194                    p.3 = i + 1;
17195                } else {
17196                    p.2 = false;
17197                }
17198                if i + 1 < p.1.len() {
17199                    self.dspark_pending.push(p);
17200                    continue;
17201                }
17202            }
17203            self.dspark_hist.push(p.3);
17204            self.dspark_real.push(token_id);
17205        }
17206        let Some(b) = &mut self.dsv4 else { return };
17207        let (g, layers, cfg) = (&b.0, &b.1, b.2);
17208        let n_layers = layers.len();
17209        if self.dspark.is_none() {
17210            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
17211            if t.is_empty() {
17212                return;
17213            }
17214            eprintln!(
17215                "DSpark: capture from layers {t:?}, block {}",
17216                crate::dsv4::dspark_block()
17217            );
17218            crate::dsv4::dspark_arm(&t, cfg.dim);
17219            self.dspark = Some(crate::dsv4::DsparkState::new(
17220                self.dsv4_mtp.len(),
17221                &cfg,
17222                t.len(),
17223            ));
17224        }
17225        let ds = self.dspark.as_mut().unwrap();
17226        if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
17227            return; // this token ran on a path that captures nothing
17228        }
17229        let mut conf = Vec::new();
17230        crate::dsv4::pick_tally_arm();
17231        // The trunk has already consumed the adaptive VRAM budget. Until the
17232        // draft owns an explicit bounded device pack, its tensors are an
17233        // out-of-core CPU/disk tier by contract: never let per-op probes try
17234        // to squeeze another multi-gigabyte MTP expert cache onto the card.
17235        let draft_started = std::time::Instant::now();
17236        #[cfg(feature = "gpu")]
17237        let gpu_draft = crate::dsv4::dspark_gpu_on();
17238        #[cfg(not(feature = "gpu"))]
17239        let gpu_draft = false;
17240        let props = if gpu_draft {
17241            #[cfg(feature = "gpu")]
17242            {
17243                let kv_id = b.3.kv_id;
17244                match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
17245                    Some(pk) => crate::dsv4::dspark_draft_gpu(
17246                        g,
17247                        &self.dsv4_mtp,
17248                        &cfg,
17249                        ds,
17250                        pk,
17251                        kv_id,
17252                        token_id,
17253                        position,
17254                        self.pool.as_deref(),
17255                        &mut conf,
17256                    ),
17257                    None => Vec::new(),
17258                }
17259            }
17260            #[cfg(not(feature = "gpu"))]
17261            Vec::new()
17262        } else {
17263            crate::gpu::cpu_scope(|| {
17264                crate::dsv4::dspark_draft(
17265                    g,
17266                    &self.dsv4_mtp,
17267                    &cfg,
17268                    ds,
17269                    token_id,
17270                    position,
17271                    self.pool.as_deref(),
17272                    &mut conf,
17273                )
17274            })
17275        };
17276        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
17277        let draft_picks = crate::dsv4::pick_tally_take();
17278        crate::dsv4::dspark_freq_note(&draft_picks);
17279        // Re-arm for the NEXT trunk token; the probe runs after the forward,
17280        // so this is the only place that can.
17281        crate::dsv4::pick_tally_arm();
17282        if !props.is_empty() {
17283            // Two ratios, side by side: what a batched verify over the trunk
17284            // would read against what it asks for, and the same for the
17285            // draft's three stages. Near 1.0 means a batch amortises nothing.
17286            let (tu, tt) = {
17287                let flat: Vec<(usize, Vec<usize>)> = self
17288                    .dspark_trunk_picks
17289                    .iter()
17290                    .flat_map(|v| v.iter().cloned())
17291                    .collect();
17292                // Per layer, across the window of tokens.
17293                let mut per: std::collections::HashMap<usize, Vec<usize>> =
17294                    std::collections::HashMap::new();
17295                for (li, picks) in flat {
17296                    per.entry(li).or_default().extend(picks);
17297                }
17298                let n = per.len().max(1);
17299                let mut u = 0usize;
17300                let mut t = 0usize;
17301                for (_, v) in per {
17302                    t += v.len();
17303                    u += v.iter().collect::<std::collections::HashSet<_>>().len();
17304                }
17305                (u / n, t / n)
17306            };
17307            let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
17308            self.dspark_exp.push((tu, tt, du, dt));
17309            self.dspark_pending.push((position, props, true, 0));
17310        }
17311        if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
17312            let n = self.dspark_hist.len() as f32;
17313            let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
17314            let block = crate::dsv4::dspark_block();
17315            let mut at = vec![0usize; block + 1];
17316            for &k in &self.dspark_hist {
17317                at[k] += 1;
17318            }
17319            // Prefix survival: S_i = P(the first i positions all held).
17320            let mut surv = Vec::with_capacity(block);
17321            for i in 1..=block {
17322                let k = at[i..].iter().sum::<usize>() as f32 / n;
17323                surv.push(format!("{k:.2}"));
17324            }
17325            let distinct = self
17326                .dspark_real
17327                .iter()
17328                .collect::<std::collections::HashSet<_>>()
17329                .len();
17330            let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
17331                (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
17332            });
17333            let m = self.dspark_exp.len().max(1);
17334            eprintln!(
17335                "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
17336                 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
17337                self.dspark_hist.len(),
17338                mean + 1.0,
17339                surv.join(" ")
17340            );
17341            eprintln!(
17342                "DSpark: {distinct} distinct tokens of {} (degeneracy), \
17343                 trunk experts {}/{} per layer over {block} tokens, \
17344                 draft {}/{} per block, draft {:.2} ms/block",
17345                self.dspark_real.len(),
17346                tu / m,
17347                tt / m,
17348                du / m,
17349                dt / m,
17350                self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
17351            );
17352        }
17353    }
17354
17355    fn forward_layers_upto(
17356        &mut self,
17357        hidden: &[f32],
17358        position: usize,
17359        task_mask: Option<&TaskMask>,
17360        upto: Option<usize>,
17361    ) -> Vec<f32> {
17362        // In-process multi-GPU: each segment runs pinned to its card,
17363        // and the only thing crossing the boundary is one hidden vector
17364        // that never leaves this address space. Same layer split the
17365        // network mode does, minus the second process, the socket, the
17366        // serialization and the dir_hash handshake.
17367        if let Some(plan) = self.gpu_plan.clone() {
17368            if upto.is_none() && plan.len() > 1 {
17369                let mut h = hidden.to_vec();
17370                for &(dev, from, upto_incl) in plan.iter() {
17371                    h = crate::gpu::with_device(dev, || {
17372                        self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
17373                    });
17374                }
17375                return h;
17376            }
17377        }
17378        self.forward_layers_span(hidden, position, task_mask, 0, upto)
17379    }
17380
17381    /// Split this pipeline's layer stack across local GPUs: segment i
17382    /// runs on `devices[i]`. Contiguous and even by layer count — the
17383    /// VRAM-weighted planner is the next step, and an uneven card pair
17384    /// is why it will be needed. `None` clears the plan.
17385    pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
17386        self.set_gpu_plan_at(devices, None)
17387    }
17388
17389    /// The same, with an explicit first boundary (`--peer-split`): card
17390    /// 0 takes layers `[0..at)`, the rest split what remains. Uneven
17391    /// cards, or an attention-heavy head, are why this knob exists.
17392    pub fn set_gpu_plan_at(
17393        &mut self,
17394        devices: Option<&[usize]>,
17395        at: Option<usize>,
17396    ) -> Result<(), String> {
17397        let Some(devs) = devices.filter(|d| d.len() > 1) else {
17398            self.gpu_plan = None;
17399            return Ok(());
17400        };
17401        self.split_supported()?;
17402        let n = self.num_layers;
17403        if devs.len() > n {
17404            return Err(format!("{} devices for {n} layers", devs.len()));
17405        }
17406        if let Some(k) = at {
17407            if k == 0 || k >= n {
17408                return Err(format!("split at {k}: the model has {n} layers"));
17409            }
17410            if devs.len() == 2 {
17411                self.gpu_plan = Some(std::sync::Arc::new(vec![
17412                    (devs[0], 0, k - 1),
17413                    (devs[1], k, n - 1),
17414                ]));
17415                return Ok(());
17416            }
17417            return Err(format!(
17418                "an explicit split point takes exactly 2 devices, got {}",
17419                devs.len()
17420            ));
17421        }
17422        let per = n.div_ceil(devs.len());
17423        let mut plan = Vec::with_capacity(devs.len());
17424        let mut from = 0usize;
17425        for &d in devs {
17426            if from >= n {
17427                break;
17428            }
17429            let upto = (from + per - 1).min(n - 1);
17430            plan.push((d, from, upto));
17431            from = upto + 1;
17432        }
17433        self.gpu_plan = Some(std::sync::Arc::new(plan));
17434        Ok(())
17435    }
17436
17437    /// The active in-process split, if any: (device, first layer, last).
17438    pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
17439        self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
17440    }
17441
17442    /// Layer span [from ..= upto] (upto None = last layer): the building
17443    /// block the network pipeline-split rides on. `from > 0` skips the
17444    /// arch escape hatches (the pub `forward_span` refuses those archs
17445    /// first) and the whole-token graph — the plain per-layer loop is
17446    /// the canonical executor for a partial stack.
17447    fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
17448        if let Some(x) = t.as_f32() {
17449            return x.to_vec();
17450        }
17451        let mut out = vec![0.0; t.rows() * t.cols()];
17452        for r in 0..t.rows() {
17453            t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
17454        }
17455        out
17456    }
17457
17458    fn embryo_resident_eligible(&self) -> bool {
17459        // One mixer family per file: vmf_phase (kind 0/1) or
17460        // gated_delta_net (kind 4); the anchors are full (2) or bounded (3).
17461        if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
17462            || self.num_layers != self.physical_layers
17463            || self.loop_final_norm
17464            || self.weights.layers.len() != self.num_layers
17465            || self.head_clusters.is_none()
17466            || self.final_softcap.is_some()
17467            || self.logit_multiplier.is_some()
17468            || self.attn_softcap != 0.0
17469            || self.mtp.is_some()
17470            || self.per_token_stack()
17471            || self.dsv4.is_some()
17472            || self.dsv41.is_some()
17473            || self.qwen4_exp.is_some()
17474            // Dynamic routing swaps FFN weights mid-sequence under the
17475            // packed graph; a blend has no single overlay. A STATIC skill
17476            // (`from_model_with_skill`) is fine: the pack reads the live
17477            // `weights.layers[*].ffn`, i.e. the skill's tensors, and a
17478            // later `set_active_skill` change drops the pack
17479            // (`invalidate_for_weight_change`).
17480            || self.dyn_router.is_some()
17481            || self.dyn_phi_layer.is_some()
17482            || self.dyn_blend_loaded
17483            || self.o1_cfg.is_some()
17484            || self.swa.is_some()
17485            || self.sliding_layers.is_some()
17486            || self.global_attn.is_some()
17487            || self.attention_heads_per_layer.is_some()
17488            || self.attn_v_norm
17489            || self
17490                .kv_cache
17491                .layers
17492                .iter()
17493                .any(|l| l.mode != crate::kv_cache::KvMode::F32)
17494            || self.rope_scale != 1.0
17495            || self.rope_scale_local != 1.0
17496            || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
17497            || self.hidden_size == 0
17498            || self.hidden_size > 1024
17499            || self.intermediate_size > 1024
17500            || self.num_heads == 0
17501            || self.num_kv_heads == 0
17502            || self.num_heads % self.num_kv_heads != 0
17503            || self.num_heads.saturating_mul(self.head_dim) > 1024
17504            || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
17505            || self.vocab_size == 0
17506            || self.kv_cache.max_seq_len == 0
17507            || self.rotary_dim == 0
17508            || self.rotary_dim > self.head_dim
17509            || self.rotary_dim % 2 != 0
17510            || self.inv_freq.len() < self.rotary_dim / 2
17511        {
17512            return false;
17513        }
17514        // The resident shader is deliberately an f32 profile.  Dequantizing
17515        // a Q4/Q8 tensor into the packed buffer would silently change the
17516        // operator relative to the CPU quantized path, so quantized CMFs
17517        // retain the exact ordinary executor instead of claiming parity.
17518        // Measured consequence (RTX PRO 4000, S4 bounded export requantized
17519        // with `cortiq requant --quant q4tp-quantize`): `eligible=false`,
17520        // the generic wgpu whole-token graph refuses too, and the per-op
17521        // path decodes at ~73 tok/s against ~200 tok/s on the CPU q4tp
17522        // path — a q4tp Embryo-O1 file is a CPU artifact today; the
17523        // resident graph serves the f32 export.
17524        if self.weights.lm_head.as_f32().is_none()
17525            || self.weights.embed_tokens.as_f32().is_none()
17526            || self.weights.lm_head.rows() < self.vocab_size
17527            || self.weights.lm_head.cols() != self.hidden_size
17528            || self.weights.embed_tokens.rows() < self.vocab_size
17529            || self.weights.embed_tokens.cols() != self.hidden_size
17530            || self.weights.final_norm.len() != self.hidden_size
17531        {
17532            return false;
17533        }
17534        if let Some(cfg) = self.vmf_cfg {
17535            if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
17536                || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
17537                || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
17538                || cfg.state_len() == 0
17539            {
17540                return false;
17541            }
17542        }
17543        if let Some(g) = self.gdn_cfg {
17544            // The resident GDN kernels (gpu_wgpu.rs `embryo_core_gdn_*`):
17545            // fused projection ≤ 2048 rows, nv·dv ≤ 1024, dk ≤ 128 lanes,
17546            // dv ≤ 256 lanes in vec4 rows, SiLU output gate (the Embryo
17547            // export), same rms eps as the stack.
17548            if g.num_v_heads == 0
17549                || g.num_k_heads == 0
17550                || g.num_v_heads % g.num_k_heads != 0
17551                || g.key_head_dim == 0
17552                || g.key_head_dim > 128
17553                || g.value_head_dim == 0
17554                || g.value_head_dim > 256
17555                || g.value_head_dim % 4 != 0
17556                || g.conv_kernel == 0
17557                || g.num_v_heads > 512
17558                || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
17559                || g.conv_dim() > 2048
17560                || g.conv_dim() % 4 != 0
17561                || g.hidden_size != self.hidden_size
17562                || g.output_gate_sigmoid
17563                || g.rms_eps != self.rms_eps
17564                || g.state_len() == 0
17565            {
17566                return false;
17567            }
17568        }
17569        let mut full_seen = false;
17570        for lw in &self.weights.layers {
17571            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
17572                return false;
17573            }
17574            match &lw.attn {
17575                AttnKind::LinearGdn(w) => {
17576                    let Some(g) = self.gdn_cfg else {
17577                        return false;
17578                    };
17579                    let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
17580                    if w.in_proj_qkv.rows() != g.conv_dim()
17581                        || w.in_proj_qkv.cols() != self.hidden_size
17582                        || w.in_proj_qkv.as_f32().is_none()
17583                        || w.in_proj_z.rows() != nv * dv
17584                        || w.in_proj_z.cols() != self.hidden_size
17585                        || w.in_proj_z.as_f32().is_none()
17586                        || w.in_proj_a.rows() != nv
17587                        || w.in_proj_a.cols() != self.hidden_size
17588                        || w.in_proj_a.as_f32().is_none()
17589                        || w.in_proj_b.rows() != nv
17590                        || w.in_proj_b.cols() != self.hidden_size
17591                        || w.in_proj_b.as_f32().is_none()
17592                        || w.conv1d.len() != g.conv_dim() * kk
17593                        || w.a_log.len() != nv
17594                        || w.dt_bias.len() != nv
17595                        || w.norm.len() != dv
17596                        || w.out_proj.rows() != self.hidden_size
17597                        || w.out_proj.cols() != nv * dv
17598                        || w.out_proj.as_f32().is_none()
17599                    {
17600                        return false;
17601                    }
17602                }
17603                AttnKind::Linear(w) => {
17604                    let Some(cfg) = self.vmf_cfg else {
17605                        return false;
17606                    };
17607                    if w.thq.rows() != cfg.num_heads * cfg.nphase
17608                        || w.thq.cols() != self.hidden_size
17609                        || w.thq.as_f32().is_none()
17610                        || w.thk.rows() != cfg.num_heads * cfg.nphase
17611                        || w.thk.cols() != self.hidden_size
17612                        || w.thk.as_f32().is_none()
17613                        || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
17614                        || w.v_proj.cols() != self.hidden_size
17615                        || w.v_proj.as_f32().is_none()
17616                        || w.out_proj.rows() != self.hidden_size
17617                        || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
17618                        || w.out_proj.as_f32().is_none()
17619                        || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
17620                    {
17621                        return false;
17622                    }
17623                    if let Some((kg, kb)) = &w.k_gate {
17624                        if kg.rows() != cfg.num_heads
17625                            || kg.cols() != self.hidden_size
17626                            || kg.as_f32().is_none()
17627                            || kb.len() != cfg.num_heads
17628                        {
17629                            return false;
17630                        }
17631                    }
17632                    if let Some(conv) = &w.conv {
17633                        if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
17634                            return false;
17635                        }
17636                    }
17637                }
17638                AttnKind::Full {
17639                    wq,
17640                    wk,
17641                    wv,
17642                    wo,
17643                    q_norm,
17644                    k_norm,
17645                    output_gate,
17646                    softplus_gate,
17647                    bias,
17648                } => {
17649                    if full_seen
17650                        || q_norm.is_some()
17651                        || k_norm.is_some()
17652                        || *output_gate
17653                        || softplus_gate.is_some()
17654                        || bias.is_some()
17655                        || wq.as_f32().is_none()
17656                        || wk.as_f32().is_none()
17657                        || wv.as_f32().is_none()
17658                        || wo.as_f32().is_none()
17659                        || wq.rows() != self.num_heads * self.head_dim
17660                        || wk.rows() != self.num_kv_heads * self.head_dim
17661                        || wv.rows() != self.num_kv_heads * self.head_dim
17662                        || wq.cols() != self.hidden_size
17663                        || wk.cols() != self.hidden_size
17664                        || wv.cols() != self.hidden_size
17665                        || wo.rows() != self.hidden_size
17666                        || wo.cols() != self.num_heads * self.head_dim
17667                    {
17668                        return false;
17669                    }
17670                    full_seen = true;
17671                }
17672                AttnKind::Bounded(w) => {
17673                    // The resident bounded attend walks S + W keys in
17674                    // 256-lane tiles (online softmax), any window the format
17675                    // allows; per-group windows come from header word 22.
17676                    let Some(ac) = self.anchor_core.as_ref() else {
17677                        return false;
17678                    };
17679                    if self.bounded_rope.is_none()
17680                        || w.window != ac.window
17681                        || w.sink != ac.sink
17682                        || w.group_windows != ac.group_windows
17683                        || w.window == 0
17684                        || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
17685                        || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
17686                        || w.wq.as_f32().is_none()
17687                        || w.wk.as_f32().is_none()
17688                        || w.wv.as_f32().is_none()
17689                        || w.wo.as_f32().is_none()
17690                        || w.wq.rows() != self.num_heads * self.head_dim
17691                        || w.wk.rows() != self.num_kv_heads * self.head_dim
17692                        || w.wv.rows() != self.num_kv_heads * self.head_dim
17693                        || w.wq.cols() != self.hidden_size
17694                        || w.wk.cols() != self.hidden_size
17695                        || w.wv.cols() != self.hidden_size
17696                        || w.wo.rows() != self.hidden_size
17697                        || w.wo.cols() != self.num_heads * self.head_dim
17698                    {
17699                        return false;
17700                    }
17701                }
17702                _ => return false,
17703            }
17704            match &lw.ffn {
17705                FfnKind::Dense(d) => {
17706                    if d.act != Act::Silu
17707                        || !d.segs.is_empty()
17708                        || d.gate_proj.as_f32().is_none()
17709                        || d.up_proj.as_f32().is_none()
17710                        || d.down_proj.as_f32().is_none()
17711                        || d.gate_proj.rows() != self.intermediate_size
17712                        || d.gate_proj.cols() != self.hidden_size
17713                        || d.up_proj.rows() != self.intermediate_size
17714                        || d.up_proj.cols() != self.hidden_size
17715                        || d.down_proj.rows() != self.hidden_size
17716                        || d.down_proj.cols() != self.intermediate_size
17717                    {
17718                        return false;
17719                    }
17720                }
17721                FfnKind::Moe(m) => {
17722                    if m.resonance.is_none()
17723                        || m.top_k != 1
17724                        || m.router_sigmoid
17725                        || !m.norm_topk_prob
17726                        || m.expert_bias.is_some()
17727                        || m.routed_scaling != 1.0
17728                        || m.route_tau.is_some()
17729                        || m.shared.is_none()
17730                        || m.mask.is_some()
17731                        || m.per_expert_scale.is_some()
17732                        || m.router_input_norm
17733                        || m.experts.is_empty()
17734                        || m.experts.len() > 8
17735                    {
17736                        return false;
17737                    }
17738                    let r = m.resonance.as_ref().unwrap();
17739                    if r.mu.len() != m.experts.len() * self.hidden_size
17740                        || r.bias.len() != m.experts.len()
17741                        || r.u.len() != m.experts.len() * r.k * self.hidden_size
17742                        || r.k > 128
17743                    {
17744                        return false;
17745                    }
17746                    let Some((shared, gate)) = &m.shared else {
17747                        return false;
17748                    };
17749                    if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
17750                        return false;
17751                    }
17752                    if shared.gate_proj.as_f32().is_none()
17753                        || shared.up_proj.as_f32().is_none()
17754                        || shared.down_proj.as_f32().is_none()
17755                        || shared.gate_proj.rows() != self.intermediate_size
17756                        || shared.gate_proj.cols() != self.hidden_size
17757                        || shared.up_proj.rows() != self.intermediate_size
17758                        || shared.up_proj.cols() != self.hidden_size
17759                        || shared.down_proj.rows() != self.hidden_size
17760                        || shared.down_proj.cols() != self.intermediate_size
17761                    {
17762                        return false;
17763                    }
17764                    for e in &m.experts {
17765                        if e.act != Act::Silu
17766                            || !e.segs.is_empty()
17767                            || e.gate_proj.as_f32().is_none()
17768                            || e.up_proj.as_f32().is_none()
17769                            || e.down_proj.as_f32().is_none()
17770                            || e.gate_proj.rows() != self.intermediate_size
17771                            || e.gate_proj.cols() != self.hidden_size
17772                            || e.up_proj.rows() != self.intermediate_size
17773                            || e.up_proj.cols() != self.hidden_size
17774                            || e.down_proj.rows() != self.hidden_size
17775                            || e.down_proj.cols() != self.intermediate_size
17776                        {
17777                            return false;
17778                        }
17779                    }
17780                }
17781                FfnKind::DenseMoe(_) => return false,
17782            }
17783            if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
17784                return false;
17785            }
17786        }
17787        if full_seen && self.anchor_core.is_some() {
17788            return false;
17789        }
17790        full_seen || self.num_layers > 0
17791    }
17792
17793    /// The resident Embryo graph is the owner of this pipeline's forward:
17794    /// the same gate `forward_layers_span` applies before handing a token
17795    /// to `forward_embryo_graph` (both graph phases on, the explicit
17796    /// opt-in, a wgpu device, no earlier refusal, an eligible stack).
17797    fn embryo_resident_wanted(&self) -> bool {
17798        crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17799            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17800            && matches!(
17801                std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17802                Ok("1") | Ok("parallel")
17803            )
17804            && crate::gpu::enabled_here()
17805            && !self.graph_refused()
17806            && self.embryo_resident_eligible()
17807    }
17808
17809    /// Chunked prefill on the resident graph: `ids` from `start` in
17810    /// chunks of `EMBRYO_CHUNK_MAX`, one submit each, projections/FFN as
17811    /// chunk GEMMs and the recurrent layers walked in time on the device.
17812    /// Returns the last position's logits when the whole span ran there.
17813    /// `None` = refused before any device work (the per-position path
17814    /// takes the span).  `CMF_EMBRYO_CHUNK=0` keeps the per-position
17815    /// prefill (A/B and the parity reference).
17816    fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
17817        if ids.len() < 2
17818            || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
17819            || !self.embryo_resident_wanted()
17820        {
17821            return None;
17822        }
17823        let model = self.ensure_embryo_graph()?;
17824        let cmax = std::env::var("CMF_EMBRYO_CHUNK")
17825            .ok()
17826            .and_then(|v| v.parse::<usize>().ok())
17827            .filter(|&v| v >= 1)
17828            .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
17829            .min(crate::gpu::EMBRYO_CHUNK_MAX);
17830        let hs = self.hidden_size;
17831        let n = ids.len();
17832        let mut pos = start;
17833        let mut last = None;
17834        let mut rows = Vec::with_capacity(cmax * hs);
17835        while pos < n {
17836            let end = (pos + cmax).min(n);
17837            rows.clear();
17838            for &id in &ids[pos..end] {
17839                rows.extend_from_slice(&self.embed_single(id));
17840            }
17841            let mut lg = Vec::new();
17842            if !crate::gpu::forward_embryo_graph_chunk(
17843                &model,
17844                self.graph_kv_id,
17845                &rows,
17846                pos,
17847                end - pos,
17848                &mut lg,
17849            ) {
17850                if pos == start {
17851                    if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17852                        eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
17853                    }
17854                    return None;
17855                }
17856                // The device sequence advanced through the earlier chunks;
17857                // a host continuation would mix two owners of the state.
17858                // Fail this sequence alone, leaving no stale state or key.
17859                self.kv_cache.clear();
17860                self.clear_history();
17861                crate::gpu::graph_kv_reset(self.graph_kv_id);
17862                panic!(
17863                    "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
17864                );
17865            }
17866            last = Some(lg);
17867            pos = end;
17868        }
17869        if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17870            eprintln!(
17871                "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
17872                n - start,
17873                (n - start).div_ceil(cmax)
17874            );
17875        }
17876        last
17877    }
17878
17879    fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
17880        if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
17881            const UMAX: u32 = u32::MAX;
17882            const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
17883            const REC: usize = 64;
17884            struct Pack {
17885                data: Vec<f32>,
17886            }
17887            impl Pack {
17888                fn put(&mut self, x: &[f32]) -> u32 {
17889                    if x.is_empty() {
17890                        return u32::MAX;
17891                    }
17892                    let off = self.data.len();
17893                    self.data.extend_from_slice(x);
17894                    off as u32
17895                }
17896            }
17897            // The mixer family of the file: vmf_phase geometry fills the
17898            // phase header words, gated_delta_net fills words 24..29.  A
17899            // file has exactly one linear core, so at most one is live.
17900            let vmf = self.vmf_cfg;
17901            let gdn = self.gdn_cfg;
17902            let mut pack = Pack { data: Vec::new() };
17903            let mut meta = vec![0u32; HEADER];
17904            meta[0] = self.hidden_size as u32;
17905            meta[1] = self.intermediate_size as u32;
17906            meta[2] = self.vocab_size as u32;
17907            meta[3] = self.num_layers as u32;
17908            meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
17909            meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
17910            meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
17911            if let Some(g) = gdn {
17912                meta[24] = g.num_v_heads as u32;
17913                meta[25] = g.num_k_heads as u32;
17914                meta[26] = g.key_head_dim as u32;
17915                meta[27] = g.value_head_dim as u32;
17916                meta[28] = g.conv_kernel as u32;
17917                meta[29] = g.conv_dim() as u32;
17918            }
17919            meta[7] = self.num_heads as u32;
17920            meta[8] = self.num_kv_heads as u32;
17921            meta[9] = self.head_dim as u32;
17922            meta[10] = self.kv_cache.max_seq_len as u32;
17923            let clusters = self.head_clusters.as_ref().unwrap();
17924            let cluster_count = clusters.len() / self.hidden_size;
17925            if clusters.len() % self.hidden_size != 0
17926                || cluster_count == 0
17927                || cluster_count > 1024
17928                || self.vocab_size % cluster_count != 0
17929                || self.weights.lm_head.rows() < self.vocab_size
17930                || self.weights.final_norm.len() != self.hidden_size
17931            {
17932                return None;
17933            }
17934            meta[11] = cluster_count as u32;
17935            meta[12] = (self.vocab_size / cluster_count) as u32;
17936            meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
17937            meta[16] = self.rotary_dim as u32;
17938            meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
17939            meta[19] = (self.rms_eps as f32).to_bits();
17940            let max_conv = self
17941                .weights
17942                .layers
17943                .iter()
17944                .filter_map(|lw| match &lw.attn {
17945                    AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
17946                    _ => None,
17947                })
17948                .max()
17949                .unwrap_or(1);
17950            // One state slot per recurrent layer: the phase state plus its
17951            // hidden-wide conv ring, or the GDN record `[conv ring | S]`
17952            // (`GdnCfg::state_len`).  A file carries one mixer family, so
17953            // the stride is exactly that family's record and the device
17954            // state buffer equals the header's recurrent bytes.
17955            let phase_stride = vmf
17956                .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
17957                .unwrap_or(0);
17958            let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
17959            let state_stride = phase_stride.max(gdn_stride);
17960            // Bounded genome: the KV plane of an anchor is its ring
17961            // `[kvh][W][hd]` K + V, and only anchors own one.  Legacy full
17962            // anchors keep the `max_seq` planes indexed by layer.
17963            let bounded = self.anchor_core.clone();
17964            let (anchor_window, anchor_sink) = bounded
17965                .as_ref()
17966                .map(|ac| (ac.window, ac.sink))
17967                .unwrap_or((0, 0));
17968            let kv_stride = if bounded.is_some() {
17969                2usize
17970                    .saturating_mul(self.num_kv_heads)
17971                    .saturating_mul(anchor_window)
17972                    .saturating_mul(self.head_dim)
17973            } else {
17974                2usize
17975                    .saturating_mul(self.num_kv_heads)
17976                    .saturating_mul(self.kv_cache.max_seq_len)
17977                    .saturating_mul(self.head_dim)
17978            };
17979            meta[14] = state_stride as u32;
17980            meta[15] = kv_stride as u32;
17981            meta[18] = anchor_window as u32;
17982            meta[20] = anchor_sink as u32;
17983            meta[21] = match &self.bounded_rope {
17984                Some(rope) => {
17985                    // [W][half] cos then [W][half] sin, one contiguous table.
17986                    let off = pack.put(&rope.cos);
17987                    let _ = pack.put(&rope.sin);
17988                    off
17989                }
17990                None => UMAX,
17991            };
17992            // swa_sink_v2: one window per KV group (f32 in the pack; exact
17993            // for any window the format allows); UMAX = every group `W`.
17994            meta[22] = match bounded.as_ref().filter(|ac| !ac.group_windows.is_empty()) {
17995                Some(ac) => {
17996                    let gw: Vec<f32> = ac.group_windows.iter().map(|&w| w as f32).collect();
17997                    pack.put(&gw)
17998                }
17999                None => UMAX,
18000            };
18001            let mut full_seen = false;
18002            let mut bounded_seen = 0usize;
18003            // Recurrent state slots belong to mixer layers only (phase or
18004            // GDN): an anchor owns a ring, not a state stride, so the
18005            // device state buffer is exactly the header's recurrent bytes.
18006            let mut phase_seen = 0usize;
18007            let mut gdn_seen = 0usize;
18008            for (li, lw) in self.weights.layers.iter().enumerate() {
18009                let base = meta.len();
18010                meta.resize(base + REC, UMAX);
18011                meta[base] = match &lw.attn {
18012                    AttnKind::Linear(w) if w.phase_delta => 1,
18013                    AttnKind::Linear(_) => 0,
18014                    AttnKind::Full { .. } => 2,
18015                    AttnKind::Bounded(_) => 3,
18016                    AttnKind::LinearGdn(_) => 4,
18017                    _ => UMAX,
18018                };
18019                meta[base + 1] = pack.put(&lw.input_norm);
18020                meta[base + 2] = pack.put(&lw.post_norm);
18021                meta[base + 25] = match &lw.attn {
18022                    AttnKind::Linear(_) => {
18023                        let off = ((phase_seen + gdn_seen) * state_stride) as u32;
18024                        phase_seen += 1;
18025                        off
18026                    }
18027                    AttnKind::LinearGdn(_) => {
18028                        let off = ((phase_seen + gdn_seen) * state_stride) as u32;
18029                        gdn_seen += 1;
18030                        off
18031                    }
18032                    _ => UMAX,
18033                };
18034                match &lw.attn {
18035                    AttnKind::LinearGdn(w) => {
18036                        // Layer record words 56..63 + 29, as the resident
18037                        // kernels read them (gpu_wgpu.rs `embryo_core_gdn_*`).
18038                        meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
18039                        meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
18040                        meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
18041                        meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
18042                        meta[base + 60] = pack.put(&w.conv1d);
18043                        meta[base + 61] = pack.put(&w.a_log);
18044                        meta[base + 62] = pack.put(&w.dt_bias);
18045                        meta[base + 63] = pack.put(&w.norm);
18046                        meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
18047                        meta[base + 24] = 0;
18048                    }
18049                    AttnKind::Linear(w) => {
18050                        meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
18051                        meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
18052                        meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
18053                        meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
18054                        let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
18055                        meta[base + 7] = pack.put(&decay);
18056                        if let Some((kg, kb)) = &w.k_gate {
18057                            meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
18058                            meta[base + 9] = pack.put(kb);
18059                        }
18060                        if let Some(conv) = &w.conv {
18061                            meta[base + 10] = pack.put(conv);
18062                            meta[base + 24] = (conv.len() / self.hidden_size) as u32;
18063                        } else {
18064                            meta[base + 24] = 0;
18065                        }
18066                    }
18067                    AttnKind::Full { wq, wk, wv, wo, .. } => {
18068                        full_seen = true;
18069                        meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
18070                        meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
18071                        meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
18072                        meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
18073                        meta[base + 26] = (li * kv_stride) as u32;
18074                    }
18075                    AttnKind::Bounded(w) => {
18076                        meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
18077                        meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
18078                        meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
18079                        meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
18080                        // Ring slot of this anchor (anchors only, packed).
18081                        meta[base + 26] = (bounded_seen * kv_stride) as u32;
18082                        meta[base + 27] = pack.put(&w.sink_k);
18083                        meta[base + 28] = pack.put(&w.sink_v);
18084                        bounded_seen += 1;
18085                    }
18086                    _ => return None,
18087                }
18088                match &lw.ffn {
18089                    FfnKind::Dense(d) => {
18090                        meta[base + 15] = 0;
18091                        meta[base + 16] = 0;
18092                        meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
18093                        meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
18094                        meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
18095                    }
18096                    FfnKind::Moe(m) => {
18097                        let r = m.resonance.as_ref().unwrap();
18098                        let (shared, _) = m.shared.as_ref().unwrap();
18099                        meta[base + 15] = 1;
18100                        meta[base + 16] = m.experts.len() as u32;
18101                        meta[base + 17] = pack.put(&r.mu);
18102                        meta[base + 18] = pack.put(&r.u);
18103                        meta[base + 19] = pack.put(&r.bias);
18104                        meta[base + 20] = r.k as u32;
18105                        // Word 30: the growth shell as the runtime applies
18106                        // it now (`+inf` on trunk rows, the stored finite
18107                        // shell on grown rows, all `+inf` under
18108                        // `CMF_GROWTH_SHELL=off`), followed by one `−∞`
18109                        // sentinel at index E the kernel writes as the
18110                        // score of an expert outside its shell — WGSL has
18111                        // no infinity literal, so the value travels as
18112                        // data (`embryo_core_route_finalize`).
18113                        let mut shell = r.effective_shell(m.experts.len());
18114                        shell.push(f32::NEG_INFINITY);
18115                        meta[base + 30] = pack.put(&shell);
18116                        meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
18117                        meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
18118                        meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
18119                        for (e, ex) in m.experts.iter().enumerate() {
18120                            meta[base + 32 + e * 3] =
18121                                pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
18122                            meta[base + 33 + e * 3] =
18123                                pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
18124                            meta[base + 34 + e * 3] =
18125                                pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
18126                        }
18127                    }
18128                    FfnKind::DenseMoe(_) => return None,
18129                }
18130            }
18131            if !full_seen && self.num_layers == 0 {
18132                return None;
18133            }
18134            let id = {
18135                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
18136                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
18137            };
18138            let model = crate::gpu::EmbryoGraphModel {
18139                id,
18140                hidden: self.hidden_size,
18141                intermediate: self.intermediate_size,
18142                vocab: self.vocab_size,
18143                layers: self.num_layers,
18144                phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
18145                nphase: vmf.map(|c| c.nphase).unwrap_or(0),
18146                phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
18147                anchor_q_heads: self.num_heads,
18148                anchor_kv_heads: self.num_kv_heads,
18149                anchor_head_dim: self.head_dim,
18150                rotary_dim: self.rotary_dim,
18151                max_seq: self.kv_cache.max_seq_len,
18152                cluster_count,
18153                cluster_size: self.vocab_size / cluster_count,
18154                phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
18155                state_stride,
18156                kv_stride,
18157                norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
18158                phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
18159                weights: pack.data,
18160                meta,
18161                lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
18162                clusters: clusters.as_ref().clone(),
18163                final_norm: self.weights.final_norm.clone(),
18164                inv_freq: self.inv_freq.as_ref().clone(),
18165                bounded: bounded.is_some(),
18166                kv_layers: if bounded.is_some() {
18167                    bounded_seen
18168                } else {
18169                    self.num_layers
18170                },
18171                state_layers: phase_seen + gdn_seen,
18172                anchor_window,
18173                anchor_sink,
18174                phase_layers: phase_seen,
18175                gdn_layers: gdn_seen,
18176                gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
18177                gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
18178                gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
18179                gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
18180                gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
18181            };
18182            self.embryo_graph = Some(std::sync::Arc::new(model));
18183        }
18184        self.embryo_graph.clone()
18185    }
18186
18187    fn forward_layers_span(
18188        &mut self,
18189        hidden: &[f32],
18190        position: usize,
18191        task_mask: Option<&TaskMask>,
18192        from: usize,
18193        upto: Option<usize>,
18194    ) -> Vec<f32> {
18195        #[cfg(target_os = "macos")]
18196        let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
18197            position,
18198            from,
18199            upto.map_or(self.num_layers, |u| u.saturating_add(1)),
18200        );
18201        debug_assert!(
18202            from == 0
18203                || (self.dsv4.is_none()
18204                    && self.dsv41.is_none()
18205                    && self.qwen4_exp.is_none()
18206                    && !self.per_token_stack())
18207        );
18208        // Every plain forward — the whole-token Metal graph (`q1_graph_gpu`
18209        // wraps the GDN owners zero-copy and reallocates them on a size
18210        // change) and the CPU layer loop (reads/swaps `linear_state`) —
18211        // must see the previous speculative commit's asynchronous replay
18212        // complete. One mutex probe when nothing is pending.
18213        #[cfg(target_os = "macos")]
18214        if !crate::gpu_metal::wait_replay() {
18215            self.fail_metal_graph("the pending async replay failed before a plain forward");
18216            return vec![0.0; self.hidden_size];
18217        }
18218        if let Some(b) = &mut self.qwen4_exp {
18219            let _ = (task_mask, upto);
18220            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18221            let mut logits = Vec::new();
18222            crate::qwen4_exp::forward_token(
18223                &b.0,
18224                &b.1,
18225                &b.2,
18226                &mut b.3,
18227                token_id,
18228                position,
18229                &self.inv_freq,
18230                self.pool.as_deref(),
18231                &mut logits,
18232                true,
18233            );
18234            self.graph_logits = Some(logits);
18235            return vec![0.0; self.hidden_size];
18236        }
18237        // DeepSeek-V4 runs its own stack: the state is hc_mult copies, and
18238        // the forward returns LOGITS, not a hidden — the head is inside it
18239        // (the final fold sits between the last layer and the norm). The
18240        // token id rides in `hidden[0]`, written by embed_single, because
18241        // the hash layers route by id rather than by content.
18242        if let Some(b) = &mut self.dsv4 {
18243            let _ = (task_mask, upto);
18244            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18245            let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
18246            st.pos = position;
18247            let mut logits = Vec::new();
18248            crate::dsv4::forward_token(
18249                g,
18250                layers,
18251                &cfg,
18252                st,
18253                token_id,
18254                &self.inv_freq,
18255                self.pool.as_deref(),
18256                &mut logits,
18257            );
18258            self.graph_logits = Some(logits);
18259            self.dspark_probe(position, token_id);
18260            // The caller expects a hidden; the logits went out of band, as
18261            // with the fused lm_head path.
18262            return vec![0.0; self.hidden_size];
18263        }
18264        // DeepSeek-V4.1 owns its complete stack and emits logits out of band.
18265        if let Some(b) = &mut self.dsv41 {
18266            let _ = (task_mask, upto);
18267            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18268            let mut logits = Vec::new();
18269            crate::dsv41::forward_token(
18270                &b.0,
18271                &b.1,
18272                &b.2,
18273                &mut b.3,
18274                token_id,
18275                position,
18276                self.pool.as_deref(),
18277                &mut logits,
18278            );
18279            self.graph_logits = Some(logits);
18280            return vec![0.0; self.hidden_size];
18281        }
18282        // Gemma-3n runs its own stack (4 AltUp replicas don't fit this
18283        // loop); `hidden` is the extended embedding from embed_single.
18284        // Gemma-4 E-series: same hand-off — its dual-geometry attention
18285        // with shared-KV tail layers and the PLE injection live in
18286        // `g4e_forward`.
18287        if let Some(b) = &self.g4e {
18288            let _ = (task_mask, upto);
18289            return crate::g4e::g4e_forward(
18290                &b.0,
18291                &b.1,
18292                hidden,
18293                position,
18294                &mut self.kv_cache.layers,
18295                self.num_heads,
18296                self.num_kv_heads,
18297                self.pool.as_deref(),
18298            );
18299        }
18300        if let Some(b) = &self.g3n {
18301            let _ = (task_mask, upto);
18302            return crate::g3n::g3n_forward(
18303                &b.0,
18304                &b.1,
18305                hidden,
18306                position,
18307                &mut self.kv_cache.layers,
18308                self.num_heads,
18309                self.num_kv_heads,
18310                self.head_dim,
18311                self.pool.as_deref(),
18312            );
18313        }
18314        // Cortiq Embryo owns a separate resident graph: phase recurrent
18315        // state, resonance routing, the GQA anchor KV and hierarchical head
18316        // all execute in one Vulkan submit. It is limited to a complete
18317        // unmasked stack; spans and task masks retain the exact host path.
18318        // `CMF_EMBRYO_DBG=1` names the gate that keeps a token off the
18319        // resident graph — every refusal below is otherwise silent.
18320        if from == 0
18321            && upto.is_none()
18322            && task_mask.is_none()
18323            && self.anchor_core.is_some()
18324            && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
18325        {
18326            static ONCE: std::sync::Once = std::sync::Once::new();
18327            ONCE.call_once(|| {
18328                eprintln!(
18329                    "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
18330                     unsupported={} eligible={}",
18331                    crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
18332                    crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
18333                    std::env::var("CMF_EMBRYO_RESIDENT").ok(),
18334                    crate::gpu::enabled_here(),
18335                    self.graph_refused(),
18336                    self.embryo_resident_eligible(),
18337                );
18338            });
18339        }
18340        if from == 0
18341            && upto.is_none()
18342            && task_mask.is_none()
18343            // Embryo's recurrent/KV state has no host import path.  Do not
18344            // seed it for a prefill-only graph and then silently decode from
18345            // an empty CPU cache; both phases must select the resident owner.
18346            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
18347            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
18348            // This whole-token Embryo path remains explicitly opt-in.
18349            // `CMF_GPU_WGPU_GRAPH=1` still enables the mature generic graph,
18350            // but must not silently select this model-specific resident path.
18351            && matches!(
18352                std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
18353                Ok("1") | Ok("parallel")
18354            )
18355            && crate::gpu::enabled_here()
18356            && !self.graph_refused()
18357            // Sequence owner: past position zero the device continues only
18358            // a sequence it holds. A host-owned sequence (the graph refused
18359            // at its start, or its prefix was prefilled on the host) keeps
18360            // the host path to its end — never a device attempt at p > 0
18361            // over an empty device image.
18362            && (position == 0 || self.device_sequence_position().is_some())
18363            && self.embryo_resident_eligible()
18364            && let Some(model) = self.ensure_embryo_graph()
18365        {
18366            let mut lg = Vec::new();
18367            if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
18368            {
18369                self.graph_logits = Some(lg);
18370                return vec![0.0; self.hidden_size];
18371            }
18372            if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
18373                eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
18374            }
18375            // The refusal is THIS pipeline's: falling through once is safe
18376            // at position zero (the host owns the sequence from here),
18377            // while a refusal at a later position of a device-owned
18378            // sequence would mix a host KV/state path with a partial
18379            // device sequence.
18380            self.mark_graph_refused();
18381            if position != 0 {
18382                // Fail this sequence alone and leave nothing stale behind:
18383                // no reuse key, no host or device state for the next
18384                // request on this slot to "extend".
18385                self.kv_cache.clear();
18386                self.clear_history();
18387                crate::gpu::graph_kv_reset(self.graph_kv_id);
18388                panic!(
18389                    "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
18390                );
18391            }
18392        }
18393        let mut h = hidden.to_vec();
18394        // MiMo-V2 expert placement: decided before the graph or the per-op
18395        // arena can claim the budget the expert bank needs.
18396        self.mimo_moe_prepare();
18397        let _mimo_q8 = self
18398            .mimo_moe
18399            .is_on()
18400            .then(crate::qtensor::enter_full_gpu_q8_scope);
18401        // Split borrows: copy scalars / clone handles so the per-layer
18402        // cfg does not hold `&self` while the KV cache is `&mut`.
18403        let (nh, _nkv, _hd, hs, _rd, eps) = (
18404            self.num_heads,
18405            self.num_kv_heads,
18406            self.head_dim,
18407            self.hidden_size,
18408            self.rotary_dim,
18409            self.rms_eps,
18410        );
18411        let pool = self.pool.clone();
18412        // Opt-in wgpu token-graph attention (discrete Vulkan/DX12): the whole
18413        // attention sub-block runs resident in one submit. Off by default.
18414        // Whole-token wgpu graph: eligibility + arbitration.
18415        //  - explicit CMF_GPU_WGPU_GRAPH forces it on/off;
18416        //  - discrete adapters (4090: decode 76 -> 137 tok/s) and GDN
18417        //    hybrids (recurrent state device-resident, no CPU twin to
18418        //    race) TRUST it;
18419        //  - integrated/mobile adapters RACE it against the normal path
18420        //    at generation granularity (gpu::graph_race_*) — tiled
18421        //    mobile GPUs can turn the ~300-dispatch graph into seconds
18422        //    per token, while a fast phone GPU keeps its win.
18423        let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
18424        let graph_on = match graph_env.as_deref() {
18425            Some("0") => false,
18426            Some("prefill") => false, // decode keeps the per-op path
18427            Some(_) => true,
18428            // Unset: same discrete-only default as every other graph
18429            // site. "Is the GPU on" used to stand in here — which made
18430            // the 0.2 tok/s whole-token graph race-eligible on mobile
18431            // adapters and cost 12-14× on first tokens (cmfmobile
18432            // TUNING.md); integrated GPUs keep the per-op probe path.
18433            None => crate::gpu::wgpu_graph_default(),
18434        };
18435        let graph_trusted =
18436            graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
18437        let race_eligible =
18438            graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
18439        let mut tail_start = 0usize;
18440        if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
18441            let t_graph = std::time::Instant::now();
18442            let mut lg = Vec::new();
18443            let mut gl = 0usize;
18444            let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
18445            let declined = built.is_none();
18446            let built = match built {
18447                Some(Ok(hh)) => Some(hh),
18448                Some(Err(())) => {
18449                    // O(1) state was admitted before the device failure; the
18450                    // CPU mirrors are stale by construction.  Clear the whole
18451                    // sequence and stop rather than walking that stale state.
18452                    self.clear_sequence_state();
18453                    self.graph_failed
18454                        .store(true, std::sync::atomic::Ordering::Relaxed);
18455                    self.cancel
18456                        .store(true, std::sync::atomic::Ordering::Relaxed);
18457                    tracing::error!("token graph failed after admission; sequence state cleared");
18458                    return vec![0.0; self.hidden_size];
18459                }
18460                None => None,
18461            };
18462            // Past the transient guards (o1 still collecting, a softcap)
18463            // a refusal is about the weights and will never change —
18464            // remember it instead of walking every layer again next
18465            // token.
18466            if declined && !self.o1_active() && self.attn_softcap == 0.0 {
18467                self.mark_graph_refused();
18468            }
18469            graph_note(built.is_some(), gl, self.num_layers);
18470            if let Some(hh) = built {
18471                let dur = t_graph.elapsed();
18472                if std::env::var("CMF_GRAPH_PROF").is_ok() {
18473                    eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
18474                }
18475                if gl > 0 && gl < self.num_layers {
18476                    // Device prefix: the graph ran layers 0..gl and handed
18477                    // back the boundary hidden — the loop below owns the
18478                    // tail. The prefix layers' KV/state advanced on the
18479                    // device; the tail's advances on the host below. One
18480                    // boundary crossing per token.
18481                    h = hh;
18482                    tail_start = gl;
18483                } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
18484                    if !graph_trusted {
18485                        crate::gpu::graph_race_record(true, dur);
18486                    }
18487                    if !lg.is_empty() {
18488                        // Graph produced logits (final-norm + lm_head folded in) —
18489                        // pad/cap to vocab and hand them to the sampler directly.
18490                        lg.resize(self.vocab_size, 0.0);
18491                        if let Some(c) = self.final_softcap {
18492                            softcap_in_place(&mut lg, c, self.pool.as_deref());
18493                        }
18494                        self.graph_logits = Some(lg);
18495                    }
18496                    return hh;
18497                }
18498                // Hopeless first graph token: discard it and fall through
18499                // to the normal path. Safe exactly here — the prompt KV is
18500                // still CPU-owned (chunked prefill), so recomputing this
18501                // position is exact; the mirror's extra row is never read
18502                // (the race just settled on the normal path).
18503            }
18504        }
18505        // KIMI-LINEAR HAS NO SPLIT BUG. The 2.6× reported from the
18506        // model rotation (12.2 tok/s on one card against 4.6 on two)
18507        // was a single measurement of a model whose arm arbitration is
18508        // borderline, and it did not survive repetition. Three runs an
18509        // arm, same binary, back to back:
18510        //   probe on : 1 GPU 9.5 / 5.7 / 5.9   2 GPU 7.8 / 13.0 / 13.3
18511        //   pinned   : 1 GPU 5.6 / 5.3 / 5.2   2 GPU 3.5 / 4.2 / 3.4
18512        // With the arms pinned the split costs about 1.45×, which is
18513        // what a layer split costs. With the probe free, TWO CARDS RUN
18514        // FASTER — because for this model the CPU arm wins some op
18515        // classes and the probe finds that.
18516        //
18517        // Two things do stand, and both are measured. The token graph
18518        // builds NOTHING here (`covered 0 of 14 layers [0..14)`), so
18519        // every layer walks per-op on either arm — that is where the
18520        // headroom is, not in the split. And this model's benchmark is
18521        // unusable without `CMF_GPU_PROBE=0`: the arbitration alone
18522        // moves it by more than 2×.
18523        //
18524        // Span runs (network split): the graph covers exactly [from..=upto]
18525        // — one submit per SEGMENT per token. No race: its state is global
18526        // and calibrated on full stacks, so spans take the graph only where
18527        // it is trusted by default (discrete adapters / CMF_GPU_WGPU_GRAPH).
18528        let span = from > 0 || upto.is_some();
18529        if span && graph_on && task_mask.is_none() && graph_trusted {
18530            let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
18531            let mut lg = Vec::new();
18532            let mut gl = 0usize;
18533            let span_res =
18534                self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
18535            let span_res = match span_res {
18536                Some(Ok(hh)) => Some(hh),
18537                Some(Err(())) => {
18538                    self.clear_sequence_state();
18539                    self.graph_failed
18540                        .store(true, std::sync::atomic::Ordering::Relaxed);
18541                    self.cancel
18542                        .store(true, std::sync::atomic::Ordering::Relaxed);
18543                    tracing::error!(
18544                        "span token graph failed after admission; sequence state cleared"
18545                    );
18546                    return vec![0.0; self.hidden_size];
18547                }
18548                None => None,
18549            };
18550            graph_note(span_res.is_some(), gl, upto_excl - from);
18551            if std::env::var("CMF_GPU_DEBUG").is_ok() {
18552                // How much of the span the graph actually covered. A
18553                // prefix of nothing means every layer walks per-op and
18554                // the split's extra cost is elsewhere.
18555                static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
18556                if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
18557                    eprintln!(
18558                        "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
18559                        upto_excl - from,
18560                        span_res.is_some()
18561                    );
18562                }
18563            }
18564            if let Some(hh) = span_res {
18565                if gl == upto_excl - from {
18566                    if !lg.is_empty() {
18567                        lg.resize(self.vocab_size, 0.0);
18568                        if let Some(c) = self.final_softcap {
18569                            softcap_in_place(&mut lg, c, self.pool.as_deref());
18570                        }
18571                        self.graph_logits = Some(lg);
18572                    }
18573                    crate::gpu::set_layer(-1);
18574                    return hh;
18575                }
18576                // Partial device prefix of the span: CPU owns the tail.
18577                h = hh;
18578                tail_start = from + gl;
18579            }
18580        }
18581        // Layers the host is about to run whose device mirror moved ahead
18582        // of the host cache (a device prefix that shrank since the prompt,
18583        // a batched-prefill prefix longer than this token's): bring their
18584        // rows over first. One comparison per layer when nothing lags.
18585        let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
18586
18587        // A partial graph is an explicit GPU-prefix / CPU-tail split. Keep
18588        // the tail PURE host-side: letting its QTensor hooks re-enter the
18589        // residency arena streams every omitted layer through Vulkan and the
18590        // driver's freed-allocation cache can grow to the full model size
18591        // (25.4 GiB observed with a 14 GiB budget on Granite 30B Q8_2F).
18592        // With a MiMo expert bank the tail is not a whole-layer host
18593        // stream: its experts run from the bank (never the arena) and its
18594        // projections stay per-op on the device, which the bank's placement
18595        // left room for.
18596        let host_tail = tail_start > from;
18597        let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
18598        let automatic_gpu_prefix = self.automatic_gpu_prefix();
18599
18600        let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
18601        #[cfg(target_os = "macos")]
18602        let mut gpu_skip_until = 0usize;
18603        for li in tail_start.max(from)..self.num_layers {
18604            let _capacity_tail = automatic_gpu_prefix
18605                .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
18606                .map(|_| crate::gpu::enter_cpu_scope());
18607            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU (CMF_GPU_LAYERS)
18608            if let Some(u) = upto {
18609                if li > u {
18610                    break;
18611                }
18612            }
18613            if let Some(mask) = task_mask {
18614                if !mask.layer_alive(li) {
18615                    continue; // dead layer: residual pass-through
18616                }
18617            }
18618            // MiniMax's dynamic tail: attention + pooled MoE of this layer as
18619            // ONE device frame (single-layer graph call), cold experts on
18620            // the host. Any decline falls through to the ordinary host
18621            // layer; a post-admission device failure stops the sequence.
18622            if host_tail && task_mask.is_none() {
18623                match self.m3_pooled_layer(li, position, &mut h) {
18624                    Ok(true) => {
18625                        crate::gpu::set_layer(-1);
18626                        continue;
18627                    }
18628                    Ok(false) => {}
18629                    Err(()) => {
18630                        self.clear_sequence_state();
18631                        self.graph_failed
18632                            .store(true, std::sync::atomic::Ordering::Relaxed);
18633                        self.cancel
18634                            .store(true, std::sync::atomic::Ordering::Relaxed);
18635                        tracing::error!(
18636                            "pooled tail token graph failed after admission; sequence state cleared"
18637                        );
18638                        return vec![0.0; self.hidden_size];
18639                    }
18640                }
18641            }
18642            // Whole-block q1 token graph: a run of consecutive q1
18643            // layers — GDN and full attention — executes with one sync
18644            // per CPU attend instead of per op (macOS/Metal).
18645            #[cfg(target_os = "macos")]
18646            {
18647                if li < gpu_skip_until {
18648                    continue;
18649                }
18650                if task_mask.is_none() {
18651                    let end = self.q1_graph_gpu(li, upto, position, &mut h);
18652                    if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
18653                        // The graph may have mutated device state before a
18654                        // command-buffer error. Never continue with a CPU
18655                        // tail or read a stale host mirror after admission.
18656                        return vec![0.0; self.hidden_size];
18657                    }
18658                    if end > li {
18659                        gpu_skip_until = end;
18660                        // Looped Transformer: the graph stopped at a loop
18661                        // boundary — apply final norm before the next iteration.
18662                        if self.is_loop_end(end - 1) && end < self.num_layers {
18663                            h = inference::rms_norm(
18664                                &h,
18665                                &self.weights.final_norm,
18666                                self.rms_eps,
18667                                self.norm_style,
18668                            );
18669                        }
18670                        continue;
18671                    }
18672                }
18673            }
18674
18675            if task_mask.is_none() {
18676                match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
18677                    crate::gpu::BatchGraphOutcome::Completed => continue,
18678                    crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
18679                    crate::gpu::BatchGraphOutcome::Declined => {}
18680                }
18681            }
18682            #[cfg(feature = "gpu")]
18683            self.pull_lagging_host_kv(li, li + 1, position);
18684            // SenseNova-Loop mHC: the attention reads the routed branch of
18685            // the streams.
18686            let mhc_attn = self.mhc_layer(li).then(|| self.mhc_pre(li, false, &mut h));
18687            let lw = &self.weights.layers[self.phys_layer(li)];
18688            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
18689                if tp.parse::<usize>().ok() == Some(position) {
18690                    let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
18691                    eprintln!(
18692                        "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
18693                        h[0], h[1]
18694                    );
18695                }
18696            }
18697            // Norm into the pipeline scratch — the returning rms_norm
18698            // allocated twice per layer per token (roadmap §3 P0).
18699            let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
18700            inference::rms_norm_into(
18701                &h,
18702                &lw.input_norm,
18703                self.rms_eps,
18704                self.norm_style,
18705                &mut self.ws.n1,
18706            );
18707            drop(prof);
18708
18709            let attn_out = match &lw.attn {
18710                AttnKind::Mla(w) => {
18711                    let inv_freq_l = self.layer_inv_freq(li);
18712                    let rs = self.layer_rope_scale(li);
18713                    let eps = self.rms_eps;
18714                    let pool = self.pool.clone();
18715                    mla_attention(
18716                        w,
18717                        &self.ws.n1,
18718                        &mut self.kv_cache.layers[li],
18719                        position,
18720                        &inv_freq_l,
18721                        rs,
18722                        eps,
18723                        pool.as_deref(),
18724                    )
18725                }
18726                AttnKind::Linear(w) => {
18727                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
18728                    vmf_phase_forward(
18729                        &self.ws.n1,
18730                        w,
18731                        &cfg,
18732                        &mut self.kv_cache.layers[li].linear_state,
18733                        self.pool.as_deref(),
18734                    )
18735                }
18736                AttnKind::Kda(w) => {
18737                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
18738                    crate::linear_core::kda_forward(
18739                        &self.ws.n1,
18740                        w,
18741                        &cfg,
18742                        &mut self.kv_cache.layers[li].linear_state,
18743                        self.pool.as_deref(),
18744                    )
18745                }
18746                AttnKind::LinearGdn(w) => {
18747                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
18748                    gdn_forward(
18749                        &self.ws.n1,
18750                        w,
18751                        &cfg,
18752                        &mut self.kv_cache.layers[li].linear_state,
18753                        self.pool.as_deref(),
18754                    )
18755                }
18756                AttnKind::ShortConv(w) => {
18757                    let cfg = self
18758                        .short_conv_cfg
18759                        .expect("short-conv layer without short_conv_cfg");
18760                    short_conv_forward(
18761                        &self.ws.n1,
18762                        w,
18763                        &cfg,
18764                        &mut self.kv_cache.layers[li].linear_state,
18765                        self.pool.as_deref(),
18766                    )
18767                }
18768                AttnKind::Bounded(w) => {
18769                    // Natively bounded anchor: insert into the ring, attend
18770                    // over sinks ∪ window. No position, nothing appended.
18771                    let rope = self
18772                        .bounded_rope
18773                        .clone()
18774                        .expect("bounded layer without an installed rotation table");
18775                    let cfg = crate::bounded::BoundedAttnCfg {
18776                        num_heads: self.num_heads,
18777                        num_kv_heads: self.num_kv_heads,
18778                        head_dim: self.head_dim,
18779                        hidden_size: hs,
18780                        scale: self.attn_scale,
18781                        rope: &rope,
18782                        pool: pool.as_deref(),
18783                    };
18784                    crate::bounded::bounded_attention(
18785                        &self.ws.n1,
18786                        w,
18787                        &mut self.kv_cache.layers[li],
18788                        &cfg,
18789                    )
18790                }
18791                AttnKind::Full {
18792                    wq,
18793                    wk,
18794                    wv,
18795                    wo,
18796                    q_norm,
18797                    k_norm,
18798                    output_gate,
18799                    softplus_gate,
18800                    bias,
18801                } if self.kv_cache.layers[li].o1_sealed() => {
18802                    // O(1) override: decode on the sealed Nyström state
18803                    // instead of the growing KV cache.
18804                    let inv_freq_l = self.layer_inv_freq(li);
18805                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18806                    let cfg = QwenAttnCfg {
18807                        mrope: None,
18808                        num_heads: self.layer_num_heads(li),
18809                        num_kv_heads: nkv_l,
18810                        head_dim: hd_l,
18811                        hidden_size: hs,
18812                        position,
18813                        inv_freq: &inv_freq_l,
18814                        rotary_dim: rd_l,
18815                        scale: self.attn_scale,
18816                        softcap: self.attn_softcap,
18817                        window: None,
18818                        v_norm: self.attn_v_norm,
18819                        qk_norm_after_rope: self.qk_norm_after_rope,
18820                        gate_sigmoid: self.proj_gate_sigmoid,
18821                        q_norm: q_norm.as_deref(),
18822                        k_norm: k_norm.as_deref(),
18823                        output_gate: *output_gate,
18824                        softplus_gate: softplus_gate
18825                            .as_ref()
18826                            .map(|(gate, per_head)| (gate, *per_head)),
18827                        rope_scale: self.layer_rope_scale(li),
18828                        bias: bias
18829                            .as_ref()
18830                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18831                        rms_eps: eps,
18832                        norm_style: self.norm_style,
18833                        pool: pool.as_deref(),
18834                        v_head_dim: self.layer_v_dim(li),
18835                    };
18836                    attention::qwen_attention_nystrom(
18837                        &self.ws.n1,
18838                        wq,
18839                        wk,
18840                        wv,
18841                        wo,
18842                        &mut self.kv_cache.layers[li],
18843                        &cfg,
18844                    )
18845                }
18846                AttnKind::Full {
18847                    wq,
18848                    wk,
18849                    wv,
18850                    wo,
18851                    q_norm,
18852                    k_norm,
18853                    output_gate,
18854                    softplus_gate,
18855                    bias,
18856                } => 'attn: {
18857                    // wgpu token-graph attention (opt-in): whole sub-block in
18858                    // one submit, device K/V mirror. q1 only, no gate/bias/mask.
18859                    // Its kernel has no window, sink or narrow-V slot and
18860                    // one mirror geometry: such models stay on the CPU attend.
18861                    let dropin_reason =
18862                        graph_on.then(|| self.graph_attn_decline_reason()).flatten();
18863                    if let Some(reason) = dropin_reason {
18864                        self.note_graph_decline("wgpu attn dropin", reason);
18865                    }
18866                    if graph_on
18867                        && dropin_reason.is_none()
18868                        && !*output_gate
18869                        && softplus_gate.is_none()
18870                        && self.attention_heads_per_layer.is_none()
18871                        && bias.is_none()
18872                        && task_mask.is_none()
18873                    {
18874                        let inv_freq_l = self.layer_inv_freq(li);
18875                        let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18876                        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
18877                        if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
18878                            wq.mapped_q1(),
18879                            wk.mapped_q1(),
18880                            wv.mapped_q1(),
18881                            wo.mapped_q1(),
18882                        ) {
18883                            let gm = gm.clone();
18884                            let mut out = vec![0f32; hs];
18885                            let cache = &self.kv_cache.layers[li];
18886                            if crate::gpu::attn_dropin(
18887                                &gm,
18888                                self.graph_kv_id,
18889                                li,
18890                                &self.ws.n1,
18891                                qi,
18892                                ki,
18893                                vi,
18894                                oi,
18895                                q_norm.as_deref(),
18896                                k_norm.as_deref(),
18897                                self.qk_norm_after_rope,
18898                                &inv_freq_l,
18899                                nh,
18900                                nkv_l,
18901                                hd_l,
18902                                rd_l,
18903                                hs,
18904                                position,
18905                                self.kv_cache.max_seq_len,
18906                                gemma,
18907                                eps as f32,
18908                                cache.k_heads(),
18909                                cache.v_heads(),
18910                                &mut out,
18911                            ) {
18912                                break 'attn out;
18913                            }
18914                        }
18915                    }
18916                    let masked = task_mask
18917                        .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
18918                        .unwrap_or(false);
18919                    let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
18920                    // The masked kernel knows one pipeline-wide geometry and
18921                    // RoPE table, no window and no sink.
18922                    let plain = self.layer_attn_plain(li);
18923                    match (masked, f32_view) {
18924                        // Historical masked path (f32 slices; the loader
18925                        // keeps masked models in f32).
18926                        (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
18927                            let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
18928                            attention::multi_head_attention(
18929                                &self.ws.n1,
18930                                q,
18931                                k,
18932                                v,
18933                                o,
18934                                &mut self.kv_cache.layers[li],
18935                                self.num_heads,
18936                                self.num_kv_heads,
18937                                self.head_dim,
18938                                self.hidden_size,
18939                                position,
18940                                &active_heads,
18941                                &self.inv_freq,
18942                            )
18943                        }
18944                        (masked, _) => {
18945                            if masked {
18946                                tracing::warn!(
18947                                    "layer {li}: head mask on quantized weights or on a \
18948                                     window/sink/per-layer-geometry layer not supported \
18949                                     yet — executing dense"
18950                                );
18951                            }
18952                            let inv_freq_l = self.layer_inv_freq(li);
18953                            let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18954                            let cfg = QwenAttnCfg {
18955                                mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
18956                                num_heads: self.layer_num_heads(li),
18957                                num_kv_heads: nkv_l,
18958                                head_dim: hd_l,
18959                                hidden_size: hs,
18960                                position,
18961                                inv_freq: &inv_freq_l,
18962                                rotary_dim: rd_l,
18963                                scale: self.attn_scale,
18964                                softcap: self.attn_softcap,
18965                                window: self.layer_window(li),
18966                                v_norm: self.attn_v_norm,
18967                                qk_norm_after_rope: self.qk_norm_after_rope,
18968                                gate_sigmoid: self.proj_gate_sigmoid,
18969                                q_norm: q_norm.as_deref(),
18970                                k_norm: k_norm.as_deref(),
18971                                output_gate: *output_gate,
18972                                softplus_gate: softplus_gate
18973                                    .as_ref()
18974                                    .map(|(gate, per_head)| (gate, *per_head)),
18975                                rope_scale: self.layer_rope_scale(li),
18976                                bias: bias
18977                                    .as_ref()
18978                                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18979                                rms_eps: eps,
18980                                norm_style: self.norm_style,
18981                                pool: pool.as_deref(),
18982                                v_head_dim: self.layer_v_dim(li),
18983                            };
18984                            attention::qwen_attention(
18985                                &self.ws.n1,
18986                                wq,
18987                                wk,
18988                                wv,
18989                                wo,
18990                                &mut self.kv_cache.layers[li],
18991                                &cfg,
18992                            )
18993                        }
18994                    }
18995                }
18996            };
18997            // Gemma sandwich norm: normalize the attention branch before
18998            // it joins the residual stream.
18999            let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
19000                Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
19001                None => attn_out,
19002            };
19003            let mhc_ffn = match mhc_attn {
19004                Some(c) => {
19005                    // mHC: fold the attention output in, route the FFN's
19006                    // branch, and norm that.
19007                    self.mhc_post(li, false, &attn_out, &c, &mut h);
19008                    let cf = self.mhc_pre(li, true, &mut h);
19009                    let lw = &self.weights.layers[self.phys_layer(li)];
19010                    inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
19011                    Some(cf)
19012                }
19013                None => {
19014                    let lw = &self.weights.layers[self.phys_layer(li)];
19015                    let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
19016                    inference::add_rmsnorm_fused_into(
19017                        &mut h,
19018                        &attn_out,
19019                        &lw.post_norm,
19020                        self.rms_eps,
19021                        self.norm_style,
19022                        &mut self.ws.p1,
19023                    );
19024                    drop(prof);
19025                    None
19026                }
19027            };
19028            let lw = &self.weights.layers[self.phys_layer(li)];
19029            let mut attn_out = attn_out;
19030            attention::recycle_buf(&mut attn_out);
19031            let post_normed = &self.ws.p1;
19032
19033            let ffn_masked = task_mask
19034                .map(|m| m.ffn_active_count(li) < self.intermediate_size)
19035                .unwrap_or(false);
19036            // One masked dense CONTRACT, dispatched by cost. The
19037            // activation-zeroing arm (the batched sweep's, validated
19038            // against the replica to 0.8%) computes the FULL fused FFN
19039            // and zeroes the dead — right whenever most neurons live.
19040            // The sparse arm reads ONLY active rows and down columns —
19041            // per-row dots are slower per element than the fused kernel,
19042            // so it pays only once the mask is deep enough. The 0.5
19043            // crossover is first-principles (fused kernels run ~2x the
19044            // per-row dot throughput); a shallow specialist (95% alive)
19045            // stays fused, a --target-sparsity bake flips arms on its
19046            // own weight.
19047            let ffn_out = match (ffn_masked, &lw.ffn) {
19048                // A defragged tube layer answers its own mask: the core
19049                // always runs, each tube runs when its bit is on, and
19050                // the tubes that are off are never read from the mmap.
19051                (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
19052                    let row = task_mask
19053                        .and_then(|tm| tm.ffn_masks.get(li))
19054                        .map(|v| v.as_slice());
19055                    tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
19056                }
19057                (true, FfnKind::Dense(d)) => {
19058                    let tm = task_mask.unwrap();
19059                    let alive = tm.ffn_active_count(li);
19060                    let deep = alive * 2 <= self.intermediate_size;
19061                    if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
19062                        let active = tm.ffn_active_indices(li);
19063                        sparse_ffn_quant(
19064                            d,
19065                            post_normed,
19066                            &active,
19067                            self.hidden_size,
19068                            self.pool.as_deref(),
19069                        )
19070                    } else if deep
19071                        && let (Some(g), Some(u), Some(dn)) = (
19072                            d.gate_proj.as_f32(),
19073                            d.up_proj.as_f32(),
19074                            d.down_proj.as_f32(),
19075                        )
19076                    {
19077                        let active = tm.ffn_active_indices(li);
19078                        inference::sparse_ffn_forward(
19079                            post_normed,
19080                            g,
19081                            u,
19082                            dn,
19083                            self.hidden_size,
19084                            self.intermediate_size,
19085                            &active,
19086                            self.pool.as_deref(),
19087                        )
19088                    } else {
19089                        let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
19090                        dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
19091                    }
19092                }
19093                (true, FfnKind::Moe(m)) => {
19094                    // MoE is sparse by expert selection; a task mask
19095                    // narrows the ROUTABLE set via its expert fields
19096                    // (spec §5) when it carries them.
19097                    let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
19098                    ffn_forward(
19099                        &lw.ffn,
19100                        post_normed,
19101                        self.pool.as_deref(),
19102                        allowed.as_deref(),
19103                    )
19104                }
19105                (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
19106                    dm,
19107                    post_normed,
19108                    &h,
19109                    self.rms_eps,
19110                    self.norm_style,
19111                    self.pool.as_deref(),
19112                ),
19113                (false, _) => match &lw.ffn {
19114                    FfnKind::DenseMoe(dm) => dense_moe_ffn(
19115                        dm,
19116                        post_normed,
19117                        &h,
19118                        self.rms_eps,
19119                        self.norm_style,
19120                        self.pool.as_deref(),
19121                    ),
19122                    FfnKind::Moe(m)
19123                        if task_mask.is_none()
19124                            && host_tail
19125                            && li >= tail_start
19126                            && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
19127                    {
19128                        moe_ffn_minicpm_tail(
19129                            &mut self.minicpm_tail,
19130                            li,
19131                            self.num_layers,
19132                            m,
19133                            post_normed,
19134                            self.pool.as_deref(),
19135                        )
19136                    }
19137                    FfnKind::Moe(m)
19138                        if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
19139                    {
19140                        moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
19141                    }
19142                    _ => {
19143                        let allowed = match (&lw.ffn, task_mask) {
19144                            (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
19145                            _ => None,
19146                        };
19147                        ffn_forward(
19148                            &lw.ffn,
19149                            post_normed,
19150                            self.pool.as_deref(),
19151                            allowed.as_deref(),
19152                        )
19153                    }
19154                },
19155            };
19156            let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
19157                Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
19158                None => ffn_out,
19159            };
19160            match mhc_ffn {
19161                Some(c) => self.mhc_post(li, true, &ffn_out, &c, &mut h),
19162                None => {
19163                    for (i, &f) in ffn_out.iter().enumerate() {
19164                        h[i] += f;
19165                    }
19166                }
19167            }
19168            let mut ffn_out = ffn_out;
19169            attention::recycle_buf(&mut ffn_out);
19170
19171            // Gemma-4: the layer output is scaled by a learned scalar.
19172            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
19173                for v in h.iter_mut() {
19174                    *v *= sc;
19175                }
19176            }
19177            // CMF_LAYER_DUMP: this position's hidden after layer li.
19178            if self.layer_dump.is_some() {
19179                self.dump_layer_row(position, li, &h);
19180            }
19181
19182            // Looped Transformer: apply final norm at the end of each loop iteration.
19183            // Nanbeige 4.2: after layer 21 (virtual), apply norm before looping back to layer 0.
19184            if self.is_loop_end(li) && li + 1 < self.num_layers {
19185                h = inference::rms_norm(
19186                    &h,
19187                    &self.weights.final_norm,
19188                    self.rms_eps,
19189                    self.norm_style,
19190                );
19191            }
19192
19193            // Dynamic routing φ capture (on-policy): the
19194            // EMA of the post-residual hidden at the router's phi_layer,
19195            // updated as the context evolves during decode.
19196            if self.dyn_phi_layer == Some(li) {
19197                self.update_dyn_phi(&h);
19198            }
19199        }
19200        crate::gpu::set_layer(-1); // layers done — lm_head outside layer-split
19201        if let Some(t) = t_race_cpu {
19202            crate::gpu::graph_race_record(false, t.elapsed());
19203        }
19204
19205        h
19206    }
19207
19208    /// EMA of φ at the router layer (rolling, weight 0.2 = ~5-token
19209    /// horizon). First observation seeds it exactly.
19210    fn update_dyn_phi(&mut self, h: &[f32]) {
19211        const A: f32 = 0.2;
19212        if self.dyn_phi_ema.len() != h.len() {
19213            self.dyn_phi_ema = vec![0.0; h.len()];
19214            self.dyn_phi_seen = 0;
19215        }
19216        if self.dyn_phi_seen == 0 {
19217            self.dyn_phi_ema.copy_from_slice(h);
19218        } else {
19219            for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
19220                *e = (1.0 - A) * *e + A * v;
19221            }
19222        }
19223        self.dyn_phi_seen += 1;
19224    }
19225
19226    /// Current router φ (EMA at phi_layer); empty until first capture.
19227    pub fn dyn_phi(&self) -> &[f32] {
19228        &self.dyn_phi_ema
19229    }
19230
19231    /// Enable/disable φ capture at the router layer, reset the EMA.
19232    pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
19233        self.dyn_phi_layer = layer;
19234        self.dyn_phi_ema.clear();
19235        self.dyn_phi_seen = 0;
19236    }
19237
19238    /// Skills eligible for dynamic switching: (index, id, phi_layer).
19239    pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
19240        let Some(model) = &self.model else {
19241            return Vec::new();
19242        };
19243        model
19244            .header
19245            .skills
19246            .iter()
19247            .enumerate()
19248            .filter_map(|(i, sk)| {
19249                // A v2 record routes only through the request-level
19250                // backbone-gated decision (its status and gate are
19251                // checked there), never per token.
19252                if sk.is_v2() {
19253                    return None;
19254                }
19255                let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
19256                let sel = sk.selection.as_ref()?;
19257                (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
19258            })
19259            .collect()
19260    }
19261
19262    /// Index of the currently overlaid skill (None = backbone).
19263    pub fn active_skill(&self) -> Option<usize> {
19264        self.dyn_active
19265    }
19266
19267    /// Enable dynamic per-token skill routing: build the hysteresis
19268    /// router from the container's routable skills, start φ capture at
19269    /// their (shared) phi_layer. Returns the number of routable skills
19270    /// (0 = nothing to route; router stays off). Idempotent.
19271    pub fn enable_dynamic_routing(&mut self) -> usize {
19272        use crate::swarm::{DynRouter, RoutableSkill};
19273        let Some(model) = self.model.clone() else {
19274            return 0;
19275        };
19276        // Router policy v2 (spec §9.4) routes per REQUEST: the backbone is
19277        // the default and only the backbone-gated decision may pick a
19278        // skill. A per-token switch would bypass that gate (and change
19279        // the O(1) state mid-sequence), so the hysteresis router never
19280        // runs on such a file; the caller keeps the request-level
19281        // decision.
19282        if let Some(r) = &model.header.router {
19283            tracing::warn!(
19284                "dynamic routing disabled: this file declares router policy '{}' with \
19285                 granularity \"{}\" — the request-level decision applies instead",
19286                r.policy,
19287                r.granularity
19288            );
19289            return 0;
19290        }
19291        // Format-v2 skill records (bit SKILLS_V2) without a router policy:
19292        // their status/gate contract ("auto-routing requires active +
19293        // measured") lives in the backbone-gated decision only — the
19294        // hysteresis router would switch into a quarantined record
19295        // (fail-open). Refuse the whole file, not just its v2 records.
19296        if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
19297            || model.header.skills.iter().any(|s| s.is_v2())
19298        {
19299            tracing::warn!(
19300                "dynamic routing disabled: this file carries format-v2 skill records \
19301                 (SKILLS_V2) — they route per request through a router policy only"
19302            );
19303            return 0;
19304        }
19305        // A blend materialized f32 working tensors into the layers; there
19306        // is no single skill index to revert from → refuse (honest).
19307        if self.dyn_blend_loaded {
19308            tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
19309            return 0;
19310        }
19311        // A statically-overlaid skill that is NOT FFN-eligible can't be
19312        // cheaply reverted at generation start → refuse rather than
19313        // silently keep it overlaid.
19314        if let Some(a) = self.dyn_active {
19315            if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
19316                tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
19317                return 0;
19318            }
19319        }
19320        let hidden = self.hidden_size;
19321        let mut skills = Vec::new();
19322        for (idx, id, _phi) in self.dynamic_skills() {
19323            if let Some(sel) = model.header.skills[idx].selection.as_ref() {
19324                if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
19325                    skills.push(rs);
19326                }
19327            }
19328        }
19329        if skills.is_empty() {
19330            return 0;
19331        }
19332        // Skills should share a phi_layer; warn (not fail) if they don't.
19333        let phi = skills[0].phi_layer;
19334        if skills.iter().any(|s| s.phi_layer != phi) {
19335            tracing::warn!("routable skills disagree on phi_layer; using {phi}");
19336        }
19337        let n = skills.len();
19338        self.set_dyn_phi_layer(Some(phi));
19339        self.dyn_router = Some(DynRouter::new(skills));
19340        n
19341    }
19342
19343    /// Human-readable switch log from the last dynamic-routed generation.
19344    pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
19345        self.dyn_router
19346            .as_ref()
19347            .map(|r| r.switches.clone())
19348            .unwrap_or_default()
19349    }
19350
19351    /// LM head: hidden → logits [vocab_size]. The dominant matvec of
19352    /// every decode step — row-parallel on the worker pool.
19353    fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
19354        let _mimo_q8 = self
19355            .mimo_moe
19356            .is_on()
19357            .then(crate::qtensor::enter_full_gpu_q8_scope);
19358        let rows = self.weights.lm_head.rows();
19359        let mut logits = attention::take_buf(rows.min(self.vocab_size));
19360        // Banked MiMo uses the same exact projection family for the
19361        // plain/draft head and the batched verification head. Read both
19362        // scale planes in-place instead of preparing per-op scale buffers.
19363        let served = self.mimo_moe.is_on()
19364            && crate::gpu::mimo_q8_short_enabled()
19365            && rows == self.vocab_size
19366            && !self.weights.lm_head.has_prism_contract()
19367            && self
19368                .weights
19369                .lm_head
19370                .graph_weight()
19371                .is_some_and(|(model, idx, kind, _)| {
19372                    kind == 7
19373                        && crate::gpu::q82_short_rows(
19374                            model,
19375                            idx,
19376                            hidden,
19377                            1,
19378                            rows,
19379                            self.hidden_size,
19380                            &mut logits,
19381                        )
19382                });
19383        if !served {
19384            self.weights
19385                .lm_head
19386                .matvec(hidden, &mut logits, self.pool.as_deref());
19387        }
19388        logits.resize(self.vocab_size, 0.0);
19389        if let Some(m) = self.logit_multiplier {
19390            for l in logits.iter_mut() {
19391                *l *= m;
19392            }
19393        }
19394        if let Some(c) = self.final_softcap {
19395            softcap_in_place(&mut logits, c, self.pool.as_deref());
19396        }
19397        if let Some(cm) = self.head_clusters.as_ref() {
19398            self.hierarchical_head_logprobs(hidden, cm, &mut logits);
19399        }
19400        logits
19401    }
19402
19403    /// Two-level head (Cortiq Embryo): in place, logits[v] ← log p(v) =
19404    /// (lc[c] − lse(lc)) + (logit[v] − lse over v's cluster block), c = v / S.
19405    fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
19406        let h = hidden.len();
19407        let ncl = cm.len() / h.max(1);
19408        if ncl == 0 || logits.len() % ncl != 0 {
19409            return;
19410        }
19411        let cs = logits.len() / ncl;
19412        // cluster logits + log-softmax
19413        let mut lc = vec![0.0f32; ncl];
19414        for c in 0..ncl {
19415            let row = &cm[c * h..(c + 1) * h];
19416            let mut s = 0.0f32;
19417            for j in 0..h {
19418                s += row[j] * hidden[j];
19419            }
19420            lc[c] = s;
19421        }
19422        let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19423        let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
19424        for c in 0..ncl {
19425            let blk = &mut logits[c * cs..(c + 1) * cs];
19426            let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19427            let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
19428            let add = lc[c] - lse - bl;
19429            for v in blk.iter_mut() {
19430                *v += add;
19431            }
19432        }
19433    }
19434
19435    /// Prefill `ids` and return the next-token logits — what the model
19436    /// would predict next, WITHOUT committing to generation (introspection
19437    /// for `cortiq explain`). Clears and repopulates the KV cache; leaves
19438    /// the active overlay untouched.
19439    pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
19440        #[cfg(target_os = "macos")]
19441        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
19442        self.clear_sequence_state();
19443        // This helper is used by the pooled classification endpoint, where
19444        // every request is a fresh sequence. The shared reset also clears the
19445        // wgpu token graph's device-side recurrent state.
19446        crate::gpu::graph_race_begin_generation();
19447        if task_mask.is_none() {
19448            self.o1_begin();
19449        }
19450        let mut hidden = vec![0.0f32; self.hidden_size];
19451        for (pos, &id) in ids.iter().enumerate() {
19452            let emb = self.embed_single(id);
19453            hidden = self.forward_layers(&emb, pos, task_mask);
19454        }
19455        if let Err(err) = self.o1_seal_checked() {
19456            self.o1_fail(err);
19457        }
19458        // Stacks that own their head (V4, V4.1, Qwen3.8-Flash-Next, GLM-5)
19459        // return a zero hidden and hand the logits out of band.
19460        if let Some(logits) = self.graph_logits.take() {
19461            return logits;
19462        }
19463        inference::rms_norm_into(
19464            &hidden,
19465            &self.weights.final_norm,
19466            self.rms_eps,
19467            self.norm_style,
19468            &mut self.ws.n1,
19469        );
19470        self.lm_head_forward(&self.ws.n1)
19471    }
19472}
19473
19474/// Convenience: deterministic tiny pipeline for tests.
19475pub fn create_test_pipeline(
19476    hidden_size: usize,
19477    intermediate_size: usize,
19478    num_heads: usize,
19479    num_kv_heads: usize,
19480    head_dim: usize,
19481    num_layers: usize,
19482    vocab_size: usize,
19483) -> Pipeline {
19484    // Small pseudo-random weights: constant weights make attention
19485    // degenerate and hide indexing bugs.
19486    let synth = |n: usize, salt: usize| -> Vec<f32> {
19487        (0..n)
19488            .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
19489            .collect()
19490    };
19491    let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
19492        QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19493    };
19494    let layer_weights: Vec<LayerWeights> = (0..num_layers)
19495        .map(|li| LayerWeights {
19496            input_norm: vec![1.0; hidden_size],
19497            post_norm: vec![1.0; hidden_size],
19498            attn_out_norm: None,
19499            ffn_out_norm: None,
19500            layer_scale: None,
19501            ffn: FfnKind::Dense(DenseFfn {
19502                gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
19503                up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
19504                down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
19505                act: Act::Silu,
19506                down_t: None,
19507                segs: Vec::new(),
19508            }),
19509            attn: AttnKind::Full {
19510                bias: None,
19511                wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
19512                wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
19513                wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
19514                wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
19515                q_norm: None,
19516                k_norm: None,
19517                output_gate: false,
19518                softplus_gate: None,
19519            },
19520        })
19521        .collect();
19522
19523    Pipeline::new(
19524        Tokenizer::byte_level(),
19525        PipelineWeights {
19526            embed_tokens: qt(vocab_size, hidden_size, 100),
19527            layers: layer_weights,
19528            lm_head: qt(vocab_size, hidden_size, 200),
19529            final_norm: vec![1.0; hidden_size],
19530        },
19531        hidden_size,
19532        intermediate_size,
19533        num_heads,
19534        num_kv_heads,
19535        head_dim,
19536        num_layers,
19537        num_layers, // physical_layers = num_layers (non-looped)
19538        false,      // loop_final_norm
19539        vocab_size,
19540        1e-6,
19541        10_000.0,
19542        NormStyle::Qwen,
19543        4096,
19544        SamplerConfig {
19545            seed: Some(42),
19546            ..Default::default()
19547        },
19548    )
19549}
19550
19551/// Batched dense-FFN: gate/up/down via matmat (element-wise the same
19552/// math as b × dense_ffn — the same dot kernels).
19553/// One mask bit, LSB-first per byte — `TaskMask::ffn_active_indices`'s
19554/// convention.
19555#[inline]
19556fn mask_bit(row: &[u8], j: usize) -> bool {
19557    (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
19558}
19559
19560/// Zero the CLOSED neurons' activations in a [rows × inter] panel — the
19561/// masked-inference fast path's whole trick: full fused quant compute,
19562/// then the mask lands on the ACTIVATIONS, which is arithmetically the
19563/// pruned network without touching a quantized weight byte. Whole open
19564/// bytes (0xFF = 8 open neurons) skip in one test.
19565/// `CMF_FFN_MASK_GAIN` — Patent 12 FIG. 4, variance-preserving
19566/// rescaling: truncation removes a share of the layer's output energy,
19567/// so the survivors are scaled up to put the variance back where the
19568/// downstream norm expects it. A scalar here; per layer it is
19569/// `sqrt(total energy / kept energy)`.
19570fn mask_gain() -> f32 {
19571    static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
19572    *G.get_or_init(|| {
19573        std::env::var("CMF_FFN_MASK_GAIN")
19574            .ok()
19575            .and_then(|v| v.parse().ok())
19576            .unwrap_or(1.0)
19577    })
19578}
19579
19580fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
19581    // With CMF_FFN_MEANFILL a closed neuron contributes its average
19582    // instead of nothing — same bytes read, one constant restored.
19583    let fill = meanfill().and_then(|(i, v)| {
19584        let li = crate::gpu::cur_layer();
19585        (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
19586    });
19587    for r in 0..rows {
19588        let base = r * inter;
19589        for (bi, &byte) in row.iter().enumerate() {
19590            if byte == 0xFF {
19591                continue;
19592            }
19593            let j0 = bi * 8;
19594            for bit in 0..8 {
19595                let j = j0 + bit;
19596                if j < inter && byte & (1 << bit) == 0 {
19597                    g[base + j] = fill.map_or(0.0, |f| f[j]);
19598                }
19599            }
19600        }
19601    }
19602    let gain = mask_gain();
19603    if gain != 1.0 {
19604        for v in g[..rows * inter].iter_mut() {
19605            *v *= gain;
19606        }
19607    }
19608}
19609
19610/// True when neuron `i`'s bit is set (no mask = everything runs).
19611#[inline]
19612fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
19613    row.is_none_or(|r| mask_bit(r, i))
19614}
19615
19616/// Every bit below `n` set — the common case for a tube file's CORE,
19617/// where only the tube bits vary per task.
19618fn all_bits_on(row: &[u8], n: usize) -> bool {
19619    (0..n).all(|i| mask_bit(row, i))
19620}
19621
19622/// `CMF_TUBE_TOPK` — how many tubes a TOKEN may open (0 = the task mask
19623/// decides alone). This is the dense FFN read as a mixture: the tubes
19624/// are the experts a k-means over `gate_proj` rows found, and the token
19625/// picks among them. `CMF_TUBE_SCORE=gate` scores a tube by its own
19626/// gate (realizable: only `up`/`down` of the losers go unread),
19627/// `=oracle` scores by the true `silu(gate)·up` mass (the ceiling —
19628/// only `down` is saved, and the selection has read what it predicts).
19629fn tube_topk() -> usize {
19630    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19631    *K.get_or_init(|| {
19632        std::env::var("CMF_TUBE_TOPK")
19633            .ok()
19634            .and_then(|v| v.parse().ok())
19635            .unwrap_or(0)
19636    })
19637}
19638
19639fn tube_score_oracle() -> bool {
19640    static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19641    *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
19642}
19643
19644/// The routed arm of `tube_ffn`: a token opens only its best `k` tubes.
19645/// At `b == 1` (decode) the losers are genuinely never read — that is
19646/// the speed. At `b > 1` (the scoring sweep) every tube is computed and
19647/// the losers' activations are zeroed instead: same arithmetic, so the
19648/// perplexity is the routed model's, measured without a per-token
19649/// gather in the middle of a GEMM.
19650fn tube_ffn_routed(
19651    d: &DenseFfn,
19652    xs: &[f32],
19653    b: usize,
19654    pool: Option<&Pool>,
19655    mask_row: Option<&[u8]>,
19656    k: usize,
19657) -> Vec<f32> {
19658    let hidden = d.down_proj.rows();
19659    let core = d.gate_proj.rows();
19660    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19661    let mut out = match (b, core_full, mask_row) {
19662        (1, true, _) => dense_ffn(d, xs, pool),
19663        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19664        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19665        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19666    };
19667    let cand: Vec<usize> = (0..d.segs.len())
19668        .filter(|&i| tube_bit(mask_row, d.segs[i].start))
19669        .collect();
19670    if cand.is_empty() {
19671        return out;
19672    }
19673    // gate (and, where the score or the batch needs it, up) per tube.
19674    // The SCORE is taken at the point the serving path could take it:
19675    // off the gate alone, or off the finished activation for the oracle.
19676    let oracle = tube_score_oracle();
19677    let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
19678    let mut scores = vec![0f32; b * cand.len()];
19679    for (ci, &i) in cand.iter().enumerate() {
19680        let seg = &d.segs[i];
19681        let w = seg.width;
19682        let mut g = vec![0.0f32; b * w];
19683        if b == 1 {
19684            seg.gate.matvec(xs, &mut g, pool);
19685        } else {
19686            seg.gate.matmat(xs, b, &mut g, pool);
19687        }
19688        for v in g.iter_mut() {
19689            *v = Act::Silu.combine(*v, 1.0);
19690        }
19691        if !oracle {
19692            for t in 0..b {
19693                scores[t * cand.len() + ci] =
19694                    g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19695            }
19696        }
19697        if oracle || b > 1 {
19698            let mut u = vec![0.0f32; b * w];
19699            if b == 1 {
19700                seg.up.matvec(xs, &mut u, pool);
19701            } else {
19702                seg.up.matmat(xs, b, &mut u, pool);
19703            }
19704            for (a, &v) in g.iter_mut().zip(u.iter()) {
19705                *a *= v;
19706            }
19707            if oracle {
19708                for t in 0..b {
19709                    scores[t * cand.len() + ci] =
19710                        g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19711                }
19712            }
19713        }
19714        acts.push(g);
19715    }
19716    // per-token scores and the winners
19717    let keep = k.min(cand.len());
19718    let mut scratch: Vec<f32> = Vec::new();
19719    for t in 0..b {
19720        let mut sc: Vec<(f32, usize)> = (0..cand.len())
19721            .map(|ci| (scores[t * cand.len() + ci], ci))
19722            .collect();
19723        sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
19724        let mut alive = vec![false; cand.len()];
19725        for &(_, ci) in sc.iter().take(keep) {
19726            alive[ci] = true;
19727        }
19728        if b > 1 {
19729            for (ci, a) in acts.iter_mut().enumerate() {
19730                if !alive[ci] {
19731                    let w = d.segs[cand[ci]].width;
19732                    a[t * w..(t + 1) * w].fill(0.0);
19733                }
19734            }
19735        } else {
19736            // decode: finish only the winners — the losers' up/down
19737            // (and, with the gate score, everything but their gate)
19738            // are never touched.
19739            for (ci, &i) in cand.iter().enumerate() {
19740                if !alive[ci] {
19741                    continue;
19742                }
19743                let seg = &d.segs[i];
19744                let w = seg.width;
19745                let g = &mut acts[ci];
19746                if !tube_score_oracle() {
19747                    scratch.clear();
19748                    scratch.resize(w, 0.0);
19749                    seg.up.matvec(xs, &mut scratch, pool);
19750                    for (a, &v) in g.iter_mut().zip(scratch.iter()) {
19751                        *a *= v;
19752                    }
19753                }
19754                let mut acc = vec![0.0f32; hidden];
19755                seg.down.matvec(g, &mut acc, pool);
19756                for (o, a) in out.iter_mut().zip(&acc) {
19757                    *o += *a;
19758                }
19759            }
19760        }
19761    }
19762    if b > 1 {
19763        for (ci, &i) in cand.iter().enumerate() {
19764            let seg = &d.segs[i];
19765            let mut acc = vec![0.0f32; b * hidden];
19766            seg.down.matmat(&acts[ci], b, &mut acc, pool);
19767            for (o, a) in out.iter_mut().zip(&acc) {
19768                *o += *a;
19769            }
19770        }
19771    }
19772    out
19773}
19774
19775/// FFN of a defragged tube layer: the always-on core plus the tubes the
19776/// task mask switches on. Each tube is a normal tensor triple, so the
19777/// same kernels run it and an inactive tube's bytes are never read —
19778/// that is the whole point of the defrag (a scattered mask cannot skip
19779/// bytes; a contiguous one is just a smaller matrix).
19780fn tube_ffn(
19781    d: &DenseFfn,
19782    xs: &[f32],
19783    b: usize,
19784    pool: Option<&Pool>,
19785    mask_row: Option<&[u8]>,
19786) -> Vec<f32> {
19787    if tube_topk() > 0 {
19788        return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
19789    }
19790    let hidden = d.down_proj.rows();
19791    let core = d.gate_proj.rows();
19792    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19793    let mut out = match (b, core_full, mask_row) {
19794        (1, true, _) => dense_ffn(d, xs, pool),
19795        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19796        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19797        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19798    };
19799    TUBE_SCRATCH.with(|sc| {
19800        let mut sc = sc.borrow_mut();
19801        let [g, u, acc] = &mut *sc;
19802        for seg in &d.segs {
19803            if !tube_bit(mask_row, seg.start) {
19804                continue;
19805            }
19806            let w = seg.width;
19807            g.resize(b * w, 0.0);
19808            if b == 1
19809                && d.act == Act::Silu
19810                && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
19811            {
19812                // g holds silu(gate)·up.
19813            } else {
19814                u.resize(b * w, 0.0);
19815                if b == 1 {
19816                    QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
19817                } else {
19818                    seg.gate.matmat(xs, b, g, pool);
19819                    seg.up.matmat(xs, b, u, pool);
19820                }
19821                for i in 0..b * w {
19822                    g[i] = d.act.combine(g[i], u[i]);
19823                }
19824            }
19825            acc.resize(b * hidden, 0.0);
19826            acc.fill(0.0);
19827            if b == 1 {
19828                seg.down.matvec(g, acc, pool);
19829            } else {
19830                seg.down.matmat(g, b, acc, pool);
19831            }
19832            for (o, a) in out.iter_mut().zip(acc.iter()) {
19833                *o += *a;
19834            }
19835        }
19836        out
19837    })
19838}
19839
19840thread_local! {
19841    /// gate / up / down-accumulator scratch for the tube loop — a tube
19842    /// runs once per layer per token, and a fresh Vec each time is a
19843    /// malloc per tube per layer per token.
19844    static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
19845        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
19846}
19847
19848/// CMF_PREFILL_PROF: cumulative ns of the batched walk's attention and
19849/// FFN halves (all layers, all chunks).
19850static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
19851    std::sync::atomic::AtomicU64::new(0),
19852    std::sync::atomic::AtomicU64::new(0),
19853];
19854
19855/// The wgpu side of `CMF_PREFILL_PROF`: q|k|v GEMMs' enqueue and readback
19856/// wait, the chunk attend's readback wait (ms, cumulative) and the int8
19857/// GEMMs the `zi_mm` route took.
19858fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
19859    #[cfg(feature = "gpu")]
19860    {
19861        use std::sync::atomic::Ordering::Relaxed;
19862        let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
19863        (
19864            ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
19865            ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
19866            ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
19867            crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
19868        )
19869    }
19870    #[cfg(not(feature = "gpu"))]
19871    (0.0, 0.0, 0.0, 0)
19872}
19873
19874/// The wgpu prefill mirror attend's outcome counters
19875/// (`gpu_wgpu::MIRROR_EVENTS`).
19876fn wgpu_mirror_counters() -> [u64; 6] {
19877    #[cfg(feature = "gpu")]
19878    {
19879        crate::gpu_wgpu::MIRROR_EVENTS
19880            .each_ref()
19881            .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
19882    }
19883    #[cfg(not(feature = "gpu"))]
19884    [0; 6]
19885}
19886
19887fn prefill_prof_on() -> bool {
19888    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19889    *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
19890}
19891
19892/// Accumulate the dense FFN activations which feed the down projection.
19893///
19894/// MTP calibration intentionally calls this independently of the trunk's
19895/// `FFN_PROBE` TLS recorder: an MTP sidecar can use a different FFN width,
19896/// and borrowing the trunk's layer-numbered accumulator would silently turn
19897/// draft statistics into trunk proxies.  The caller owns the execution scope
19898/// (the public MTP probe uses `gpu::cpu_scope`) and supplies one accumulator
19899/// per actual MTP FFN channel.
19900pub(crate) fn accumulate_dense_ffn_activation_mass(
19901    d: &DenseFfn,
19902    xs: &[f32],
19903    b: usize,
19904    pool: Option<&Pool>,
19905    mass: &mut [f64],
19906    squared_mass: &mut [f64],
19907) -> Result<(), String> {
19908    let inter = d.gate_proj.rows();
19909    let hidden = d.gate_proj.cols();
19910    if mass.len() != inter || squared_mass.len() != inter {
19911        return Err(format!(
19912            "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
19913            mass.len(),
19914            squared_mass.len()
19915        ));
19916    }
19917    if d.up_proj.rows() != inter
19918        || d.up_proj.cols() != hidden
19919        || d.down_proj.cols() != inter
19920        || d.down_proj.rows() != hidden
19921    {
19922        return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
19923    }
19924    if !d.segs.is_empty() {
19925        return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
19926    }
19927    if xs.len() != b.saturating_mul(hidden) {
19928        return Err(format!(
19929            "MTP FFN probe: {} input values for {b} × {hidden} rows",
19930            xs.len()
19931        ));
19932    }
19933    if b == 0 {
19934        return Ok(());
19935    }
19936    let mut gate = vec![0.0f32; b * inter];
19937    let mut up = vec![0.0f32; b * inter];
19938    d.gate_proj.matmat(xs, b, &mut gate, pool);
19939    d.up_proj.matmat(xs, b, &mut up, pool);
19940    for row in 0..b {
19941        for i in 0..inter {
19942            let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
19943            let a = a as f64;
19944            mass[i] += a.abs();
19945            squared_mass[i] += a * a;
19946        }
19947    }
19948    Ok(())
19949}
19950
19951fn dense_ffn_batch(
19952    d: &DenseFfn,
19953    xs: &[f32],
19954    b: usize,
19955    pool: Option<&Pool>,
19956    mask_row: Option<&[u8]>,
19957) -> Vec<f32> {
19958    let inter = d.gate_proj.rows();
19959    let hidden = d.down_proj.rows();
19960    // Fused on-device SwiGLU when the device is in play: three separate
19961    // `matmat` calls are three round trips per layer, and the gate/up
19962    // panels (b × inter — 22 MB each at a 512-token chunk) cross the bus
19963    // twice for nothing. The kernel already existed for the image DiT;
19964    // the LLM prefill was simply never wired to it. A task mask needs the
19965    // activations on the host between the halves, so it keeps the CPU
19966    // arm below.
19967    // SiLU/GELU have device arms everywhere they are admitted.  MiniMax
19968    // M3's frozen OAI contract has an additional WGPU-only arm; callers on
19969    // native Metal still get an honest decline from `q4_ffn_act` rather than
19970    // an accidental SiLU evaluation.
19971    let fused_act = d.act.wgpu_graph_act();
19972    if mask_row.is_none()
19973        && fused_act.is_some()
19974        && b >= 32
19975        && crate::gpu::enabled_here()
19976        && !crate::gpu::mm_killed()
19977        // The refit pass needs this layer's activations on the host; the
19978        // fused chain keeps them on the device. Refusing it here costs
19979        // one round trip and keeps every GEMM on the card — the
19980        // alternative was running the whole calibration on the CPU.
19981        && refit_dir().is_none()
19982        // Same for the mass/hit probes. The accumulator at the bottom of
19983        // this function only sees `g` when `g` came back to the host, so
19984        // a fused batch would leave it summing nothing — a probe that
19985        // reports zeros rather than failing, which is worse.
19986        && !ffn_probe_active()
19987        // GPTQ calibration folds every projection's input on the host
19988        // (`QTensor::matmat`); the fused chain would hand gate/up/down
19989        // their inputs on the card and leave the FFN uncalibrated.
19990        && !crate::gptq_capture::capturing()
19991    {
19992        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
19993            d.gate_proj.mapped_q4t(),
19994            d.up_proj.mapped_q4t(),
19995            d.down_proj.mapped_q4t(),
19996        ) {
19997            let mut out = vec![0.0f32; b * hidden];
19998            let act = fused_act.expect("checked above");
19999            if crate::gpu::q4_ffn_act(
20000                model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
20001            ) {
20002                return out;
20003            }
20004        }
20005        // The q4tp twin (same kernel family, scale from the row ladder) —
20006        // the DiT has run it in production since the pipeline containers;
20007        // the LLM prefill was simply never wired to it, so a q4tp model's
20008        // prefill panels stayed on the CPU.
20009        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
20010            d.gate_proj.mapped_q4tp(),
20011            d.up_proj.mapped_q4tp(),
20012            d.down_proj.mapped_q4tp(),
20013        ) {
20014            let mut out = vec![0.0f32; b * hidden];
20015            let act = fused_act.expect("checked above");
20016            if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
20017            {
20018                return out;
20019            }
20020        }
20021        // Every other device codec — int8, or gate/up and down in different
20022        // codecs: the three GEMMs with the panels kept on the card. Without
20023        // it a q8_2f prefill read both b·inter panels home, folded them on
20024        // one host thread, and sent the result back for down.
20025        // CMF_FFN_KEEP=0 keeps the per-GEMM path (A/B).
20026        if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
20027            std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
20028            d.gate_proj.mapped_device_gemm(),
20029            d.up_proj.mapped_device_gemm(),
20030            d.down_proj.mapped_device_gemm(),
20031        ) {
20032            let mut out = vec![0.0f32; b * hidden];
20033            let act = fused_act.expect("checked above");
20034            if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
20035                return out;
20036            }
20037        }
20038    }
20039    let mut g = vec![0.0f32; b * inter];
20040    d.gate_proj.matmat(xs, b, &mut g, pool);
20041    let mut u = vec![0.0f32; b * inter];
20042    d.up_proj.matmat(xs, b, &mut u, pool);
20043    if gate_topk() > 0 && d.act == Act::Silu {
20044        for t in 0..b {
20045            let row = &mut g[t * inter..(t + 1) * inter];
20046            for v in row.iter_mut() {
20047                *v = Act::Silu.combine(*v, 1.0);
20048            }
20049            keep_top_k(row, gate_topk());
20050        }
20051        for i in 0..b * inter {
20052            g[i] *= u[i];
20053        }
20054    } else {
20055        for i in 0..b * inter {
20056            g[i] = d.act.combine(g[i], u[i]);
20057        }
20058    }
20059    if let Some(row) = mask_row {
20060        zero_masked_cols(&mut g, b, inter, row);
20061    }
20062    if oracle_topk() > 0 {
20063        for t in 0..b {
20064            keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
20065        }
20066    }
20067    let mut out = vec![0.0f32; b * hidden];
20068    d.down_proj.matmat(&g, b, &mut out, pool);
20069    if refit_dir().is_some() {
20070        let li = crate::gpu::cur_layer();
20071        if li >= 0 {
20072            refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
20073        }
20074    }
20075    // The DTG-MA probe, on the batched path: one prefill sweep gives the
20076    // same per-neuron statistic the per-position probe does, and on a 27B
20077    // that is minutes instead of hours.
20078    FFN_PROBE.with(|pr| {
20079        if let Some(acc) = pr.borrow_mut().as_mut() {
20080            let li = crate::gpu::cur_layer();
20081            if li < 0 {
20082                return;
20083            }
20084            let Some(row) = acc.get_mut(li as usize) else {
20085                return;
20086            };
20087            let sq = probe_sq();
20088            for t in 0..b {
20089                for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
20090                    *a += if sq {
20091                        (v as f64) * (v as f64)
20092                    } else {
20093                        (v as f64).abs()
20094                    };
20095                }
20096            }
20097        }
20098    });
20099    out
20100}
20101
20102/// Batched MoE-FFN: router batched, positions are GROUPED by expert —
20103/// an expert's weights are read once for all its positions in the chunk
20104/// (the main prefill-GEMM win on MoE: 960MB/token of 35B experts).
20105/// Accumulate per-channel activation energy for `CMF_RMS_TRACE`.
20106fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
20107    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20108    static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20109    let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
20110    let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
20111    if (!on && !dump) || b == 0 {
20112        return;
20113    }
20114    let hidden = xs.len() / b;
20115    if on {
20116        let mut acc = m.act_sq.borrow_mut();
20117        if acc.len() < hidden {
20118            acc.resize(hidden, 0.0);
20119        }
20120        for t in 0..b {
20121            let row = &xs[t * hidden..(t + 1) * hidden];
20122            for (a, &v) in acc.iter_mut().zip(row) {
20123                *a += (v as f64) * (v as f64);
20124            }
20125        }
20126    }
20127    if dump {
20128        // Cap the capture: the covariance needs a few thousand rows, and a
20129        // whole prefill of every layer would be gigabytes for no extra rank.
20130        let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
20131            .ok()
20132            .and_then(|v| v.parse().ok())
20133            .unwrap_or(4096);
20134        let mut rows = m.act_rows.borrow_mut();
20135        if rows.len() < cap * hidden {
20136            let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
20137            rows.extend_from_slice(&xs[..take * hidden]);
20138        }
20139    }
20140}
20141
20142/// Send-able cursor over a Vec-of-Vecs: each pool worker writes only its
20143/// own slots (disjoint by construction in the caller).
20144#[derive(Clone, Copy)]
20145struct SendVecs(*mut Vec<f32>);
20146unsafe impl Send for SendVecs {}
20147unsafe impl Sync for SendVecs {}
20148impl SendVecs {
20149    #[inline]
20150    fn at(self, i: usize) -> *mut Vec<f32> {
20151        unsafe { self.0.add(i) }
20152    }
20153}
20154
20155fn moe_ffn_batch(
20156    m: &MoeFfn,
20157    xs: &[f32],
20158    b: usize,
20159    hidden: usize,
20160    pool: Option<&Pool>,
20161    allowed: Option<&[bool]>,
20162) -> Vec<f32> {
20163    accumulate_act(m, xs, b);
20164    let ne = m.experts.len();
20165    let mut logits = vec![0.0f32; b * ne];
20166    match &m.resonance {
20167        Some(r) => {
20168            let hdim = xs.len() / b.max(1);
20169            for bi in 0..b {
20170                r.scores(
20171                    &xs[bi * hdim..(bi + 1) * hdim],
20172                    &mut logits[bi * ne..(bi + 1) * ne],
20173                );
20174            }
20175        }
20176        None => m.router.matmat(xs, b, &mut logits, pool),
20177    }
20178
20179    // Assignments: expert → [(position, weight)] — same routing as
20180    // moe_ffn, per position (see `moe_route`).
20181    let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
20182    {
20183        let mut st = m.stats.borrow_mut();
20184        if st.len() < ne {
20185            st.resize(ne, 0);
20186        }
20187        for bi in 0..b {
20188            let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
20189            for &e in &idx {
20190                st[e] += 1;
20191                assign[e].push((bi, p[e] / wsum));
20192            }
20193        }
20194    }
20195
20196    let mut out = vec![0.0f32; b * hidden];
20197    let cols = m.experts[0].gate_proj.cols();
20198    let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
20199        let sb = list.len();
20200        let mut sub = vec![0.0f32; sb * cols];
20201        for (k, &(bi, _)) in list.iter().enumerate() {
20202            sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
20203        }
20204        let eo = dense_ffn_batch(d, &sub, sb, pool, None);
20205        for (k, &(bi, w)) in list.iter().enumerate() {
20206            for i in 0..hidden {
20207                out[bi * hidden + i] += w * eo[k * hidden + i];
20208            }
20209        }
20210    };
20211    // Routed experts: the panels are TINY (b·top_k spread over every
20212    // expert — a few positions each), so a pool dispatch per expert is
20213    // pure barrier cost. Invert the parallelism: workers take WHOLE
20214    // experts (serial math inside), then one deterministic scatter in
20215    // expert order — the exact accumulation order the serial loop had.
20216    let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
20217    if pool.is_some() && active.len() >= 8 {
20218        let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
20219        {
20220            let panel_ptr = SendVecs(panels.as_mut_ptr());
20221            // Capture only the expert table: `m` itself carries RefCell
20222            // stats and must not cross the pool boundary.
20223            let experts = &m.experts;
20224            let (active_r, assign_r) = (&active, &assign);
20225            let inherit_cpu = crate::gpu::inherit_cpu_scope();
20226            let run = |start: usize, end: usize| {
20227                let _cpu_scope = inherit_cpu();
20228                // Native Metal: the per-op device helpers (`q4tp_ffn`,
20229                // `q8_matmat`, …) stage through size-keyed shared io
20230                // buffers and are not re-entrant — two workers whose experts
20231                // drew the same row count wrote one buffer at once, so a
20232                // Mellum2.1 prompt came out different on every run. The
20233                // workers keep whole experts on the CPU there; the device
20234                // prefill is the MoE chunk graph.
20235                #[cfg(target_os = "macos")]
20236                let _metal_cpu = crate::gpu::enter_cpu_scope();
20237                for ai in start..end {
20238                    let e = active_r[ai];
20239                    let list = &assign_r[e];
20240                    let sb = list.len();
20241                    let mut sub = vec![0.0f32; sb * cols];
20242                    for (k, &(bi, _)) in list.iter().enumerate() {
20243                        sub[k * cols..(k + 1) * cols]
20244                            .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
20245                    }
20246                    // SAFETY: each worker owns a disjoint panels[ai].
20247                    unsafe {
20248                        *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
20249                    }
20250                }
20251            };
20252            match pool {
20253                Some(p) => p.run_rows(active.len(), &run),
20254                None => run(0, active.len()),
20255            }
20256        }
20257        for (ai, &e) in active.iter().enumerate() {
20258            for (k, &(bi, w)) in assign[e].iter().enumerate() {
20259                let eo = &panels[ai][k * hidden..(k + 1) * hidden];
20260                for i in 0..hidden {
20261                    out[bi * hidden + i] += w * eo[i];
20262                }
20263            }
20264        }
20265    } else {
20266        for &e in &active {
20267            run_expert(&m.experts[e], &assign[e], &mut out);
20268        }
20269    }
20270    if let Some((se, gate)) = &m.shared {
20271        let all: Vec<(usize, f32)> = if let Some(gate) = gate {
20272            let mut gl = vec![0.0f32; b];
20273            gate.matmat(xs, b, &mut gl, pool);
20274            (0..b)
20275                .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
20276                .collect()
20277        } else {
20278            (0..b).map(|bi| (bi, 1.0)).collect()
20279        };
20280        run_expert(se, &all, &mut out);
20281    }
20282    out
20283}
20284
20285/// Decode-exact multi-token MoE — the MiMo speculative verify's FFN. Row
20286/// `r` of the result is bit-identical to `moe_ffn(m, x_r)` on the CPU
20287/// (`moe_ffn_cpu` → `moe_ffn_cpu_batched`): router matvec per row, the same
20288/// routing, the same int8 gate/up/SiLU and down terms
20289/// (`QTensor::moe_gate_up_rows` / `moe_down_rows`), and the row's experts
20290/// summed in ITS route order from 0. What the rows share is the weight
20291/// traffic: each routed expert is read once for every row that picked it.
20292/// (`moe_ffn_batch`, the prompt path, groups the same way but sums in
20293/// expert-index order and runs blocked kernels on wide groups — close, not
20294/// bit-equal to decode.) Any layer the kernels do not cover, or a device
20295/// that could answer `moe_ffn` itself, walks `moe_ffn` row by row.
20296fn moe_ffn_rows_exact(
20297    m: &MoeFfn,
20298    xs: &[f32],
20299    b: usize,
20300    hidden: usize,
20301    pool: Option<&Pool>,
20302) -> Vec<f32> {
20303    let mut out = vec![0.0f32; b * hidden];
20304    let per_row = |out: &mut [f32]| {
20305        for r in 0..b {
20306            let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
20307            out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
20308        }
20309    };
20310    let covered = !crate::gpu::enabled_here()
20311        && moe_batch_enabled()
20312        && m.shared.is_none()
20313        && m.resonance.is_none()
20314        && FFN_PROBE.with(|pr| pr.borrow().is_none())
20315        && m.experts.iter().all(|d| d.act == Act::Silu);
20316    if !covered {
20317        per_row(&mut out);
20318        return out;
20319    }
20320    let ne = m.experts.len();
20321    // Routing, row by row, exactly as `moe_ffn`.
20322    let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
20323    for r in 0..b {
20324        let x = &xs[r * hidden..(r + 1) * hidden];
20325        accumulate_act(m, x, 1);
20326        let mut logits = vec![0.0f32; ne];
20327        m.router.matvec(x, &mut logits, pool);
20328        let (idx, p, wsum) = moe_route(&logits, m, None);
20329        {
20330            let mut st = m.stats.borrow_mut();
20331            if st.len() < ne {
20332                st.resize(ne, 0);
20333            }
20334            for &e in &idx {
20335                st[e] += 1;
20336            }
20337        }
20338        let w: Vec<f32> = idx
20339            .iter()
20340            .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
20341            .collect();
20342        routes.push((idx, w));
20343    }
20344    if routes.iter().any(|(idx, _)| idx.is_empty()) {
20345        per_row(&mut out);
20346        return out;
20347    }
20348    // Group the (row, expert) picks by expert, in first-seen order.
20349    let mut experts: Vec<usize> = Vec::new();
20350    let mut groups: Vec<Vec<usize>> = Vec::new();
20351    for (r, (idx, _)) in routes.iter().enumerate() {
20352        for &e in idx {
20353            match experts.iter().position(|&x| x == e) {
20354                Some(g) => groups[g].push(r),
20355                None => {
20356                    experts.push(e);
20357                    groups.push(vec![r]);
20358                }
20359            }
20360        }
20361    }
20362    let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
20363    let inter = m.experts[experts[0]].gate_proj.rows();
20364    let pairs: Vec<(&QTensor, &QTensor)> = experts
20365        .iter()
20366        .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
20367        .collect();
20368    let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
20369    if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
20370        per_row(&mut out);
20371        return out;
20372    }
20373    let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
20374    let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
20375    let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
20376    if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
20377        per_row(&mut out);
20378        return out;
20379    }
20380    // Where each (row, expert) term landed in the flat pair list.
20381    let mut slot = std::collections::HashMap::with_capacity(n_pairs);
20382    let mut p = 0usize;
20383    for (g, &e) in experts.iter().enumerate() {
20384        for &r in &groups[g] {
20385            slot.insert((r, e), p);
20386            p += 1;
20387        }
20388    }
20389    for (r, (idx, w)) in routes.iter().enumerate() {
20390        let terms: Vec<(&[f32], f32)> = idx
20391            .iter()
20392            .zip(w)
20393            .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
20394            .collect();
20395        let row = &mut out[r * hidden..(r + 1) * hidden];
20396        for (i, dst) in row.iter_mut().enumerate() {
20397            // `moe_down_many`'s per-row sum: from 0, in route order.
20398            let mut acc = 0f32;
20399            for (d, we) in &terms {
20400                acc += we * d[i];
20401            }
20402            *dst = acc;
20403        }
20404    }
20405    out
20406}
20407
20408/// Gemma's final-logit soft-capping, `c·tanh(x/c)`, over a whole vocab row —
20409/// element for element the serial expression, split across the pool: at a
20410/// 262k vocab the serial `tanh` pass was ~5 ms of a 36 ms decode token.
20411pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
20412    match pool {
20413        Some(p) if v.len() >= 1 << 15 => {
20414            let base = v.as_mut_ptr() as usize;
20415            p.run_rows(v.len(), &|s, e| {
20416                // SAFETY: run_rows hands out disjoint [s, e) ranges of `v`,
20417                // which outlives the (blocking) dispatch.
20418                let sl =
20419                    unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
20420                for x in sl {
20421                    *x = c * (*x / c).tanh();
20422                }
20423            });
20424        }
20425        _ => {
20426            for x in v.iter_mut() {
20427                *x = c * (*x / c).tanh();
20428            }
20429        }
20430    }
20431}
20432
20433thread_local! {
20434    /// gate/up activation scratch for the dense FFN paths (single uses
20435    /// two slots, the fused pair all four) — these were fresh
20436    /// intermediate-size Vecs on every layer of every token.
20437    static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
20438        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
20439}
20440
20441/// Dense SwiGLU FFN through QTensor matvecs (any storage).
20442fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20443    // Per-token sparsity, when the file was built for it: gate first,
20444    // then only the chosen neurons' up/down rows leave the mmap.
20445    if gate_topk() > 0
20446        && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
20447    {
20448        return out;
20449    }
20450    // Whole-FFN GPU submit (этап 4.2 increment): gate → silu·up → down
20451    // chained in ONE command buffer with the intermediate activations
20452    // resident on the device — 3 per-op polls become 1 per layer. The
20453    // moe_block backend already implements exactly this chain; a dense
20454    // FFN is one expert with weight 1. Runtime probe: the chain still
20455    // pays one submit+poll per layer — alternate it against the pure-CPU
20456    // FFN and keep whichever is faster on this machine.
20457    // q1 FFNs offload at any practical size: the q1 CPU kernel is
20458    // compute-bound, so the UMA threshold logic does not apply — the
20459    // probe measures and decides either way.
20460    // The fused GPU block has no descriptor-aware Prism path: it would either
20461    // consume an unrotated activation or decline after inspecting the mixed
20462    // q2tp/q4tp tensors.  Do not let that structural refusal enter the FFN
20463    // probe's CPU_ONLY scope; the ordinary body below dispatches each matrix
20464    // through QTensor::matvec, which owns the signed FWHT + affine q2tp route.
20465    let prism_body = d.gate_proj.has_prism_contract()
20466        || d.up_proj.has_prism_contract()
20467        || d.down_proj.has_prism_contract();
20468    if !prism_body
20469        && crate::gpu::enabled_here()
20470        && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
20471    {
20472        let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
20473            crate::gpu::ProbeArm::Gpu
20474        } else {
20475            crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
20476        };
20477        match arm {
20478            crate::gpu::ProbeArm::Gpu => {
20479                let t0 = std::time::Instant::now();
20480                if let Some(out) = dense_ffn_gpu(d, x, pool) {
20481                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
20482                    return out;
20483                }
20484                // Declined: no timing exists, so say so. Silence here is
20485                // what left `ffn` undecided for 9000 calls and cost a
20486                // failed device attempt on half of them.
20487                crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
20488            }
20489            crate::gpu::ProbeArm::CpuTimed => {
20490                let t0 = std::time::Instant::now();
20491                let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20492                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
20493                return out;
20494            }
20495            crate::gpu::ProbeArm::Cpu => {
20496                return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20497            }
20498        }
20499    }
20500    dense_ffn_cpu(d, x, pool)
20501}
20502
20503/// The pure-CPU dense-FFN body (also the fallback of every GPU refusal).
20504fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20505    let inter = d.gate_proj.rows();
20506    FFN_SCRATCH.with(|s| {
20507        let mut s = s.borrow_mut();
20508        let [g, u, ..] = &mut *s;
20509        g.resize(inter, 0.0);
20510        // Fused gate+up+silu: one dispatch, no separate silu pass.
20511        // Falls back to matvec_many + silu loop for unsupported dtypes.
20512        if gate_topk() > 0 {
20513            // Gate first, select, and only then pay for `up`: the
20514            // measurement arm computes both and zeroes the losers, which
20515            // is the same arithmetic.
20516            u.resize(inter, 0.0);
20517            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20518            for i in 0..inter {
20519                g[i] = Act::Silu.combine(g[i], 1.0);
20520            }
20521            keep_top_k(g, gate_topk());
20522            for i in 0..inter {
20523                g[i] *= u[i];
20524            }
20525        } else if d.act == Act::Silu && {
20526            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20527            QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
20528        } {
20529            // g now holds silu(gate)·up directly.
20530        } else {
20531            u.resize(inter, 0.0);
20532            // Multi-matrix job: gate+up under one pool dispatch.
20533            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20534            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20535            let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
20536            for i in 0..inter {
20537                g[i] = d.act.combine(g[i], u[i]);
20538            }
20539        }
20540        // DTG-MA bake probe (Patent 2): accumulate this layer's
20541        // per-neuron activation mass while a probe pass is active.
20542        // `CMF_FFN_PROBE_TOPK=k` switches the statistic from mass to a
20543        // HIT COUNT — how many tokens rank the neuron in their own top
20544        // k. Mass asks "how loud is this neuron overall", the count
20545        // asks "how often does this task actually need it", and the two
20546        // rank neurons differently whenever a few tokens are loud.
20547        FFN_PROBE.with(|pr| {
20548            if let Some(acc) = pr.borrow_mut().as_mut() {
20549                let li = crate::gpu::cur_layer();
20550                if li >= 0 {
20551                    if let Some(row) = acc.get_mut(li as usize) {
20552                        match probe_topk() {
20553                            0 if probe_sq() => {
20554                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20555                                    *a += (v as f64) * (v as f64);
20556                                }
20557                            }
20558                            0 if probe_signed() => {
20559                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20560                                    *a += v as f64;
20561                                }
20562                            }
20563                            0 => {
20564                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20565                                    *a += (v as f64).abs();
20566                                }
20567                            }
20568                            k => {
20569                                let n = g.len();
20570                                let k = k.min(n);
20571                                let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
20572                                let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
20573                                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20574                                });
20575                                let thr = *kth;
20576                                for (a, &v) in row.iter_mut().zip(g.iter()) {
20577                                    if v.abs() >= thr {
20578                                        *a += 1.0;
20579                                    }
20580                                }
20581                            }
20582                        }
20583                    }
20584                }
20585            }
20586        });
20587        if oracle_topk() > 0 {
20588            keep_top_k(g, oracle_topk());
20589        }
20590        {
20591            let li = crate::gpu::cur_layer();
20592            if li >= 0 {
20593                adump_row(li as usize, g);
20594            }
20595        }
20596        let mut out = attention::take_buf(d.down_proj.rows());
20597        let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
20598        d.down_proj.matvec(g, &mut out, pool);
20599        out
20600    })
20601}
20602
20603/// Online accumulators for the AWNP refit of a narrowed FFN.
20604///
20605/// The refit needs `Gss = A_SᵀA_S` and `YA = YᵀA_S` per layer, where `A_S`
20606/// are the calibration activations of the KEPT neurons and `Y` the full
20607/// FFN output. Both are small enough to hold; the thing that is not is
20608/// the activations they are built from — a 27B layer would dump a
20609/// gigabyte per thousand tokens. So they are accumulated as the
20610/// calibration runs and written once at the end.
20611///
20612/// `CMF_FFN_REFIT=<dir>` holds `support.<L>.u32` (a u32 count then the
20613/// kept indices) for every layer to accumulate; `CMF_FFN_REFIT_FROM/TO`
20614/// bound the layer span so the accumulators fit in RAM.
20615pub struct RefitAcc {
20616    pub support: Vec<u32>,
20617    pub gss: Vec<f32>,
20618    pub ya: Vec<f32>,
20619    pub hidden: usize,
20620    pub tokens: u64,
20621    /// Activations staged transposed ([ns, t] and [hidden, t]) until the
20622    /// batch is worth a GEMM. The product costs `ns²` to move and add
20623    /// REGARDLESS of how many tokens went into it, so folding 16 chunks
20624    /// into one call cuts that cost 16× — it was 15 TB of traffic per
20625    /// calibration pass at one call per 256 tokens.
20626    pub buf_g: Vec<f32>,
20627    pub buf_o: Vec<f32>,
20628    pub buf_t: usize,
20629}
20630
20631/// The product buffer is SHARED across layers — one 473 MB allocation,
20632/// not one per layer (that was 30 GB of nothing on a 64-layer model).
20633/// It lives under the same lock as the accumulators.
20634type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
20635
20636static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
20637    std::sync::OnceLock::new();
20638
20639/// Is an FFN probe accumulator installed on this thread? The fused GPU
20640/// FFN must decline while one is, or the probe silently measures zero.
20641fn ffn_probe_active() -> bool {
20642    FFN_PROBE.with(|p| p.borrow().is_some())
20643}
20644
20645fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
20646    REFIT
20647        .get_or_init(|| {
20648            std::env::var("CMF_FFN_REFIT").ok().map(|d| {
20649                (
20650                    d,
20651                    std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
20652                )
20653            })
20654        })
20655        .as_ref()
20656}
20657
20658/// Accumulate one prefill panel into the layer's refit statistics.
20659fn refit_accumulate(
20660    li: usize,
20661    g: &[f32],
20662    b: usize,
20663    inter: usize,
20664    out: &[f32],
20665    hidden: usize,
20666    pool: Option<&Pool>,
20667) {
20668    let Some((dir, map)) = refit_dir() else {
20669        return;
20670    };
20671    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20672    let (from, to) = *SPAN.get_or_init(|| {
20673        let g = |k: &str, d: usize| {
20674            std::env::var(k)
20675                .ok()
20676                .and_then(|v| v.parse().ok())
20677                .unwrap_or(d)
20678        };
20679        (
20680            g("CMF_FFN_REFIT_FROM", 0),
20681            g("CMF_FFN_REFIT_TO", usize::MAX),
20682        )
20683    });
20684    if li < from || li > to {
20685        return;
20686    }
20687    let mut guard = map.lock().unwrap();
20688    let (map, shared) = &mut *guard;
20689    let acc = match map.entry(li) {
20690        std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
20691        std::collections::hash_map::Entry::Vacant(e) => {
20692            let path = format!("{dir}/support.{li}.u32");
20693            let Ok(bytes) = std::fs::read(&path) else {
20694                eprintln!("refit: no {path} — layer {li} skipped");
20695                return;
20696            };
20697            let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
20698            let support: Vec<u32> = bytes[4..4 + n * 4]
20699                .chunks_exact(4)
20700                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
20701                .collect();
20702            eprintln!(
20703                "refit: layer {li} support {n} ({:.0} MB of accumulator)",
20704                (n * n + hidden * n) as f64 * 4.0 / 1e6
20705            );
20706            e.insert(RefitAcc {
20707                gss: vec![0.0; n * n],
20708                ya: vec![0.0; hidden * n],
20709                buf_g: Vec::new(),
20710                buf_o: Vec::new(),
20711                buf_t: 0,
20712                support,
20713                hidden,
20714                tokens: 0,
20715            })
20716        }
20717    };
20718    let ns = acc.support.len();
20719    // Stage this chunk transposed; the GEMM fires once the batch is full.
20720    let cap = refit_batch();
20721    if acc.buf_g.is_empty() {
20722        acc.buf_g = vec![0.0; ns * cap];
20723        acc.buf_o = vec![0.0; hidden * cap];
20724    }
20725    let take = b.min(cap - acc.buf_t);
20726    for t in 0..take {
20727        let col = acc.buf_t + t;
20728        for (j, &n) in acc.support.iter().enumerate() {
20729            acc.buf_g[j * cap + col] = g[t * inter + n as usize];
20730        }
20731        for h in 0..hidden {
20732            acc.buf_o[h * cap + col] = out[t * hidden + h];
20733        }
20734    }
20735    acc.buf_t += take;
20736    acc.tokens += take as u64;
20737    if acc.buf_t < cap {
20738        return;
20739    }
20740    let bt = acc.buf_t;
20741    acc.buf_t = 0;
20742    // The GEMM WRITES its C (it zeroes the accumulators it uses), so the
20743    // chunk product lands in scratch and is added on — the one thing that
20744    // silently turns a Gram over 13 000 tokens into a Gram over 256.
20745    // Both products are `C[n, m] += X[n, b] · Yᵀ[b, m]` with X and Y
20746    // stored row-major [·, b] — exactly `gemm_nt_f32`'s shape, so the
20747    // card does them when it is up (this is the whole calibration's
20748    // cost: O(|S|²) per token, 2.9 PFLOP for a 27B pass). The tiled CPU
20749    // loop stays as the fallback. Neither accumulates, so the product
20750    // lands in scratch and is added on.
20751    let RefitAcc {
20752        gss,
20753        ya,
20754        buf_g,
20755        buf_o,
20756        ..
20757    } = acc;
20758    let need = (ns * ns).max(hidden * ns);
20759    if shared.len() < need {
20760        shared.resize(need, 0.0);
20761    }
20762    let scratch = &mut shared[..];
20763    let _ = bt;
20764    if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
20765        add_into(gss, &scratch[..ns * ns], pool);
20766        if crate::gpu::gemm_nt_f32_transient(
20767            buf_o,
20768            buf_g,
20769            &mut scratch[..hidden * ns],
20770            hidden,
20771            cap,
20772            ns,
20773        ) {
20774            add_into(ya, &scratch[..hidden * ns], pool);
20775        } else {
20776            accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20777        }
20778    } else {
20779        accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
20780        accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20781    }
20782    // No zeroing: the batch is always filled exactly (cap is a multiple
20783    // of the prefill chunk), and a memset of 178 MB a layer would cost
20784    // more than the GEMM.
20785}
20786
20787/// `CMF_FFN_REFIT_BATCH` — tokens staged before each GEMM (default 4096).
20788fn refit_batch() -> usize {
20789    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20790    *B.get_or_init(|| {
20791        std::env::var("CMF_FFN_REFIT_BATCH")
20792            .ok()
20793            .and_then(|v| v.parse().ok())
20794            .unwrap_or(4096)
20795    })
20796}
20797
20798/// `c[m, n] += Σ_t left[m, t]·right[n, t]` — both operands transposed,
20799/// the CPU fallback for the staged batch.
20800fn accum_outer_t(
20801    c: &mut [f32],
20802    m: usize,
20803    n: usize,
20804    b: usize,
20805    left: &[f32],
20806    right: &[f32],
20807    pool: Option<&Pool>,
20808) {
20809    let ptr = SendMut(c.as_mut_ptr());
20810    let body = |i: usize| {
20811        let ptr = &ptr;
20812        let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
20813        for t in 0..b {
20814            let a = left[i * b + t];
20815            if a == 0.0 {
20816                continue;
20817            }
20818            for (j, o) in row.iter_mut().enumerate() {
20819                *o += a * right[j * b + t];
20820            }
20821        }
20822    };
20823    match pool {
20824        Some(p) if m > 1 => p.run_rows(m, &|s, e| {
20825            for i in s..e {
20826                body(i);
20827            }
20828        }),
20829        _ => {
20830            for i in 0..m {
20831                body(i);
20832            }
20833        }
20834    }
20835}
20836
20837/// `dst += src`, spread over the pool — at 118 M floats a layer this is
20838/// not a loop to leave on one core.
20839fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
20840    let n = dst.len().min(src.len());
20841    match pool {
20842        Some(p) if n >= 1 << 16 => {
20843            let ptr = SendMut(dst.as_mut_ptr());
20844            let f = |s: usize, e: usize| {
20845                let ptr = &ptr;
20846                for blk in s..e {
20847                    let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
20848                    for i in a..b {
20849                        unsafe { *ptr.0.add(i) += src[i] };
20850                    }
20851                }
20852            };
20853            p.run_rows(n.div_ceil(4096), &f);
20854        }
20855        _ => {
20856            for (d, v) in dst.iter_mut().zip(&src[..n]) {
20857                *d += *v;
20858            }
20859        }
20860    }
20861}
20862
20863/// `c[m, n] += Σ_t left[t, m]·right[t, n]`, with `left` stored [m, t] and
20864/// `right` [t, n]. Tiled over the rows of `c` so a tile stays in cache
20865/// while each token's `right` row streams past it once, and parallel
20866/// over tiles.
20867fn accum_outer(
20868    c: &mut [f32],
20869    m: usize,
20870    n: usize,
20871    b: usize,
20872    left: &[f32],
20873    right: &[f32],
20874    pool: Option<&Pool>,
20875) {
20876    const TILE: usize = 32;
20877    let tiles = m.div_ceil(TILE);
20878    let cp = SendMut(c.as_mut_ptr());
20879    let body = |ti: usize| {
20880        let cp = &cp;
20881        let i0 = ti * TILE;
20882        let i1 = (i0 + TILE).min(m);
20883        for t in 0..b {
20884            let r = &right[t * n..t * n + n];
20885            for i in i0..i1 {
20886                let a = left[i * b + t];
20887                if a == 0.0 {
20888                    continue;
20889                }
20890                // SAFETY: tiles partition c's rows; workers never overlap.
20891                let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
20892                for (o, v) in row.iter_mut().zip(r) {
20893                    *o += a * *v;
20894                }
20895            }
20896        }
20897    };
20898    match pool {
20899        Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
20900            for ti in s..e {
20901                body(ti);
20902            }
20903        }),
20904        _ => {
20905            for ti in 0..tiles {
20906                body(ti);
20907            }
20908        }
20909    }
20910}
20911
20912/// Write what the calibration accumulated: `gss.<L>.f32` and `ya.<L>.f32`.
20913pub fn refit_flush() -> usize {
20914    let Some((dir, map)) = refit_dir() else {
20915        return 0;
20916    };
20917    let guard = map.lock().unwrap();
20918    let mut n = 0;
20919    for (li, acc) in guard.0.iter() {
20920        // A silently truncated write here is a Gram that reshapes to
20921        // nothing an hour later — say it out loud instead.
20922        let w = |name: &str, v: &[f32]| {
20923            let path = format!("{dir}/{name}.{li}.f32");
20924            let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
20925            match std::fs::write(&path, &bytes) {
20926                Ok(()) => {}
20927                Err(e) => eprintln!(
20928                    "refit: FAILED to write {path} ({} MB): {e}",
20929                    bytes.len() / 1_000_000
20930                ),
20931            }
20932        };
20933        w("gss", &acc.gss);
20934        w("ya", &acc.ya);
20935        println!(
20936            "refit L{li}: {} support, {} tokens, hidden {}",
20937            acc.support.len(),
20938            acc.tokens,
20939            acc.hidden
20940        );
20941        n += 1;
20942    }
20943    n
20944}
20945
20946/// `CMF_FFN_ADUMP=<prefix>` — append every probed token's FFN activation
20947/// row to `<prefix>.<layer>.f16`. The co-activation record: which
20948/// neurons fire together, which is what a tube has to group if a token
20949/// is ever going to open one tube instead of sixteen.
20950fn adump_row(li: usize, g: &[f32]) {
20951    use std::io::Write as _;
20952    static FILES: std::sync::OnceLock<
20953        Option<(
20954            String,
20955            std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
20956        )>,
20957    > = std::sync::OnceLock::new();
20958    let Some((prefix, map)) = FILES
20959        .get_or_init(|| {
20960            std::env::var("CMF_FFN_ADUMP")
20961                .ok()
20962                .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
20963        })
20964        .as_ref()
20965    else {
20966        return;
20967    };
20968    // `CMF_FFN_ADUMP_FROM/_TO` narrow the dump to a layer span, so a big
20969    // calibration run fits on disk in a few passes instead of one.
20970    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20971    let (from, to) = *SPAN.get_or_init(|| {
20972        let g = |k: &str, d: usize| {
20973            std::env::var(k)
20974                .ok()
20975                .and_then(|v| v.parse().ok())
20976                .unwrap_or(d)
20977        };
20978        (
20979            g("CMF_FFN_ADUMP_FROM", 0),
20980            g("CMF_FFN_ADUMP_TO", usize::MAX),
20981        )
20982    });
20983    if li < from || li > to {
20984        return;
20985    }
20986    let mut map = map.lock().unwrap();
20987    let f = map.entry(li).or_insert_with(|| {
20988        std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
20989    });
20990    let mut bytes = Vec::with_capacity(g.len() * 2);
20991    for v in g {
20992        bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
20993    }
20994    let _ = f.write_all(&bytes);
20995}
20996
20997/// `CMF_FFN_ORACLE_TOPK` — keep only the k largest |silu(g)·u| of each
20998/// token and zero the rest. Not a serving mode: it is the CEILING of
20999/// contextual sparsity — what a per-token router would be chasing —
21000/// measured by cheating, since the selection reads the very activations
21001/// it would have to predict.
21002fn oracle_topk() -> usize {
21003    if exact_mtp_calibration_active() {
21004        return 0;
21005    }
21006    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21007    *K.get_or_init(|| {
21008        std::env::var("CMF_FFN_ORACLE_TOPK")
21009            .ok()
21010            .and_then(|v| v.parse().ok())
21011            .unwrap_or(0)
21012    })
21013}
21014
21015/// `CMF_FFN_GATE_TOPK` — the REALIZABLE cousin of the oracle: rank the
21016/// neurons by their gate alone (which the kernel has computed anyway
21017/// before it reads `up`), keep the k best, and drop the rest. Every
21018/// dropped neuron's `up` row and `down` column stay unread, so this is
21019/// the sparsity a serving path can actually take without a router.
21020fn gate_topk() -> usize {
21021    if exact_mtp_calibration_active() {
21022        return 0;
21023    }
21024    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21025    *K.get_or_init(|| {
21026        std::env::var("CMF_FFN_GATE_TOPK")
21027            .ok()
21028            .and_then(|v| v.parse().ok())
21029            .unwrap_or(0)
21030    })
21031}
21032
21033/// `CMF_FFN_GATE_BLOCK` — select in blocks of B neurons instead of one
21034/// by one. A scattered per-neuron choice cannot be read efficiently (a
21035/// row at a time, no prefetch runway); a block of 32 is a contiguous
21036/// 32-row slab of `up` and of the transposed `down`, which the ordinary
21037/// kernels stream. The question the measurement answers is what the
21038/// block costs in quality.
21039fn gate_block() -> usize {
21040    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21041    *B.get_or_init(|| {
21042        std::env::var("CMF_FFN_GATE_BLOCK")
21043            .ok()
21044            .and_then(|v| v.parse().ok())
21045            .unwrap_or(1)
21046    })
21047}
21048
21049/// Zero all but the `k` largest BLOCKS (by summed square) of a row.
21050fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
21051    let n = g.len();
21052    let nb = n.div_ceil(block);
21053    let kb = (keep_n.div_ceil(block)).clamp(1, nb);
21054    if kb >= nb {
21055        return;
21056    }
21057    let mut score: Vec<f32> = (0..nb)
21058        .map(|b| {
21059            g[b * block..((b + 1) * block).min(n)]
21060                .iter()
21061                .map(|v| v * v)
21062                .sum::<f32>()
21063        })
21064        .collect();
21065    let mut ord = score.clone();
21066    let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
21067        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21068    });
21069    let thr = *kth;
21070    for b in 0..nb {
21071        if score[b] < thr {
21072            g[b * block..((b + 1) * block).min(n)].fill(0.0);
21073        }
21074    }
21075    score.clear();
21076}
21077
21078/// Zero all but the `k` largest magnitudes of one token's activation row.
21079fn keep_top_k(g: &mut [f32], k: usize) {
21080    if gate_block() > 1 {
21081        return keep_top_blocks(g, k, gate_block());
21082    }
21083    let n = g.len();
21084    if k == 0 || k >= n {
21085        return;
21086    }
21087    let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
21088    let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
21089        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21090    });
21091    let thr = *kth;
21092    for v in g.iter_mut() {
21093        if v.abs() < thr {
21094            *v = 0.0;
21095        }
21096    }
21097}
21098
21099/// `CMF_FFN_PROBE_SQ` — accumulate Σa², so the dump divided by the token
21100/// count and square-rooted is the RMS activation trace Patent 12 weights
21101/// its matrices by.
21102fn probe_sq() -> bool {
21103    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21104    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
21105}
21106
21107/// `CMF_FFN_PROBE_SIGNED` — accumulate the SIGNED activation sum
21108/// instead of its magnitude: what a dropped neuron contributes ON
21109/// AVERAGE, which is the bias a narrowed FFN can add back for free.
21110fn probe_signed() -> bool {
21111    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21112    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
21113}
21114
21115/// `CMF_FFN_MEANFILL=<file>` — a masked-out neuron contributes its MEAN
21116/// activation instead of zero (`u32 layers, u32 inter, f32[…]`, the mass
21117/// dump layout, holding per-neuron means). Dropping a neuron outright
21118/// also drops its average contribution, which shifts the layer output by
21119/// a constant; filling the mean back is one add per layer and costs no
21120/// bytes off the bus. This is the measurement arm — in a tube file the
21121/// same correction ships as a per-task bias vector.
21122fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
21123    static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
21124    M.get_or_init(|| {
21125        let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
21126        let b = std::fs::read(&p).ok()?;
21127        let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
21128        let vals: Vec<f32> = b[8..]
21129            .chunks_exact(4)
21130            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
21131            .collect();
21132        eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
21133        Some((inter, vals))
21134    })
21135    .as_ref()
21136}
21137
21138/// `CMF_FFN_PROBE_TOPK` — 0 (default) = accumulate mass, k>0 = count
21139/// how often a neuron lands in a token's top k.
21140fn probe_topk() -> usize {
21141    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21142    *K.get_or_init(|| {
21143        std::env::var("CMF_FFN_PROBE_TOPK")
21144            .ok()
21145            .and_then(|v| v.parse().ok())
21146            .unwrap_or(0)
21147    })
21148}
21149
21150thread_local! {
21151    /// DTG-MA activation probe: per-layer per-neuron Σ|silu(g)·u|
21152    /// accumulator, alive only during `Pipeline::probe_ffn_mass`.
21153    static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
21154        const { std::cell::RefCell::new(None) };
21155}
21156
21157/// Per-token structured sparsity, paid for in bytes.
21158///
21159/// The gate is the cheapest third of an FFN and it already says which
21160/// neurons matter: `silu(gate)` near zero means the neuron contributes
21161/// nothing whatever `up` says. So compute every gate, keep the `k`
21162/// loudest, and read ONLY those neurons' `up` rows and `down` rows —
21163/// the latter needs `down_proj` stored transposed, otherwise a neuron's
21164/// down weights are a strided column and "reading only those" costs a
21165/// full cache line each.
21166///
21167/// Returns `None` when the file has no transposed `down` (the caller
21168/// then runs the ordinary dense path).
21169fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
21170    // The scatter path reads individual rows/columns and cannot express the
21171    // per-matrix signed FWHT boundary.  Let the descriptor-aware dense path
21172    // handle Prism files rather than silently running an unrotated sparse
21173    // approximation.
21174    if d.gate_proj.has_prism_contract()
21175        || d.up_proj.has_prism_contract()
21176        || d.down_proj.has_prism_contract()
21177    {
21178        return None;
21179    }
21180    let dt = d.down_t.as_ref()?;
21181    let inter = d.gate_proj.rows();
21182    let hidden = dt.cols();
21183    if k == 0 || k >= inter || d.act != Act::Silu {
21184        return None;
21185    }
21186    DYN_SCRATCH.with(|sc| {
21187        let mut sc = sc.borrow_mut();
21188        let DynScratch {
21189            g,
21190            mag,
21191            live,
21192            parts,
21193        } = &mut *sc;
21194        g.resize(inter, 0.0);
21195        d.gate_proj.matvec(x, g, pool);
21196        for v in g.iter_mut() {
21197            *v = inference::silu(*v);
21198        }
21199        // The k-th largest |silu(gate)| is the threshold; ties keep more,
21200        // which is the safe side.
21201        mag.clear();
21202        mag.extend(g.iter().map(|v| v.abs()));
21203        let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
21204            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21205        });
21206        let thr = *kth;
21207        live.clear();
21208        live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
21209        let mut out = vec![0.0f32; hidden];
21210        match pool {
21211            Some(p) if live.len() >= 64 => {
21212                let nw = p.n_workers() + 1;
21213                parts.clear();
21214                parts.resize(nw * hidden, 0.0);
21215                let ptr = SendMut(parts.as_mut_ptr());
21216                let n = live.len();
21217                let live_ref: &[u32] = live;
21218                let g_ref: &[f32] = g;
21219                p.run(&|w, workers| {
21220                    let chunk = n.div_ceil(workers);
21221                    let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
21222                    if s >= e {
21223                        return;
21224                    }
21225                    WORKER_SCRATCH.with(|ws| {
21226                        let mut ws = ws.borrow_mut();
21227                        let [scratch, acc] = &mut *ws;
21228                        scratch.resize(hidden.max(x.len()), 0.0);
21229                        acc.clear();
21230                        acc.resize(hidden, 0.0);
21231                        for (o, &nrm) in live_ref[s..e].iter().enumerate() {
21232                            // One neuron of runway: the next row's lines
21233                            // start moving while this one is multiplied.
21234                            if let Some(&nx) = live_ref[s..e].get(o + 1) {
21235                                d.up_proj.prefetch_row(nx as usize);
21236                                dt.prefetch_row(nx as usize);
21237                            }
21238                            let idx = nrm as usize;
21239                            let up = d.up_proj.row_dot(idx, x, scratch);
21240                            let a = g_ref[idx] * up;
21241                            if a != 0.0 {
21242                                dt.add_row_scaled(idx, a, acc, scratch);
21243                            }
21244                        }
21245                        for (j, v) in acc.iter().enumerate() {
21246                            unsafe { *ptr.at(w * hidden + j) = *v };
21247                        }
21248                    });
21249                });
21250                for w in 0..nw {
21251                    for (j, o) in out.iter_mut().enumerate() {
21252                        *o += parts[w * hidden + j];
21253                    }
21254                }
21255            }
21256            _ => {
21257                WORKER_SCRATCH.with(|ws| {
21258                    let mut ws = ws.borrow_mut();
21259                    let [scratch, _acc] = &mut *ws;
21260                    scratch.resize(hidden.max(x.len()), 0.0);
21261                    for &nrm in live.iter() {
21262                        let idx = nrm as usize;
21263                        let up = d.up_proj.row_dot(idx, x, scratch);
21264                        let a = g[idx] * up;
21265                        if a != 0.0 {
21266                            dt.add_row_scaled(idx, a, &mut out, scratch);
21267                        }
21268                    }
21269                });
21270            }
21271        }
21272        Some(out)
21273    })
21274}
21275
21276/// Caller-side scratch of the dynamic path — one allocation per thread,
21277/// not one per layer per token (that alone cost a third of the decode).
21278struct DynScratch {
21279    g: Vec<f32>,
21280    mag: Vec<f32>,
21281    live: Vec<u32>,
21282    parts: Vec<f32>,
21283}
21284
21285thread_local! {
21286    static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
21287        std::cell::RefCell::new(DynScratch {
21288            g: Vec::new(),
21289            mag: Vec::new(),
21290            live: Vec::new(),
21291            parts: Vec::new(),
21292        })
21293    };
21294    /// Pool-worker scratch: the row buffer and this worker's partial sum.
21295    static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
21296        const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
21297}
21298
21299/// `dense_ffn_cpu` with a per-visit mask landing on the activations —
21300/// the masked-inference fast path's decode arm. Full fused quant
21301/// compute, closed neurons zeroed before down: arithmetically the
21302/// pruned network, no dequant, no weight bytes touched.
21303fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
21304    let inter = d.gate_proj.rows();
21305    FFN_SCRATCH.with(|s| {
21306        let mut s = s.borrow_mut();
21307        let [g, u, ..] = &mut *s;
21308        g.resize(inter, 0.0);
21309        if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
21310            // g holds silu(gate)·up.
21311        } else {
21312            u.resize(inter, 0.0);
21313            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
21314            for i in 0..inter {
21315                g[i] = d.act.combine(g[i], u[i]);
21316            }
21317        }
21318        zero_masked_cols(g, 1, inter, mask_row);
21319        let mut out = attention::take_buf(d.down_proj.rows());
21320        d.down_proj.matvec(g, &mut out, pool);
21321        out
21322    })
21323}
21324
21325/// Dense FFN as one GPU submission via the MoE block path (single
21326/// expert, weight 1.0): gate → silu·up → down chained in one command
21327/// buffer, intermediate activations device-resident. None → weights
21328/// not q8-mapped in the primary shard / over the VRAM budget / backend
21329/// refusal → honest CPU path.
21330fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
21331    if d.gate_proj.has_prism_contract()
21332        || d.up_proj.has_prism_contract()
21333        || d.down_proj.has_prism_contract()
21334    {
21335        return None;
21336    }
21337    // The GPU block applies the standard graph activations plus the exact
21338    // MiniMax-M3 OAI contract; any other activation stays on the CPU.
21339    d.act.moe_job_graph_contract()?;
21340    // Threshold: tiny FFNs are not worth a submission (q1 excepted —
21341    // see the caller's gate).
21342    if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
21343        return None;
21344    }
21345    let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
21346    let mut model_ref = None;
21347    moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
21348    let model = model_ref?;
21349    let hidden = jobs[0].down.1;
21350    let mut out = attention::take_buf(hidden);
21351    if crate::gpu::moe_block(&model, &jobs, &mut out) {
21352        Some(out)
21353    } else {
21354        let mut out = out;
21355        attention::recycle_buf(&mut out);
21356        None
21357    }
21358}
21359
21360/// q8-mapped primary-shard tensor parts for a GPU job: q8_2f carries
21361/// its column field, q8_row runs with empty col slices (the backend
21362/// skips the multiply). Shared by the MoE block and the dense-FFN
21363/// single-job path.
21364#[allow(clippy::type_complexity)]
21365#[allow(clippy::type_complexity)]
21366pub(crate) fn moe_parts(
21367    t: &QTensor,
21368) -> Option<(
21369    &std::sync::Arc<cortiq_core::CmfModel>,
21370    usize,
21371    usize,
21372    usize,
21373    &[f32],
21374    &[f32],
21375    bool,
21376    bool,
21377    bool,
21378)> {
21379    match t {
21380        QTensor::Mapped {
21381            model,
21382            idx,
21383            dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
21384            rows,
21385            cols,
21386            row_scale,
21387            col_field,
21388            ..
21389        } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
21390            model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
21391        )),
21392        // q1: tile-embedded scales — empty rs/col slices, raw xs.
21393        QTensor::Mapped {
21394            model,
21395            idx,
21396            dtype: cortiq_core::TensorDtype::Q1,
21397            rows,
21398            cols,
21399            ..
21400        } => Some((
21401            model,
21402            *idx,
21403            *rows,
21404            *cols,
21405            &[][..],
21406            &[][..],
21407            true,
21408            false,
21409            false,
21410        )),
21411        // q4_tiled: 18-byte tiles with embedded f16 scales — raw xs.
21412        QTensor::Mapped {
21413            model,
21414            idx,
21415            dtype: cortiq_core::TensorDtype::Q4Tiled,
21416            rows,
21417            cols,
21418            ..
21419        } => Some((
21420            model,
21421            *idx,
21422            *rows,
21423            *cols,
21424            &[][..],
21425            &[][..],
21426            false,
21427            true,
21428            false,
21429        )),
21430        // q4tp: same raw-xs contract, different stride and scale plane.
21431        QTensor::Mapped {
21432            model,
21433            idx,
21434            dtype: cortiq_core::TensorDtype::Q4TiledP,
21435            rows,
21436            cols,
21437            ..
21438        } => Some((
21439            model,
21440            *idx,
21441            *rows,
21442            *cols,
21443            &[][..],
21444            &[][..],
21445            false,
21446            true,
21447            false,
21448        )),
21449        // q2tp: the 2-bit expert plane of the mixed profile — q4 family
21450        // for stride bookkeeping, flagged q2 so the trio validation can
21451        // demand a q4tp down.
21452        QTensor::Mapped {
21453            model,
21454            idx,
21455            dtype: cortiq_core::TensorDtype::Q2TiledP,
21456            rows,
21457            cols,
21458            ..
21459        } => Some((
21460            model,
21461            *idx,
21462            *rows,
21463            *cols,
21464            &[][..],
21465            &[][..],
21466            false,
21467            true,
21468            true,
21469        )),
21470        _ => None,
21471    }
21472}
21473
21474/// The routed experts of a MoE layer as the Metal chunk prefill takes
21475/// them: f32 router rows and every expert's q4tp (gate, up, down) tensor
21476/// indices, uniform SiLU shapes. Only the plain routed form — no shared
21477/// expert, no per-expert scale, no resonance or τ router, no router input
21478/// norm; the batched host walk keeps the rest. The routing itself stays the
21479/// host's (`moe_route`), so mask and selection bias need nothing here.
21480#[cfg(target_os = "macos")]
21481#[allow(clippy::type_complexity)]
21482fn chunk_moe_parts(
21483    m: &MoeFfn,
21484    hidden: usize,
21485) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
21486    if m.shared.is_some()
21487        || m.per_expert_scale.is_some()
21488        || m.resonance.is_some()
21489        || m.route_tau.is_some()
21490        || m.router_input_norm
21491        || m.experts.is_empty()
21492        || m.top_k == 0
21493        || std::env::var("CMF_RMS_TRACE").is_ok()
21494        || std::env::var("CMF_ACT_DUMP").is_ok()
21495        || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
21496    {
21497        return None;
21498    }
21499    let (rf, rr, rc) = m.router.f32_parts()?;
21500    if rr != m.experts.len() || rc != hidden {
21501        return None;
21502    }
21503    let inter = m.experts[0].gate_proj.rows();
21504    // All-q8_2f experts (Mellum2.1 q8_2f) ride the `moe_q8` GEMMs;
21505    // `CMF_METAL_MOE_Q8=0` keeps them on the host walk.
21506    let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
21507    let experts = m
21508        .experts
21509        .iter()
21510        .map(|e| {
21511            if e.act != Act::Silu
21512                || !e.segs.is_empty()
21513                || e.gate_proj.rows() != inter
21514                || e.up_proj.rows() != inter
21515                || e.down_proj.cols() != inter
21516                || e.gate_proj.cols() != hidden
21517                || e.down_proj.rows() != hidden
21518            {
21519                return None;
21520            }
21521            if q8 {
21522                let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21523                return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21524            }
21525            Some((
21526                e.gate_proj.mapped_q4tp()?.1,
21527                e.up_proj.mapped_q4tp()?.1,
21528                e.down_proj.mapped_q4tp()?.1,
21529            ))
21530        })
21531        .collect::<Option<Vec<_>>>()?;
21532    Some((rf, experts, q8))
21533}
21534
21535/// Map a MoE onto the Metal token graph's contract: f32 router, a
21536/// shared expert (gated — Qwen — or ungated at weight 1 — DeepSeek-V3 /
21537/// HunYuan hy_v3), softmax or sigmoid scores with an optional selection
21538/// bias and routed scale, experts uniformly q4tp (or the mixed profile:
21539/// q2tp gate/up over a q4tp down). τ routers, masks, per-expert scales
21540/// and Gemma's router-input norm refuse here — those semantics stay on
21541/// the CPU path.
21542/// Map Flint's released ragged MoE into the native page graph.  This stays
21543/// separate from `metal_moe_graph_parts`: its six routed slots are Q2TP and
21544/// 512-wide, while its ungated shared SwiGLU is Q4TP and 1024-wide.  Feeding
21545/// the shared tuple into the uniform jobs descriptor would make the shader
21546/// use the routed stride on a different payload.
21547#[cfg(target_os = "macos")]
21548fn metal_flint_moe_graph_parts(
21549    m: &MoeFfn,
21550    hidden: usize,
21551    scratch_key: usize,
21552) -> Option<crate::gpu::GpuFlintMoe<'_>> {
21553    use std::sync::Arc;
21554
21555    const HIDDEN: usize = 2_048;
21556    const ROUTED_INTER: usize = 512;
21557    const SHARED_INTER: usize = 1_024;
21558    const EXPERTS: usize = 256;
21559    const TOP_K: usize = 6;
21560
21561    if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
21562        return None;
21563    }
21564    let routed0 = m.experts.first()?;
21565    let (model, _) = routed0.gate_proj.mapped_q2tp()?;
21566    let (router, router_rows, router_cols) = m.router.f32_parts()?;
21567    if router_rows != EXPERTS || router_cols != HIDDEN {
21568        return None;
21569    }
21570    if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
21571        return None;
21572    }
21573    let same_q2 = |t: &QTensor| -> Option<usize> {
21574        let (owner, idx) = t.mapped_q2tp()?;
21575        Arc::ptr_eq(&owner, &model).then_some(idx)
21576    };
21577    let same_q4 = |t: &QTensor| -> Option<usize> {
21578        let (owner, idx) = t.mapped_q4tp()?;
21579        Arc::ptr_eq(&owner, &model).then_some(idx)
21580    };
21581    let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21582        if e.act != Act::Silu
21583            || !e.segs.is_empty()
21584            || e.gate_proj.rows() != ROUTED_INTER
21585            || e.gate_proj.cols() != HIDDEN
21586            || e.up_proj.rows() != ROUTED_INTER
21587            || e.up_proj.cols() != HIDDEN
21588            || e.down_proj.rows() != HIDDEN
21589            || e.down_proj.cols() != ROUTED_INTER
21590        {
21591            return None;
21592        }
21593        Some((
21594            same_q2(&e.gate_proj)?,
21595            same_q2(&e.up_proj)?,
21596            same_q4(&e.down_proj)?,
21597        ))
21598    };
21599    let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21600        if e.act != Act::Silu
21601            || !e.segs.is_empty()
21602            || e.gate_proj.rows() != SHARED_INTER
21603            || e.gate_proj.cols() != HIDDEN
21604            || e.up_proj.rows() != SHARED_INTER
21605            || e.up_proj.cols() != HIDDEN
21606            || e.down_proj.rows() != HIDDEN
21607            || e.down_proj.cols() != SHARED_INTER
21608        {
21609            return None;
21610        }
21611        Some((
21612            same_q4(&e.gate_proj)?,
21613            same_q4(&e.up_proj)?,
21614            same_q4(&e.down_proj)?,
21615        ))
21616    };
21617    let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
21618    if experts.len() != EXPERTS {
21619        return None;
21620    }
21621    let (shared_ffn, shared_gate) = m.shared.as_ref()?;
21622    if shared_gate.is_some() {
21623        return None;
21624    }
21625    Some(crate::gpu::GpuFlintMoe {
21626        router,
21627        bias: m.expert_bias.as_deref(),
21628        experts,
21629        scratch_key,
21630        shared: shared(shared_ffn)?,
21631        top_k: m.top_k,
21632        norm_topk: m.norm_topk_prob,
21633        route_scale: m.routed_scaling,
21634        sigmoid: m.router_sigmoid,
21635        sigmoid_denom_eps: m.sigmoid_denom_eps,
21636    })
21637}
21638
21639#[cfg(target_os = "macos")]
21640fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
21641    if m.router_input_norm
21642        || m.route_tau.is_some()
21643        || m.mask.is_some()
21644        || m.per_expert_scale.is_some()
21645        || m.experts.is_empty()
21646        || m.top_k == 0
21647        || m.resonance.is_some()
21648    {
21649        return None;
21650    }
21651    // Without a shared expert the select kernel fills the k routed jobs
21652    // only (flag 32). Measured on Mellum2.1's softmax router; the sigmoid /
21653    // biased routers without one (LFM2-MoE) keep the CPU path until they
21654    // are measured too.
21655    let (sh, sg) = match &m.shared {
21656        Some((sh, sg)) => (Some(sh), sg.as_ref()),
21657        None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
21658        None => return None,
21659    };
21660    let (rf, rr, rc) = m.router.f32_parts()?;
21661    if rr != m.experts.len() || rc != hidden {
21662        return None;
21663    }
21664    let shared_gated = sg.is_some();
21665    let sf = match sg {
21666        Some(sg) => {
21667            let (sf, sr, sc) = sg.f32_parts()?;
21668            if sr * sc != hidden {
21669                return None;
21670            }
21671            sf
21672        }
21673        // Ungated: the router's first row stands in for the gate matvec
21674        // (its logit is never read — the kernel pins weight 1).
21675        None => &rf[..hidden],
21676    };
21677    if let Some(b) = &m.expert_bias {
21678        if b.len() != m.experts.len() {
21679            return None;
21680        }
21681    }
21682    let inter = m.experts[0].gate_proj.rows();
21683    // The first expert's gate decides the profile; every trio (shared
21684    // included) must agree — the jobs ladder flips ONE kernel for all.
21685    let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
21686    // All-q8_2f routed experts (Mellum2.1 q8_2f: no shared expert, the
21687    // contract they were measured on) take the `moe_q8` kernels;
21688    // `CMF_METAL_MOE_Q8=0` keeps them off the graph (per-op path).
21689    let q8 = crate::gpu_metal::moe_q8::enabled()
21690        && m.shared.is_none()
21691        && m.experts[0].gate_proj.q8_2f_parts().is_some();
21692    let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21693        if e.act != Act::Silu
21694            || e.gate_proj.rows() != inter
21695            || e.gate_proj.cols() != hidden
21696            || e.up_proj.rows() != inter
21697            || e.up_proj.cols() != hidden
21698            || e.down_proj.rows() != hidden
21699            || e.down_proj.cols() != inter
21700        {
21701            return None;
21702        }
21703        if q8 {
21704            let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21705            return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21706        }
21707        let pick = |t: &QTensor| -> Option<usize> {
21708            if gu_q2 {
21709                t.mapped_q2tp().map(|(_, i)| i)
21710            } else {
21711                t.mapped_q4tp().map(|(_, i)| i)
21712            }
21713        };
21714        Some((
21715            pick(&e.gate_proj)?,
21716            pick(&e.up_proj)?,
21717            e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
21718        ))
21719    };
21720    let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
21721    let shared = match sh {
21722        Some(sh) => Some(trio(sh)?),
21723        None => None,
21724    };
21725    Some(crate::gpu::GpuMoe {
21726        router: rf,
21727        sgate: sf,
21728        experts,
21729        shared,
21730        n_exp: m.experts.len(),
21731        top_k: m.top_k,
21732        inter,
21733        norm_topk: m.norm_topk_prob,
21734        route_scale: m.routed_scaling,
21735        gu_q2,
21736        q8,
21737        sigmoid: m.router_sigmoid,
21738        sigmoid_denom_eps: m.sigmoid_denom_eps,
21739        bias: m.expert_bias.as_deref(),
21740        shared_gated,
21741    })
21742}
21743
21744/// `CMF_FORCE_IDS=<file>` (diagnostics): decode step n commits the n-th
21745/// whitespace-separated token id of the file instead of sampling, while the
21746/// logits are still computed (and dumped by `CMF_LOGIT_DUMP_ALL`) — one
21747/// backend teacher-forced along another's greedy sequence, so top-1
21748/// agreement can be counted past a near-tie divergence.
21749fn forced_ids_at(step: usize) -> Option<u32> {
21750    static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
21751    IDS.get_or_init(|| {
21752        std::env::var("CMF_FORCE_IDS")
21753            .ok()
21754            .and_then(|p| std::fs::read_to_string(p).ok())
21755            .map(|s| {
21756                s.split_whitespace()
21757                    .filter_map(|t| t.parse().ok())
21758                    .collect()
21759            })
21760            .unwrap_or_default()
21761    })
21762    .get(step)
21763    .copied()
21764}
21765
21766/// Build one gate/up/down GPU job from three tensors. `moe_push_job` is the
21767/// DenseFfn-shaped caller; architectures that keep their experts in their own
21768/// structs (DeepSeek-V4) come here directly.
21769pub(crate) fn moe_push_job_parts<'a>(
21770    gate: &'a QTensor,
21771    up: &'a QTensor,
21772    down: &'a QTensor,
21773    x: &[f32],
21774    w: f32,
21775    swiglu_limit: f32,
21776    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21777    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21778) -> Option<()> {
21779    use crate::qtensor::prescale;
21780    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
21781    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
21782    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
21783    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21784        return None; // mixed-dtype trio — honest CPU path
21785    }
21786    // The 2-bit profile is gate/up q2tp over a PLAIN q4tp down; any other
21787    // 2-bit arrangement stays on the CPU.
21788    if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
21789        return None;
21790    }
21791    if !gq2 && dq2 {
21792        return None;
21793    }
21794    model_ref.get_or_insert_with(|| gm.clone());
21795    let dt = |cf: &[f32]| {
21796        if cf.is_empty() {
21797            cortiq_core::TensorDtype::Q8Row
21798        } else {
21799            cortiq_core::TensorDtype::Q8_2f
21800        }
21801    };
21802    jobs.push(crate::gpu::MoeJob {
21803        gate: (gi, gr, gc, grs),
21804        up: (ui, ur, uc, urs),
21805        down: (di, dr, dc, drs),
21806        xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
21807        xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
21808        down_col: dcf,
21809        w,
21810        q1: gq1,
21811        q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
21812        q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
21813        gu_q2: gq2,
21814        swiglu_limit,
21815        act: 0,
21816    });
21817    Some(())
21818}
21819
21820/// Build one gate/up/down GPU job (see `moe_parts`).
21821fn moe_push_job<'a>(
21822    d: &'a DenseFfn,
21823    x: &[f32],
21824    w: f32,
21825    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21826    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21827) -> Option<()> {
21828    use crate::qtensor::prescale;
21829    // The block's act(gate)·up pass takes an exact device contract.  For
21830    // ordinary SiLU/GELU this is a zero clamp; MiniMax M3 contributes its
21831    // dedicated OAI marker and frozen 7.0 source limit.  Anything without a
21832    // device contract stays on the CPU.
21833    let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
21834    let act = graph_act.code();
21835    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
21836    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
21837    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
21838    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21839        return None; // mixed-dtype trio — honest CPU path
21840    }
21841    if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
21842        return None;
21843    }
21844    if !gq2 && dq2 {
21845        return None;
21846    }
21847    model_ref.get_or_insert_with(|| gm.clone());
21848    let gdt = if gcf.is_empty() {
21849        cortiq_core::TensorDtype::Q8Row
21850    } else {
21851        cortiq_core::TensorDtype::Q8_2f
21852    };
21853    let udt = if ucf.is_empty() {
21854        cortiq_core::TensorDtype::Q8Row
21855    } else {
21856        cortiq_core::TensorDtype::Q8_2f
21857    };
21858    jobs.push(crate::gpu::MoeJob {
21859        gate: (gi, gr, gc, grs),
21860        up: (ui, ur, uc, urs),
21861        down: (di, dr, dc, drs),
21862        xs_gate: prescale(x, gcf, gdt).into_owned(),
21863        xs_up: prescale(x, ucf, udt).into_owned(),
21864        down_col: dcf,
21865        w,
21866        q1: gq1,
21867        q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
21868        q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
21869        gu_q2: gq2,
21870        swiglu_limit,
21871        act,
21872    });
21873    Some(())
21874}
21875
21876/// Sparse dense-FFN directly on QUANTIZED weights (mask × mmap): reads
21877/// ONLY the active neurons' gate/up rows and down columns from the mmap
21878/// — no full-matrix dequant, no f32 model copy. This is what lets a
21879/// masked big model run at quantized RSS (the historical mask path
21880/// forced the whole model to f32). Semantics identical to the f32
21881/// sparse path within quant tolerance.
21882fn sparse_ffn_quant(
21883    d: &DenseFfn,
21884    x: &[f32],
21885    active: &[u16],
21886    hidden: usize,
21887    pool: Option<&Pool>,
21888) -> Vec<f32> {
21889    let n = active.len();
21890    let inter = d.gate_proj.rows();
21891    let mut act = vec![0.0f32; n];
21892    // Scratch is needed if EITHER projection is group-packed (q4/vbit);
21893    // gate/up normally share a dtype but sizing on both is robust.
21894    let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
21895    let compute = |ai: usize| -> f32 {
21896        let idx = active[ai] as usize;
21897        if idx >= inter {
21898            return 0.0; // defensive parity with the f32 sparse path
21899        }
21900        let mut s = if need_scratch {
21901            vec![0.0f32; hidden]
21902        } else {
21903            Vec::new()
21904        };
21905        let gate = d.gate_proj.row_dot(idx, x, &mut s);
21906        let up = d.up_proj.row_dot(idx, x, &mut s);
21907        d.act.combine(gate, up)
21908    };
21909    match pool {
21910        Some(p) if n >= 256 => {
21911            let ptr = SendMut(act.as_mut_ptr());
21912            p.run(&|widx, nw| {
21913                let chunk = n.div_ceil(nw);
21914                let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
21915                for ai in s..e {
21916                    unsafe { *ptr.at(ai) = compute(ai) };
21917                }
21918            });
21919        }
21920        _ => {
21921            for (ai, a) in act.iter_mut().enumerate() {
21922                *a = compute(ai);
21923            }
21924        }
21925    }
21926    // Scatter through active down columns (reads only those columns).
21927    let mut out = vec![0.0f32; hidden];
21928    for (ai, &idx) in active.iter().enumerate() {
21929        let w = act[ai];
21930        if w.abs() >= 1e-12 && (idx as usize) < inter {
21931            d.down_proj.add_col_scaled(idx as usize, w, &mut out);
21932        }
21933    }
21934    out
21935}
21936
21937/// Test-only re-export of the private sparse-quant FFN (mask × mmap gate).
21938#[doc(hidden)]
21939pub fn sparse_ffn_quant_for_test(
21940    d: &DenseFfn,
21941    x: &[f32],
21942    active: &[u16],
21943    hidden: usize,
21944) -> Vec<f32> {
21945    sparse_ffn_quant(d, x, active, hidden, None)
21946}
21947
21948/// Dequantize a DenseFfn's three matrices to f32 (transient; only the
21949/// q4/vbit-masked fallback uses it — the memory-lean path is
21950/// sparse_ffn_quant). Reuses row_f32 row-by-row.
21951fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
21952    let deq = |t: &QTensor| -> Vec<f32> {
21953        let (rows, cols) = (t.rows(), t.cols());
21954        let mut out = vec![0.0f32; rows * cols];
21955        for r in 0..rows {
21956            t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
21957        }
21958        out
21959    };
21960    (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
21961}
21962
21963/// Pointer wrapper for the worker-pool scatter (same pattern as qtensor).
21964struct SendMut(*mut f32);
21965unsafe impl Send for SendMut {}
21966unsafe impl Sync for SendMut {}
21967impl SendMut {
21968    #[inline]
21969    // Deliberate unsynchronized scatter: pool workers write disjoint indices
21970    // in parallel, so returning `&mut` from `&self` is intentional here.
21971    #[allow(clippy::mut_from_ref)]
21972    unsafe fn at(&self, i: usize) -> &mut f32 {
21973        unsafe { &mut *self.0.add(i) }
21974    }
21975}
21976
21977/// Router → (selected experts in torch.topk order, per-expert score
21978/// vector, normalizer). The final weight of expert `e` is `p[e] / wsum`.
21979///
21980/// Two regimes share this. Qwen: softmax over ALL experts, top-k of the
21981/// probabilities, optional renorm — `router_sigmoid=false`, no bias,
21982/// scale 1 → bit-identical to the historical path. LFM2-MoE /
21983/// DeepSeek-V3 `noaux_tc`: per-expert sigmoid scores, an optional
21984/// selection bias (top-k CHOICE only; weights stay unbiased), an
21985/// architecture-specified renorm floor and a routed scale.  The ordinary
21986/// loader contract is `1e-6`; Flint and MiniMax M3.1 preserve their
21987/// reference runtime's selected-score `sum + 1e-20`.
21988pub(crate) fn moe_route(
21989    logits: &[f32],
21990    m: &MoeFfn,
21991    allowed: Option<&[bool]>,
21992) -> (Vec<usize>, Vec<f32>, f32) {
21993    moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
21994}
21995
21996/// Router implementation with an explicit sigmoid renormalization floor.
21997///
21998/// Flint and MiniMax M3.1 reference runtimes compute
21999/// `sum(selected_scores) + 1e-20` in FP32. Using the generic 1e-6 floor is
22000/// not a harmless tolerance difference when all logits are very negative: it
22001/// collapses the routed branch toward zero instead of normalizing the selected
22002/// experts. Keeping the epsilon parameter here avoids changing the established
22003/// Qwen/LFM2 contract while allowing each architecture to preserve its own
22004/// numerical semantics.
22005pub(crate) fn moe_route_with_eps(
22006    logits: &[f32],
22007    m: &MoeFfn,
22008    allowed: Option<&[bool]>,
22009    sigmoid_denom_eps: f32,
22010) -> (Vec<usize>, Vec<f32>, f32) {
22011    let ne = logits.len();
22012    // Expert restriction: the static env mask (CMF_MOE_MASK) AND the
22013    // active task mask's expert fields (spec §5) both narrow the
22014    // candidate set; selection happens over the admitted experts only.
22015    // With norm_topk the kept weights renormalize below; without it
22016    // the excluded mass is honestly dropped.
22017    let admit = |e: usize| {
22018        m.mask.as_ref().is_none_or(|mk| mk[e])
22019            && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
22020    };
22021    // The resonance router (spec §9.5.1) selects by the RAW score: the
22022    // trainer (`resonance_winner`) and the resident graph
22023    // (`embryo_core_route_pick`) take the first maximum of the scores
22024    // and run the winner with weight 1.0. Selecting through the softmax
22025    // instead is not the same decision: `exp(l − max)` rounds two scores
22026    // closer than 2^-25 (possible below |score| 0.25) to the same 1.0, and
22027    // the lower index would take a token whose score is strictly smaller
22028    // — the trainer's trace and the graph would disagree with this path.
22029    // `−∞` (outside the shell) never wins; with no finite admitted expert
22030    // the generic path below degrades to uniform. The winner's
22031    // probability is 1.0 by construction (a one-hot `p`), so its
22032    // renormalized weight is `routed_scaling` on both norm_topk settings.
22033    if m.resonance.is_some() && m.top_k == 1 {
22034        let mut best: Option<usize> = None;
22035        for e in (0..ne).filter(|&e| admit(e)) {
22036            let l = logits[e];
22037            if l == f32::NEG_INFINITY || l.is_nan() {
22038                continue;
22039            }
22040            if best.is_none_or(|b| l > logits[b]) {
22041                best = Some(e);
22042            }
22043        }
22044        if let Some(b) = best {
22045            let mut p = vec![0.0f32; ne];
22046            p[b] = 1.0;
22047            return (vec![b], p, 1.0 / m.routed_scaling);
22048        }
22049    }
22050    // A `−∞` logit (a grown expert outside its shell, `Resonance::scores`)
22051    // takes probability 0 on both paths: sigmoid(−∞) = 0, exp(−∞ − max) = 0
22052    // — top-1 is the best FINITE expert, its renormalized weight exactly
22053    // 1.0. Every expert at −∞ cannot happen (trunk experts have no shell);
22054    // should it, the softmax would be NaN, so it degrades to uniform.
22055    let p: Vec<f32> = if m.router_sigmoid {
22056        logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
22057    } else {
22058        let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
22059        if mx == f32::NEG_INFINITY {
22060            vec![1.0 / ne.max(1) as f32; ne]
22061        } else {
22062            let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
22063            let s: f32 = e.iter().sum();
22064            for v in &mut e {
22065                *v /= s;
22066            }
22067            e
22068        }
22069    };
22070    let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
22071    // Descending by selection score, lower index wins ties (torch.topk).
22072    match &m.expert_bias {
22073        Some(b) => idx.sort_unstable_by(|&x, &y| {
22074            (p[y] + b[y])
22075                .partial_cmp(&(p[x] + b[x]))
22076                .unwrap()
22077                .then(x.cmp(&y))
22078        }),
22079        None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
22080    }
22081    idx.truncate(m.top_k);
22082    // Adaptive τ-routing: trim the tail experts once the kept mass is
22083    // enough. wsum below renormalizes over the KEPT set, so the output
22084    // stays a proper weighted average.
22085    if let Some(tau) = m.route_tau {
22086        let total: f32 = idx.iter().map(|&e| p[e]).sum();
22087        if total > 0.0 {
22088            let mut acc = 0.0f32;
22089            let mut keep = idx.len();
22090            for (i, &e) in idx.iter().enumerate() {
22091                acc += p[e];
22092                if acc >= tau * total {
22093                    keep = i + 1;
22094                    break;
22095                }
22096            }
22097            idx.truncate(keep);
22098        }
22099    }
22100    let wsum: f32 = if m.norm_topk_prob {
22101        let s: f32 = idx.iter().map(|&e| p[e]).sum();
22102        // Sigmoid routers use their architecture's reference floor; the
22103        // softmax path's probs already sum near 1, so it stays exactly as
22104        // before.
22105        (if m.router_sigmoid {
22106            s + sigmoid_denom_eps
22107        } else {
22108            s
22109        }) / m.routed_scaling
22110    } else {
22111        1.0 / m.routed_scaling
22112    };
22113    (idx, p, wsum)
22114}
22115
22116/// See the call site: one `layer:e1,e2,…` line per routed token.
22117fn moe_trace(idx: &[usize]) {
22118    moe_trace_at(crate::gpu::cur_layer() as i32, idx)
22119}
22120
22121/// The same, for callers that know their layer (DSV4 owns its layers and
22122/// never sets the pipeline's current-layer marker).
22123pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
22124    use std::io::Write;
22125    static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
22126        std::sync::OnceLock::new();
22127    let Some(f) = F.get_or_init(|| {
22128        let p = std::env::var("CMF_MOE_TRACE").ok()?;
22129        Some(std::sync::Mutex::new(
22130            std::fs::OpenOptions::new()
22131                .create(true)
22132                .append(true)
22133                .open(p)
22134                .ok()?,
22135        ))
22136    }) else {
22137        return;
22138    };
22139    let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
22140    let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
22141}
22142
22143/// MoE FFN: router → top-k experts (see `moe_route`). Only selected
22144/// experts' pages are touched in mmap.
22145pub(crate) fn moe_ffn(
22146    m: &MoeFfn,
22147    x: &[f32],
22148    pool: Option<&Pool>,
22149    allowed: Option<&[bool]>,
22150) -> Vec<f32> {
22151    let r = moe_ffn_route(m, x, pool, allowed);
22152    moe_ffn_experts(m, x, &r, pool)
22153}
22154
22155/// One token's host route through a MoE layer: the chosen experts in
22156/// selection order, the per-expert scores and the normalizer (see
22157/// `moe_route`), plus the raw router logits.
22158pub(crate) struct MoeRoute {
22159    pub idx: Vec<usize>,
22160    pub p: Vec<f32>,
22161    pub wsum: f32,
22162    pub logits: Vec<f32>,
22163}
22164
22165/// The routing half of `moe_ffn`, shared by every executor of the chosen
22166/// experts (the host/per-op path below and the MiMo dynamic device cache,
22167/// `crate::mimo_moe`): activation accounting, router logits, `moe_route`,
22168/// the selection statistics and the `CMF_MOE_TRACE` line — so switching
22169/// executors can never change which experts a token gets.
22170pub(crate) fn moe_ffn_route(
22171    m: &MoeFfn,
22172    x: &[f32],
22173    pool: Option<&Pool>,
22174    allowed: Option<&[bool]>,
22175) -> MoeRoute {
22176    accumulate_act(m, x, 1);
22177    let ne = m.experts.len();
22178    let mut logits = vec![0.0f32; ne];
22179    match &m.resonance {
22180        Some(r) => r.scores(x, &mut logits),
22181        None => m.router.matvec(x, &mut logits, pool),
22182    }
22183    let (idx, p, wsum) = moe_route(&logits, m, allowed);
22184    {
22185        let mut st = m.stats.borrow_mut();
22186        if st.len() < ne {
22187            st.resize(ne, 0);
22188        }
22189        for &e in &idx {
22190            st[e] += 1;
22191        }
22192    }
22193    // `CMF_MOE_TRACE=<file>`: append one line per (layer, token) with the
22194    // selected expert ids. The cumulative `stats` above answer "which
22195    // experts are popular"; a residency design needs the question they
22196    // cannot answer — whether CONSECUTIVE tokens reuse experts (the
22197    // temporal locality an LRU cache lives on, FreeToken §4).
22198    moe_trace(&idx);
22199    MoeRoute {
22200        idx,
22201        p,
22202        wsum,
22203        logits,
22204    }
22205}
22206
22207/// Flint's released mixed MoE profile has a native-Metal selected-expert
22208/// pager with a different numerical contract from the generic CPU probe:
22209/// the pager keeps activations in f32, whereas the fast host arm normally
22210/// uses its A8W8 activation path.  Timing those two approximations against
22211/// one another can make the probe reject the exact pager even when it is the
22212/// faster exact-f32 implementation.  Keep this gate deliberately narrower
22213/// than the pager itself; the pager revalidates every selected tuple before
22214/// it binds a no-copy view and an honest refusal still takes the CPU path.
22215#[cfg(target_os = "macos")]
22216fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
22217    const HIDDEN: usize = 2_048;
22218    const ROUTED_INTER: usize = 512;
22219    const SHARED_INTER: usize = 1_024;
22220    const EXPERTS: usize = 256;
22221    const TOP_K: usize = 6;
22222
22223    let Some(routed) = m.experts.first() else {
22224        return false;
22225    };
22226    let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
22227        return false;
22228    };
22229    let Some((shared, shared_gate)) = &m.shared else {
22230        return false;
22231    };
22232    if shared_gate.is_some()
22233        || model.header.arch.arch_name != "flint"
22234        || model.header.arch.prism_hadamard.is_some()
22235        || model.header.arch.hidden_size != HIDDEN
22236        || m.experts.len() != EXPERTS
22237        || m.top_k != TOP_K
22238        || !m.router_sigmoid
22239        || !m.norm_topk_prob
22240        || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
22241        || m.routed_scaling.to_bits() != 1.0f32.to_bits()
22242        || m.route_tau.is_some()
22243        || m.mask.is_some()
22244        || m.per_expert_scale.is_some()
22245        || m.router_input_norm
22246        || m.resonance.is_some()
22247        || !m.grown.is_empty()
22248    {
22249        return false;
22250    }
22251
22252    let routed_q2 = routed.act == Act::Silu
22253        && routed.segs.is_empty()
22254        && routed.gate_proj.rows() == ROUTED_INTER
22255        && routed.gate_proj.cols() == HIDDEN
22256        && routed.up_proj.rows() == ROUTED_INTER
22257        && routed.up_proj.cols() == HIDDEN
22258        && routed.down_proj.rows() == HIDDEN
22259        && routed.down_proj.cols() == ROUTED_INTER
22260        && routed.up_proj.mapped_q2tp().is_some()
22261        && routed.down_proj.mapped_q4tp().is_some();
22262    let shared_q4 = shared.act == Act::Silu
22263        && shared.segs.is_empty()
22264        && shared.gate_proj.rows() == SHARED_INTER
22265        && shared.gate_proj.cols() == HIDDEN
22266        && shared.up_proj.rows() == SHARED_INTER
22267        && shared.up_proj.cols() == HIDDEN
22268        && shared.down_proj.rows() == HIDDEN
22269        && shared.down_proj.cols() == SHARED_INTER
22270        && shared.gate_proj.mapped_q4tp().is_some()
22271        && shared.up_proj.mapped_q4tp().is_some()
22272        && shared.down_proj.mapped_q4tp().is_some();
22273    routed_q2 && shared_q4
22274}
22275
22276/// The expert half of `moe_ffn`: run a route's experts on the per-op GPU
22277/// block or the host.
22278pub(crate) fn moe_ffn_experts(
22279    m: &MoeFfn,
22280    x: &[f32],
22281    r: &MoeRoute,
22282    pool: Option<&Pool>,
22283) -> Vec<f32> {
22284    let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
22285    // D5: the whole layer MoE block in one GPU command buffer (experts — the
22286    // same mmap via a no-copy buffer; intermediate activations on the GPU).
22287    // MoE owns a separate probe class from dense FFN: its selected-span pager
22288    // has materially different residency and latency costs.
22289    if crate::gpu::enabled_here() {
22290        #[cfg(target_os = "macos")]
22291        if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
22292            // Do not compare the pager's exact-f32 result with the CPU's
22293            // optional A8W8 activation approximation.  The strict pager is
22294            // the validated Flint path; if a directory/view/device check
22295            // declines, preserve the existing CPU fallback rather than
22296            // leaving the probe in a half-sampled state.
22297            return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
22298                crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
22299            });
22300        }
22301        match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
22302            crate::gpu::ProbeArm::Gpu => {
22303                let t0 = std::time::Instant::now();
22304                if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
22305                    crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
22306                    return out;
22307                }
22308                crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
22309            }
22310            crate::gpu::ProbeArm::CpuTimed => {
22311                let t0 = std::time::Instant::now();
22312                let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
22313                crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
22314                return out;
22315            }
22316            crate::gpu::ProbeArm::Cpu => {
22317                return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
22318            }
22319        }
22320    }
22321    moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
22322}
22323
22324/// One MiniCPM CPU-tail MoE through its opt-in dynamic bank, or through the
22325/// same-route host fallback.  The route is formed exactly once so statistics,
22326/// tracing, and model semantics remain owned by `moe_ffn_route`.
22327fn moe_ffn_minicpm_tail(
22328    slot: &mut crate::minicpm_tail::MiniCpmTail,
22329    li: usize,
22330    total_layers: usize,
22331    m: &MoeFfn,
22332    x: &[f32],
22333    pool: Option<&Pool>,
22334) -> Vec<f32> {
22335    let route = moe_ffn_route(m, x, pool, None);
22336    match slot.forward(li, m, x, &route, pool, total_layers) {
22337        Some(out) => out,
22338        // A cache refusal must be observationally identical to the generic
22339        // CPU-tail path. The surrounding host-tail scope already disables the
22340        // per-op arena, so do not introduce a float-activation policy change
22341        // merely because this explicit opt-in cache declined a model/layout.
22342        None => moe_ffn_experts(m, x, &route, pool),
22343    }
22344}
22345
22346/// One MoE token through the MiMo expert bank (`crate::mimo_moe`), or —
22347/// when the bank does not serve it — through the host path with the SAME
22348/// route, so the routing statistics and `CMF_MOE_TRACE` see it once.
22349fn moe_ffn_banked(
22350    slot: &mut crate::mimo_moe::Slot,
22351    li: usize,
22352    m: &MoeFfn,
22353    x: &[f32],
22354    pool: Option<&Pool>,
22355) -> Vec<f32> {
22356    let t0 = std::time::Instant::now();
22357    let r = moe_ffn_route(m, x, pool, None);
22358    slot.note_route(t0.elapsed().as_nanos() as u64);
22359    match slot.forward(li, m, x, &r, pool) {
22360        Some(out) => out,
22361        None => crate::qtensor::float_activations_scope(|| {
22362            crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
22363        }),
22364    }
22365}
22366
22367/// Verify rows share a bank frame; routing and fallback are decode's.
22368fn moe_ffn_banked_rows(
22369    slot: &mut crate::mimo_moe::Slot,
22370    li: usize,
22371    m: &MoeFfn,
22372    xs: &[f32],
22373    b: usize,
22374    hidden: usize,
22375    pool: Option<&Pool>,
22376) -> Vec<f32> {
22377    let t0 = std::time::Instant::now();
22378    let routes: Vec<_> = xs
22379        .chunks_exact(hidden)
22380        .map(|x| moe_ffn_route(m, x, pool, None))
22381        .collect();
22382    slot.note_route(t0.elapsed().as_nanos() as u64);
22383    if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
22384        return out;
22385    }
22386    let mut out = Vec::with_capacity(b * hidden);
22387    for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
22388        let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
22389            // A failed bank must not stream missing experts into the arena.
22390            crate::qtensor::float_activations_scope(|| {
22391                crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
22392            })
22393        });
22394        out.extend(row);
22395    }
22396    out
22397}
22398
22399/// One-shot report of whether the whole-token wgpu graph actually formed.
22400/// A refusal silently reverts to the per-op path, which is how a model can
22401/// look "GPU-accelerated" while every layer walks the host.  A device prefix
22402/// is tracked separately because it still pays a host boundary for the tail.
22403fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
22404    use std::sync::atomic::{AtomicBool, Ordering};
22405    if built {
22406        GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
22407        if total_layers > 0 && layers_run < total_layers {
22408            GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
22409        } else {
22410            GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
22411        }
22412    } else {
22413        GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
22414    }
22415    static SAID: AtomicBool = AtomicBool::new(false);
22416    if !SAID.swap(true, Ordering::Relaxed) {
22417        if built {
22418            tracing::info!("wgpu whole-token graph: ACTIVE");
22419        } else {
22420            tracing::warn!("wgpu whole-token graph refused — per-op path");
22421        }
22422    }
22423}
22424
22425/// Whole-token graph outcomes, process-wide: a benchmark that claims a
22426/// GPU number while MISS climbs is measuring the CPU — the honest-bench
22427/// contract makes that an error, not a footnote.
22428pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22429pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22430/// Graph calls that returned a hidden after running only a leading device
22431/// prefix.  These are valid hybrid executions but must not be reported as a
22432/// full GPU graph in benchmark evidence.
22433pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22434/// Graph calls that covered the complete requested layer span.
22435pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22436
22437/// Native Metal TokenGraph completion counters. These are incremented only
22438/// after checked command-buffer completion and successful readback, so a
22439/// fused-head NLL report can prove the route rather than infer it from env.
22440pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22441pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22442pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
22443    std::sync::atomic::AtomicU64::new(0);
22444pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22445pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22446/// Ordinary native-Metal rows-prefill admissions and completed rows.  These
22447/// counters are separate from TokenGraph token/head counts so a batch NLL
22448/// receipt cannot accidentally claim serial execution as batched.
22449pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
22450    std::sync::atomic::AtomicU64::new(0);
22451pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22452pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
22453    std::sync::atomic::AtomicU64::new(0);
22454pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
22455    std::sync::atomic::AtomicU64::new(0);
22456
22457/// `CMF_MOE_BATCH=0` restores the per-expert serial loop — the A/B lever
22458/// for the batched kernel, and how its bit-identity is checked.
22459fn moe_batch_enabled() -> bool {
22460    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
22461    *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
22462}
22463
22464/// Two-dispatch CPU MoE: every routed expert (and the shared one) fused
22465/// into one gate/up/SiLU dispatch and one down dispatch, instead of two
22466/// pool barriers per expert. Bit-identical to the serial loop below —
22467/// see `moe_gate_up_many` / `moe_down_many`. `None` = the batched kernel
22468/// does not cover this layer, walk the serial path.
22469fn moe_ffn_cpu_batched(
22470    m: &MoeFfn,
22471    x: &[f32],
22472    idx: &[usize],
22473    p: &[f32],
22474    wsum: f32,
22475    pool: Option<&Pool>,
22476) -> Option<Vec<f32>> {
22477    if idx.is_empty() || !moe_batch_enabled() {
22478        return None;
22479    }
22480    // The bake probe reads per-neuron activation mass out of the
22481    // single-expert path; batching would skip it. Rare and offline —
22482    // hand those runs to the serial loop.
22483    if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
22484        return None;
22485    }
22486    let n = idx.len() + usize::from(m.shared.is_some());
22487    let mut pairs = Vec::with_capacity(n);
22488    let mut downs = Vec::with_capacity(n);
22489    let mut ws = Vec::with_capacity(n);
22490    for &e in idx {
22491        let d = &m.experts[e];
22492        if d.act != Act::Silu {
22493            return None;
22494        }
22495        pairs.push((&d.gate_proj, &d.up_proj));
22496        downs.push(&d.down_proj);
22497        ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
22498    }
22499    // The shared expert goes last, matching the serial loop's order —
22500    // the f32 accumulation order is part of the bit-identity claim.
22501    if let Some((se, gate)) = &m.shared {
22502        if se.act != Act::Silu {
22503            return None;
22504        }
22505        let g = gate.as_ref().map_or(1.0, |gate| {
22506            let mut gl = [0.0f32; 1];
22507            gate.matvec(x, &mut gl, pool);
22508            1.0 / (1.0 + (-gl[0]).exp())
22509        });
22510        pairs.push((&se.gate_proj, &se.up_proj));
22511        downs.push(&se.down_proj);
22512        ws.push(g);
22513    }
22514    let inter = pairs[0].0.rows();
22515    let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
22516    if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
22517        return None;
22518    }
22519    let mut out = attention::take_buf(x.len());
22520    if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
22521        attention::recycle_buf(&mut out);
22522        return None;
22523    }
22524    Some(out)
22525}
22526
22527/// Exact CPU completion for the routed experts a dynamic device cache did
22528/// not contain. The weights are already the router's final normalized mix.
22529/// Keeping this independent of `MoeFfn` makes the job `Sync`: its routing
22530/// statistics live in a `RefCell`, while the immutable expert tensors can be
22531/// evaluated safely in parallel with the GPU's resident subset.
22532/// Pooled-tail stage timers, reported per token by the bench JSON
22533/// (`m3_span_ms_per_tok`, `m3_cold_ms_per_tok`, `m3_arm_ms_per_tok`).
22534pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22535pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22536pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22537pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22538pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22539
22540pub(crate) fn moe_cold_experts_cpu(
22541    experts: &[(&DenseFfn, f32)],
22542    x: &[f32],
22543    pool: Option<&Pool>,
22544) -> Vec<f32> {
22545    let mut out = attention::take_buf(x.len());
22546    if experts.is_empty() {
22547        return out;
22548    }
22549    let pairs: Vec<_> = experts
22550        .iter()
22551        .map(|(e, _)| (&e.gate_proj, &e.up_proj))
22552        .collect();
22553    let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
22554    let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
22555    let inter = experts[0].0.gate_proj.rows();
22556    let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
22557    // The batched gate/up kernels implement PLAIN SiLU only (their legacy
22558    // helper passes limit 0 and no activation code). MiniMax's SwiGLU-OAI
22559    // (alpha 1.702, shifted up) must take the per-expert path that reads
22560    // each tensor's own activation — otherwise every cold expert computes
22561    // silu and the residual walks away from the reference (measured: the
22562    // pooled tail shipped cos 0.047 hidden while the resident path was
22563    // bit-close).
22564    let silu_only = experts
22565        .iter()
22566        .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
22567    if silu_only
22568        && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
22569        && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
22570    {
22571        return out;
22572    }
22573    out.fill(0.0);
22574    for &(expert, weight) in experts {
22575        let mut one = dense_ffn(expert, x, pool);
22576        for (o, v) in out.iter_mut().zip(&one) {
22577            *o += weight * v;
22578        }
22579        attention::recycle_buf(&mut one);
22580    }
22581    out
22582}
22583
22584/// Cold part of a short bank batch. Share each expert's weight stream
22585/// across its tokens, but reduce contributions in each token's route order.
22586/// On an unsupported CPU/layout, retain the single-token cold kernels.
22587pub(crate) fn moe_cold_experts_rows_cpu(
22588    jobs: &[Vec<(&DenseFfn, f32)>],
22589    xs: &[f32],
22590    hidden: usize,
22591    pool: Option<&Pool>,
22592) -> Vec<f32> {
22593    let mut out = vec![0.0; xs.len()];
22594    let mut experts: Vec<&DenseFfn> = Vec::new();
22595    let mut groups: Vec<Vec<usize>> = Vec::new();
22596    let mut terms = vec![Vec::new(); jobs.len()];
22597    for (r, row) in jobs.iter().enumerate() {
22598        for &(e, w) in row {
22599            let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
22600                Some(g) => g,
22601                None => {
22602                    experts.push(e);
22603                    groups.push(Vec::new());
22604                    groups.len() - 1
22605                }
22606            };
22607            terms[r].push((g, groups[g].len(), w));
22608            groups[g].push(r);
22609        }
22610    }
22611    if experts.is_empty() {
22612        return out;
22613    }
22614    let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
22615    let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
22616    let lens: Vec<_> = groups.iter().map(Vec::len).collect();
22617    let count: usize = lens.iter().sum();
22618    let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
22619    let mut ds = vec![vec![0.0; hidden]; count];
22620    if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
22621        && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
22622    {
22623        let mut offset = 0;
22624        let offsets: Vec<_> = lens
22625            .iter()
22626            .map(|&n| {
22627                let start = offset;
22628                offset += n;
22629                start
22630            })
22631            .collect();
22632        for (r, terms) in terms.iter().enumerate() {
22633            for &(g, slot, w) in terms {
22634                for (o, &v) in out[r * hidden..(r + 1) * hidden]
22635                    .iter_mut()
22636                    .zip(&ds[offsets[g] + slot])
22637                {
22638                    *o += w * v;
22639                }
22640            }
22641        }
22642    } else {
22643        for (r, jobs) in jobs.iter().enumerate() {
22644            if !jobs.is_empty() {
22645                let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
22646                out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
22647                attention::recycle_buf(&mut row);
22648            }
22649        }
22650    }
22651    out
22652}
22653
22654/// The pure-CPU MoE expert loop (also the fallback of every GPU refusal).
22655fn moe_ffn_cpu(
22656    m: &MoeFfn,
22657    x: &[f32],
22658    idx: &[usize],
22659    p: &[f32],
22660    wsum: f32,
22661    pool: Option<&Pool>,
22662) -> Vec<f32> {
22663    if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
22664        return out;
22665    }
22666    let mut out = attention::take_buf(x.len());
22667    for &e in idx {
22668        let mut eo = dense_ffn(&m.experts[e], x, pool);
22669        let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
22670        for i in 0..out.len() {
22671            out[i] += w * eo[i];
22672        }
22673        attention::recycle_buf(&mut eo);
22674    }
22675    if let Some((se, gate)) = &m.shared {
22676        let mut so = dense_ffn(se, x, pool);
22677        let g = gate.as_ref().map_or(1.0, |gate| {
22678            let mut gl = [0.0f32; 1];
22679            gate.matvec(x, &mut gl, pool);
22680            1.0 / (1.0 + (-gl[0]).exp())
22681        });
22682        for i in 0..out.len() {
22683            out[i] += g * so[i];
22684        }
22685        attention::recycle_buf(&mut so);
22686    }
22687    out
22688}
22689
22690/// DeepSeek-V2 MLA forward, expand-to-MHA form (see `AttnKind::Mla`):
22691/// per token the latent expands to every head's K/V and the ordinary
22692/// cache + grouped attend do the rest. K head layout is [rope | nope]
22693/// (rotary_dim = qk_rope rotates the shared rope key and each q head's
22694/// prefix); V rows are zero-padded to the K head_dim inside the cache
22695/// and the pad is sliced off before O. Attention importance is not
22696/// accumulated for MLA yet (no eviction interplay).
22697#[allow(clippy::too_many_arguments)]
22698pub(crate) fn mla_attention(
22699    w: &MlaWeights,
22700    normed: &[f32],
22701    cache: &mut crate::kv_cache::LayerKvCache,
22702    position: usize,
22703    inv_freq: &[f32],
22704    rope_scale: f32,
22705    eps: f64,
22706    pool: Option<&Pool>,
22707) -> Vec<f32> {
22708    let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
22709    let hd = dr + dn;
22710    let mut q = vec![0.0f32; nh * hd];
22711    match (&w.q_a, &w.q_a_norm) {
22712        (Some(qa), Some(qn)) => {
22713            let mut t = vec![0.0f32; qa.rows()];
22714            qa.matvec(normed, &mut t, pool);
22715            let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
22716            w.q_proj.matvec(&tn, &mut q, pool);
22717        }
22718        _ => w.q_proj.matvec(normed, &mut q, pool),
22719    }
22720    let mut ca = vec![0.0f32; lora + dr];
22721    w.kv_a.matvec(normed, &mut ca, pool);
22722    let (c_lat, k_rope) = ca.split_at_mut(lora);
22723    let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
22724    let mut kvb = vec![0.0f32; nh * (dn + dv)];
22725    w.kv_b.matvec(&latn, &mut kvb, pool);
22726    if !w.nope {
22727        attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
22728    }
22729    for h in 0..nh {
22730        if !w.nope {
22731            attention::rope_rotate_scaled(
22732                &mut q[h * hd..h * hd + dr],
22733                position,
22734                inv_freq,
22735                rope_scale,
22736            );
22737        }
22738    }
22739    let mut k = vec![0.0f32; nh * hd];
22740    let mut v = vec![0.0f32; nh * hd];
22741    for h in 0..nh {
22742        k[h * hd..h * hd + dr].copy_from_slice(k_rope);
22743        k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
22744        v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
22745    }
22746    cache.append(&k, &v, &vec![true; nh]);
22747    let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
22748    attention::recycle_buf(&mut imp);
22749    let mut ov = vec![0.0f32; nh * dv];
22750    for h in 0..nh {
22751        ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
22752    }
22753    let mut out = vec![0.0f32; w.o_proj.rows()];
22754    w.o_proj.matvec(&ov, &mut out, pool);
22755    out
22756}
22757
22758/// Gemma-4 dual-branch FFN (spec: see `FfnKind::DenseMoe`). The dense
22759/// branch reads the pre-FFN-normed activation; the router and the
22760/// expert branch read the RAW residual — the router through a
22761/// scale-less rms norm (its constant gain is folded into the weights),
22762/// the experts through `pre_norm_2`. CPU path; GPU graphs refuse the
22763/// layer kind honestly.
22764fn dense_moe_ffn(
22765    dm: &DenseMoeFfn,
22766    x_normed: &[f32],
22767    h_raw: &[f32],
22768    eps: f64,
22769    norm_style: NormStyle,
22770    pool: Option<&Pool>,
22771) -> Vec<f32> {
22772    let mut d = dense_ffn(&dm.dense, x_normed, pool);
22773    d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
22774    let m = &dm.moe;
22775    let ne = m.experts.len();
22776    let mut logits = vec![0.0f32; ne];
22777    if m.router_input_norm {
22778        let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
22779        let inv = 1.0 / (ss + eps as f32).sqrt();
22780        let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
22781        m.router.matvec(&xr, &mut logits, pool);
22782    } else {
22783        m.router.matvec(h_raw, &mut logits, pool);
22784    }
22785    let (idx, p, wsum) = moe_route(&logits, m, None);
22786    {
22787        let mut st = m.stats.borrow_mut();
22788        if st.len() < ne {
22789            st.resize(ne, 0);
22790        }
22791        for &e in &idx {
22792            st[e] += 1;
22793        }
22794    }
22795    let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
22796    let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
22797    let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
22798    for (di, mi) in d.iter_mut().zip(&mo) {
22799        *di += mi;
22800    }
22801    d
22802}
22803
22804/// Building the MoE-layer GPU jobs: all selected experts (+shared) must
22805/// be q8_2f-Mapped from the primary mapping; otherwise None → CPU path.
22806/// One-shot report of why the MoE GPU block refused. A silent `?` here
22807/// sends every expert to the CPU with nothing in the logs to say so —
22808/// which is exactly how a q4tp MoE model looked "GPU-accelerated" while
22809/// running entirely on the host.
22810fn moe_gpu_refused(why: &'static str) {
22811    use std::sync::atomic::{AtomicBool, Ordering};
22812    static SAID: AtomicBool = AtomicBool::new(false);
22813    if !SAID.swap(true, Ordering::Relaxed) {
22814        tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
22815    }
22816}
22817
22818fn moe_ffn_gpu(
22819    m: &MoeFfn,
22820    x: &[f32],
22821    idx: &[usize],
22822    p: &[f32],
22823    wsum: f32,
22824    pool: Option<&Pool>,
22825) -> Option<Vec<f32>> {
22826    use crate::gpu::MoeJob;
22827
22828    let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
22829    let mut model_ref = None;
22830    for &e in idx {
22831        if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
22832            moe_gpu_refused("push_job(expert)");
22833            return None;
22834        }
22835    }
22836    if let Some((se, gate)) = &m.shared {
22837        let g = gate.as_ref().map_or(1.0, |gate| {
22838            let mut gl = [0.0f32; 1];
22839            gate.matvec(x, &mut gl, pool);
22840            1.0 / (1.0 + (-gl[0]).exp())
22841        });
22842        if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
22843            moe_gpu_refused("push_job(shared)");
22844            return None;
22845        }
22846    }
22847    let Some(model) = model_ref else {
22848        moe_gpu_refused("no model_ref");
22849        return None;
22850    };
22851    let hidden = jobs[0].down.1;
22852    let mut out = vec![0.0f32; hidden];
22853    if crate::gpu::moe_block(&model, &jobs, &mut out) {
22854        Some(out)
22855    } else {
22856        moe_gpu_refused("gpu::moe_block");
22857        None
22858    }
22859}
22860
22861/// Single-position FFN dispatch.
22862fn ffn_forward(
22863    ffn: &FfnKind,
22864    x: &[f32],
22865    pool: Option<&Pool>,
22866    experts_allowed: Option<&[bool]>,
22867) -> Vec<f32> {
22868    match ffn {
22869        FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
22870        FfnKind::Dense(d) => dense_ffn(d, x, pool),
22871        FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
22872        // Dual-branch layers need the raw residual — their callers
22873        // dispatch dense_moe_ffn directly; the auxiliary paths that land
22874        // here (MTP draft, o1 replay) do not co-occur with gemma-4 MoE.
22875        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22876    }
22877}
22878
22879/// Fused two-position FFN: gate/up/down streamed once (dense). MoE
22880/// falls back to two singles — expert sets differ per position, there
22881/// is nothing to fuse.
22882fn ffn_forward_pair(
22883    ffn: &FfnKind,
22884    x1: &[f32],
22885    x2: &[f32],
22886    pool: Option<&Pool>,
22887    experts_allowed: Option<&[bool]>,
22888) -> (Vec<f32>, Vec<f32>) {
22889    let d = match ffn {
22890        // A tube layer has nothing to fuse across the pair — the tubes
22891        // are separate matrices; two singles are the honest path.
22892        FfnKind::Dense(d) if !d.segs.is_empty() => {
22893            return (
22894                tube_ffn(d, x1, 1, pool, None),
22895                tube_ffn(d, x2, 1, pool, None),
22896            );
22897        }
22898        FfnKind::Dense(d) => d,
22899        FfnKind::Moe(m) => {
22900            return (
22901                moe_ffn(m, x1, pool, experts_allowed),
22902                moe_ffn(m, x2, pool, experts_allowed),
22903            );
22904        }
22905        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22906    };
22907    let inter = d.gate_proj.rows();
22908    FFN_SCRATCH.with(|s| {
22909        let mut s = s.borrow_mut();
22910        let [g1, g2, u1, u2] = &mut *s;
22911        g1.resize(inter, 0.0);
22912        g2.resize(inter, 0.0);
22913        u1.resize(inter, 0.0);
22914        u2.resize(inter, 0.0);
22915        // Multi-matrix pair job: gate+up under one pool dispatch
22916        // (o1s = lane-1 outputs across tensors, o2s = lane-2).
22917        QTensor::matvec2_many(
22918            [&d.gate_proj, &d.up_proj],
22919            x1,
22920            x2,
22921            [g1.as_mut_slice(), u1.as_mut_slice()],
22922            [g2.as_mut_slice(), u2.as_mut_slice()],
22923            pool,
22924        );
22925        for i in 0..inter {
22926            g1[i] = d.act.combine(g1[i], u1[i]);
22927            g2[i] = d.act.combine(g2[i], u2[i]);
22928        }
22929        let mut o1 = attention::take_buf(d.down_proj.rows());
22930        let mut o2 = attention::take_buf(d.down_proj.rows());
22931        d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
22932        (o1, o2)
22933    })
22934}
22935
22936#[cfg(test)]
22937mod tests {
22938    use std::ffi::OsString;
22939    use std::sync::Mutex;
22940
22941    // Environment switches are process-global. Keep the speculative-path
22942    // tests serialized and restore every prior value even during unwinding.
22943    static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
22944
22945    struct ScopedTestEnv {
22946        previous: Vec<(&'static str, Option<OsString>)>,
22947    }
22948
22949    impl ScopedTestEnv {
22950        fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
22951            let previous = vars
22952                .iter()
22953                .map(|(name, _)| (*name, std::env::var_os(name)))
22954                .collect();
22955            for &(name, value) in vars {
22956                unsafe {
22957                    match value {
22958                        Some(value) => std::env::set_var(name, value),
22959                        None => std::env::remove_var(name),
22960                    }
22961                }
22962            }
22963            Self { previous }
22964        }
22965    }
22966
22967    impl Drop for ScopedTestEnv {
22968        fn drop(&mut self) {
22969            for (name, value) in self.previous.drain(..).rev() {
22970                unsafe {
22971                    match value {
22972                        Some(value) => std::env::set_var(name, value),
22973                        None => std::env::remove_var(name),
22974                    }
22975                }
22976            }
22977        }
22978    }
22979
22980    #[test]
22981    fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
22982        use super::pad_mtp_draft_logits;
22983
22984        let mut logits = vec![-3.0, -1.0, -2.0];
22985        pad_mtp_draft_logits(&mut logits, 6);
22986        assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
22987        assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
22988        assert_eq!(
22989            logits
22990                .iter()
22991                .enumerate()
22992                .max_by(|(_, a), (_, b)| a.total_cmp(b))
22993                .map(|(i, _)| i),
22994            Some(1),
22995            "an unevaluated row cannot win an all-negative shortlist"
22996        );
22997    }
22998
22999    #[test]
23000    fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
23001        use super::{Pipeline, SpecMon, SpecTrial};
23002
23003        let mut mon = SpecMon::default();
23004        let start = std::time::Instant::now();
23005        mon.round(1.0, 1);
23006        let first = Pipeline::spec_trial_round(
23007            SpecTrial::Spec {
23008                t0: start,
23009                gen0: 0,
23010                rounds: 0,
23011            },
23012            &mut mon,
23013            1,
23014        );
23015        assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
23016
23017        mon.round(1.0, 1);
23018        let second = Pipeline::spec_trial_round(first, &mut mon, 2);
23019        assert!(
23020            matches!(second, SpecTrial::Plain { gen0: 2, .. }),
23021            "two verifier-tip-only rounds must stop the losing MTP trial"
23022        );
23023
23024        // One successful draft resets the streak, so a viable head retains
23025        // the normal five-round measurement budget.
23026        let mut viable = SpecMon::default();
23027        viable.round(1.0, 1);
23028        viable.round(1.0, 2);
23029        let continued = Pipeline::spec_trial_round(
23030            SpecTrial::Spec {
23031                t0: start,
23032                gen0: 0,
23033                rounds: 1,
23034            },
23035            &mut viable,
23036            3,
23037        );
23038        assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
23039    }
23040
23041    #[test]
23042    fn fresh_mtp_request_does_not_inherit_full_head_streak() {
23043        let mut p = spark_test_pipeline(None);
23044        let head_rows = 248_070;
23045        p.draft_full_streak = 7;
23046        assert_eq!(p.draft_head_rows(head_rows), head_rows);
23047
23048        p.reset_fresh_mtp_draft_state();
23049        assert_eq!(
23050            p.draft_head_rows(head_rows),
23051            super::Pipeline::draft_vocab_rows(head_rows),
23052            "a fresh request returns to its configured shortlist"
23053        );
23054
23055        // A high-id first token of the NEW request still restores the
23056        // full-head protection immediately.
23057        p.note_draft_id(u32::MAX);
23058        assert_eq!(p.draft_head_rows(head_rows), head_rows);
23059    }
23060
23061    #[test]
23062    fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
23063        use super::graph_spec_wanted_by_policy;
23064
23065        // A capable, otherwise-default-eligible native Metal model stays on
23066        // the serial path until the user deliberately asks for the
23067        // experimental verifier.
23068        assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
23069        assert!(!graph_spec_wanted_by_policy(
23070            Some("anything-but-1"),
23071            true,
23072            false,
23073            false,
23074            true,
23075        ));
23076        assert!(graph_spec_wanted_by_policy(
23077            Some("1"),
23078            false,
23079            true,
23080            false,
23081            true,
23082        ));
23083        assert!(!graph_spec_wanted_by_policy(
23084            Some("0"),
23085            true,
23086            false,
23087            false,
23088            true,
23089        ));
23090
23091        // Do not regress the established discrete/wgpu policy while closing
23092        // the native-Metal quality gate.
23093        assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
23094        assert!(graph_spec_wanted_by_policy(
23095            Some("legacy-force"),
23096            false,
23097            true,
23098            true,
23099            false,
23100        ));
23101    }
23102
23103    #[test]
23104    fn encoder_hidden_states_are_normalized_and_reusable() {
23105        let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
23106        let ids = [1, 7, 11, 19];
23107
23108        let first = p.encode_hidden_states(&ids).expect("encoder forward");
23109        let second = p
23110            .encode_hidden_states(&ids)
23111            .expect("reusable encoder forward");
23112        assert_eq!(first.len(), ids.len() * 8);
23113        assert!(first.iter().all(|v| v.is_finite()));
23114        assert_eq!(
23115            first, second,
23116            "encoder forward must start from a fresh state"
23117        );
23118
23119        // An encoder call must not leave stale KV/GDN state behind for the
23120        // regular generative API sharing the same Pipeline implementation.
23121        assert!(p.forward_ids(&ids, None).is_ok());
23122    }
23123
23124    /// The 0.7.6 prefill-chunk rule: a plain dense stack wholly on a
23125    /// discrete card reads the prompt in wide chunks on x86; every other
23126    /// case keeps the width it had (the GDN-hybrid, MoE and DeepSeek paths
23127    /// were tuned on hardware not measured for this change).
23128    #[test]
23129    fn prefill_chunk_rule_widens_only_dense_on_discrete() {
23130        use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
23131        let dense_card = ChunkStackFacts {
23132            plain_dense: true,
23133            discrete: true,
23134            gpu_on: true,
23135            ..Default::default()
23136        };
23137        assert!(dense_card.dense_on_discrete());
23138        // The bug: a dense Llama on a Vulkan RTX 3090 got 48.
23139        assert_eq!(
23140            prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
23141            DISCRETE_DENSE_PREFILL_CHUNK
23142        );
23143        assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
23144        for (label, facts) in [
23145            (
23146                "GDN hybrid / MoE / DeepSeek stack",
23147                ChunkStackFacts {
23148                    plain_dense: false,
23149                    ..dense_card
23150                },
23151            ),
23152            (
23153                "integrated GPU",
23154                ChunkStackFacts {
23155                    discrete: false,
23156                    ..dense_card
23157                },
23158            ),
23159            (
23160                "CPU only",
23161                ChunkStackFacts {
23162                    gpu_on: false,
23163                    discrete: false,
23164                    ..dense_card
23165                },
23166            ),
23167            (
23168                "capacity split",
23169                ChunkStackFacts {
23170                    capacity_split: true,
23171                    ..dense_card
23172                },
23173            ),
23174            (
23175                "multi-GPU plan",
23176                ChunkStackFacts {
23177                    multi_gpu: true,
23178                    ..dense_card
23179                },
23180            ),
23181            (
23182                "O(1) layers",
23183                ChunkStackFacts {
23184                    o1: true,
23185                    ..dense_card
23186                },
23187            ),
23188        ] {
23189            assert!(!facts.dense_on_discrete(), "{label}");
23190            assert_eq!(
23191                prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
23192                48,
23193                "{label} keeps the historical x86 chunk"
23194            );
23195        }
23196        // Other hosts are untouched whatever the model.
23197        for dense in [false, true] {
23198            assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
23199            assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
23200        }
23201        // CMF_PREFILL_CHUNK still wins everywhere (and is clamped to ≥ 1).
23202        for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
23203            for dense in [false, true] {
23204                assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
23205                assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
23206            }
23207        }
23208    }
23209
23210    #[test]
23211    fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
23212        use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
23213        let full = |host_rows, device_rows| ReuseLayer {
23214            full: true,
23215            host_rows,
23216            device_rows,
23217            device_state: false,
23218        };
23219        // Turn 1: 300-token prompt prefilled on the host, 40 tokens decoded
23220        // by the wgpu graph into the device mirror only. Turn 2 reuses 339.
23221        assert_eq!(
23222            kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
23223            ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
23224        );
23225        // CPU / Metal: the host owner already holds every forwarded row.
23226        assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
23227        // A mirror past the prefix is fine for the host (it gets rewound).
23228        assert_eq!(
23229            kv_reuse_plan(339, &[full(339, Some(345))]),
23230            ReusePlan::Ready
23231        );
23232        // GPU prefix / CPU tail: only the device layers lag.
23233        assert_eq!(
23234            kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
23235            ReusePlan::Pull(vec![(0, 300, 339)])
23236        );
23237        // The device cannot supply the missing rows: never continue.
23238        assert_eq!(
23239            kv_reuse_plan(339, &[full(300, Some(320))]),
23240            ReusePlan::Fresh
23241        );
23242        assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
23243        assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
23244        // A recurrent state advanced on the device cannot be handed to a
23245        // host prefill (it is not rewindable and the host copy is stale).
23246        let conv = |device_state| ReuseLayer {
23247            full: false,
23248            host_rows: 0,
23249            device_rows: None,
23250            device_state,
23251        };
23252        assert_eq!(
23253            kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
23254            ReusePlan::Fresh
23255        );
23256        assert_eq!(
23257            kv_reuse_plan(339, &[conv(false), full(339, None)]),
23258            ReusePlan::Ready
23259        );
23260    }
23261
23262    #[test]
23263    fn nll_graph_policy_scopes_only_the_fused_head() {
23264        for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
23265            // A Vulkan/Wgpu hidden-only graph remains the quality route.
23266            ("vulkan graph", true, true, false, true, false),
23267            // Native Metal adds the strict fused graph-head contract.
23268            ("native Metal graph", true, true, true, true, true),
23269            // Masked NLL and the explicit non-graph fallback remain unchanged.
23270            ("masked", false, true, false, false, false),
23271            ("graph disabled", true, false, true, false, false),
23272        ] {
23273            let (graph_quality, graph_head_required) =
23274                super::nll_graph_policy(unmasked, prefer_graph, native_metal);
23275            assert_eq!(graph_quality, want_graph, "{label}: graph quality");
23276            assert_eq!(graph_head_required, want_head, "{label}: fused head");
23277        }
23278    }
23279
23280    #[test]
23281    fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
23282        assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
23283        assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
23284        assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
23285        assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
23286        assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
23287    }
23288
23289    #[test]
23290    fn cancel_flag_stops_generation() {
23291        let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
23292        // Set before the call: the prefill loops honour it, the run
23293        // returns immediately with the cancelled reason and no tokens.
23294        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23295        let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
23296        assert_eq!(r.finish_reason, "cancelled");
23297        assert!(
23298            r.token_ids.is_empty(),
23299            "no tokens after cancel: {:?}",
23300            r.token_ids
23301        );
23302        assert_eq!(p.kv_cache.seq_len(), 0);
23303        assert!(p.kv_history.is_empty());
23304        assert!(!p.graph_want_logits);
23305        assert!(p.graph_logits.is_none());
23306        // Flag auto-cleared: the next call generates normally.
23307        let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
23308        assert_ne!(r2.finish_reason, "cancelled");
23309    }
23310    use super::*;
23311
23312    #[test]
23313    fn minimax_m3_arch_selects_checked_oai_activation_only() {
23314        // Keep this fixture minimal: `Act::from_arch_full` needs only the
23315        // architecture id and activation name, while serde supplies stable
23316        // defaults for unrelated CMF fields.
23317        let arch = |arch_name: &str, hidden_act: &str| {
23318            serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
23319                "arch_name": arch_name,
23320                "hidden_size": 8,
23321                "intermediate_size": 16,
23322                "num_layers": 1,
23323                "num_attention_heads": 1,
23324                "num_kv_heads": 1,
23325                "head_dim": 8,
23326                "vocab_size": 16,
23327                "layer_types": ["FullAttention"],
23328                "rms_norm_eps": 1e-6,
23329                "max_position_embeddings": 16,
23330                "hidden_act": hidden_act
23331            }))
23332            .expect("minimal architecture")
23333        };
23334        let minimax = arch("minimax_m3_vl", "swigluoai");
23335        assert_eq!(
23336            Act::from_arch_full(&minimax),
23337            Act::SwiGluOai {
23338                alpha: 1.702,
23339                limit: 7.0,
23340            }
23341        );
23342
23343        // The same spelling must not turn into MiniMax math on a generic
23344        // model.  Only the source-validated M3 architecture owns it.
23345        let unrelated = arch("qwen2", "swigluoai");
23346        assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
23347
23348        // Conversely, an M3-tagged header with a normal activation keeps the
23349        // normal path; this prevents the architecture id from overriding the
23350        // explicit serialized contract.
23351        let non_oai = arch("minimax_m3_vl", "gelu");
23352        assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
23353    }
23354
23355    #[test]
23356    fn minimax_swiglu_oai_act_matches_scalar_contract() {
23357        let act = Act::swiglu_oai(1.702, 7.0);
23358        let got = act.combine(10.0, -10.0);
23359        let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
23360        assert!((got - expected).abs() < 1e-6);
23361        assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
23362        assert_eq!(
23363            act.wgpu_graph_act(),
23364            Some(crate::gpu::GraphAct::SwiGluOaiM3),
23365            "the WGPU-only arm accepts only the frozen M3 constants"
23366        );
23367        assert_eq!(
23368            Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
23369            None,
23370            "nearby OAI constants must not borrow MiniMax's kernel"
23371        );
23372        assert_eq!(
23373            act.moe_job_graph_contract(),
23374            Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
23375            "the direct MoE path carries the frozen clamp with the marker"
23376        );
23377        assert_eq!(
23378            Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
23379            None,
23380            "a nearby clamp must not borrow MiniMax's device contract"
23381        );
23382
23383        // `apply` is the gate half only; `combine` owns the up clamp and
23384        // +1 offset. The two must not accidentally collapse to ordinary SiLU.
23385        let gate_only = act.apply(-10.0);
23386        let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
23387        assert!((gate_only - expected_gate).abs() < 1e-10);
23388    }
23389
23390    #[test]
23391    fn explicit_sliding_schedule_selects_local_rope_profile() {
23392        // Mellum alternates three local 1,024-token layers with one global
23393        // YaRN layer.  An explicit schedule deliberately overrides the
23394        // cadence sentinel in `swa`, so test both RoPE table and scale
23395        // dispatch rather than merely checking that the metadata loaded.
23396        let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
23397        p.swa = Some((1024, usize::MAX));
23398        p.sliding_layers = Some(vec![true, true, true, false]);
23399        p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
23400        p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
23401        p.rope_scale = 1.277_258_9;
23402        p.rope_scale_local = 1.0;
23403
23404        for li in 0..3 {
23405            assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
23406            assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
23407            assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
23408        }
23409        assert_eq!(p.layer_window(3), None);
23410        assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
23411        assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
23412    }
23413
23414    /// sparse_ffn_quant must equal a dense FFN where inactive neurons are
23415    /// zeroed (mask × mmap correctness). On F32 tensors this is EXACT —
23416    /// it validates the row_dot / add_col_scaled / scatter indexing, the
23417    /// bug-prone part. The q8 branches reuse the golden-tested linear
23418    /// The per-token sparse path reads a transposed `down`; it must
23419    /// agree with the arm that computes everything and zeroes the
23420    /// losers, or the speed measurement is measuring a different model.
23421    #[test]
23422    fn dynamic_ffn_equals_the_zeroing_arm() {
23423        let (hidden, inter) = (8usize, 32usize);
23424        let synth = |n: usize, salt: usize| -> Vec<f32> {
23425            (0..n)
23426                .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
23427                .collect()
23428        };
23429        let down = synth(hidden * inter, 3);
23430        let mut down_t = vec![0.0f32; inter * hidden];
23431        for r in 0..hidden {
23432            for c in 0..inter {
23433                down_t[c * hidden + r] = down[r * inter + c];
23434            }
23435        }
23436        let d = DenseFfn {
23437            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23438            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23439            down_proj: QTensor::from_f32(down.clone(), hidden, inter),
23440            act: Act::Silu,
23441            down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
23442            segs: Vec::new(),
23443        };
23444        let x = synth(hidden, 11);
23445        let k = 12usize;
23446        let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
23447        // Reference: full compute, keep the k loudest |silu(gate)|.
23448        let mut g = vec![0.0f32; inter];
23449        d.gate_proj.matvec(&x, &mut g, None);
23450        let mut u = vec![0.0f32; inter];
23451        d.up_proj.matvec(&x, &mut u, None);
23452        for v in g.iter_mut() {
23453            *v = inference::silu(*v);
23454        }
23455        keep_top_k(&mut g, k);
23456        for i in 0..inter {
23457            g[i] *= u[i];
23458        }
23459        let mut want = vec![0.0f32; hidden];
23460        d.down_proj.matvec(&g, &mut want, None);
23461        for (a, b) in want.iter().zip(&got) {
23462            assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
23463        }
23464    }
23465
23466    /// A tube layer is the same layer, re-cut. With every tube open the
23467    /// answer must equal the dense FFN over the concatenated neurons
23468    /// (the permutation is an identity on the layer's function); with a
23469    /// tube closed it must equal the dense FFN with those neurons
23470    /// zeroed — the mask semantics, now paid for in bytes not read.
23471    #[test]
23472    fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
23473        let (hidden, core, tube) = (8usize, 12usize, 8usize);
23474        let inter = core + tube;
23475        let synth = |n: usize, salt: usize| -> Vec<f32> {
23476            (0..n)
23477                .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
23478                .collect()
23479        };
23480        let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
23481        let d_all = synth(hidden * inter, 3);
23482        // The dense layer, and the same weights cut into core + tube.
23483        let dense = DenseFfn {
23484            gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
23485            up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
23486            down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
23487            act: Act::Silu,
23488            down_t: None,
23489            segs: Vec::new(),
23490        };
23491        let rows =
23492            |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
23493        let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
23494            let mut o = Vec::with_capacity(hidden * (b - a));
23495            for r in 0..hidden {
23496                o.extend_from_slice(&v[r * inter + a..r * inter + b]);
23497            }
23498            o
23499        };
23500        let tubed = DenseFfn {
23501            down_t: None,
23502            gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
23503            up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
23504            down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
23505            act: Act::Silu,
23506            segs: vec![FfnSeg {
23507                gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
23508                up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
23509                down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
23510                start: core,
23511                width: tube,
23512            }],
23513        };
23514        let x = synth(hidden, 7);
23515        let want = dense_ffn(&dense, &x, None);
23516        let got = tube_ffn(&tubed, &x, 1, None, None);
23517        for (a, b) in want.iter().zip(&got) {
23518            assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
23519        }
23520        // Closed tube: bits on for the core, off for the tube.
23521        let mut bits = vec![0u8; inter.div_ceil(8)];
23522        for n in 0..core {
23523            bits[n / 8] |= 1 << (n % 8);
23524        }
23525        let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23526        let masked = dense_ffn_masked(&dense, &x, None, &bits);
23527        for (a, b) in masked.iter().zip(&closed) {
23528            assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
23529        }
23530        // The batched arm must agree with the single-position one.
23531        let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23532        for (a, b) in closed.iter().zip(&batch) {
23533            assert_eq!(a, b, "batch arm disagrees with decode arm");
23534        }
23535    }
23536
23537    /// scale, structurally identical to the matvec kernels.
23538    #[test]
23539    fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
23540        let (hidden, inter) = (16usize, 40usize);
23541        let synth = |n: usize, salt: usize| -> Vec<f32> {
23542            (0..n)
23543                .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
23544                .collect()
23545        };
23546        let d = DenseFfn {
23547            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23548            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23549            down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
23550            act: Act::Silu,
23551            down_t: None,
23552            segs: Vec::new(),
23553        };
23554        let x = synth(hidden, 9);
23555        // Active = every 3rd neuron.
23556        let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
23557
23558        let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
23559
23560        // Reference: full dense FFN but g[i]=0 for inactive neurons.
23561        let mut g = vec![0.0f32; inter];
23562        d.gate_proj.matvec(&x, &mut g, None);
23563        let mut u = vec![0.0f32; inter];
23564        d.up_proj.matvec(&x, &mut u, None);
23565        let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
23566        for i in 0..inter {
23567            g[i] = if act_set.contains(&(i as u16)) {
23568                inference::silu(g[i]) * u[i]
23569            } else {
23570                0.0
23571            };
23572        }
23573        let mut reference = vec![0.0f32; hidden];
23574        d.down_proj.matvec(&g, &mut reference, None);
23575
23576        let max_d = sparse
23577            .iter()
23578            .zip(&reference)
23579            .map(|(a, b)| (a - b).abs())
23580            .fold(0.0f32, f32::max);
23581        assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
23582    }
23583
23584    /// Attach a synthetic MTP head. `inter` deliberately stays independent
23585    /// from the trunk width so calibration tests can catch a trunk-proxy bug.
23586    fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
23587        let (h, inter, heads, kv, hd) = (
23588            p.hidden_size,
23589            inter,
23590            p.num_heads,
23591            p.num_kv_heads,
23592            p.head_dim,
23593        );
23594        let synth = |n: usize, salt: usize| -> Vec<f32> {
23595            (0..n)
23596                .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
23597                .collect()
23598        };
23599        let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
23600            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23601        };
23602        p.mtp = Some(MtpModule {
23603            enorm: vec![1.0; h],
23604            hnorm: vec![1.0; h],
23605            eh_proj: qt(h, 2 * h, 301),
23606            layer: LayerWeights {
23607                input_norm: vec![1.0; h],
23608                post_norm: vec![1.0; h],
23609                attn_out_norm: None,
23610                ffn_out_norm: None,
23611                layer_scale: None,
23612                ffn: FfnKind::Dense(DenseFfn {
23613                    gate_proj: qt(inter, h, 315),
23614                    up_proj: qt(inter, h, 316),
23615                    down_proj: qt(h, inter, 317),
23616                    act: Act::Silu,
23617                    down_t: None,
23618                    segs: Vec::new(),
23619                }),
23620                attn: AttnKind::Full {
23621                    bias: None,
23622                    wq: qt(heads * hd, h, 311),
23623                    wk: qt(kv * hd, h, 312),
23624                    wv: qt(kv * hd, h, 313),
23625                    wo: qt(h, heads * hd, 314),
23626                    q_norm: None,
23627                    k_norm: None,
23628                    output_gate: false,
23629                    softplus_gate: None,
23630                },
23631            },
23632            final_norm: vec![1.0; h],
23633            kv: crate::kv_cache::LayerKvCache::new(kv, hd),
23634        });
23635    }
23636
23637    /// Attach a synthetic MTP head with the trunk's conventional width.
23638    fn attach_test_mtp(p: &mut Pipeline) {
23639        attach_test_mtp_width(p, p.intermediate_size);
23640    }
23641
23642    #[test]
23643    fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
23644        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
23645        // This differs from the trunk FFN width (16). A result shaped like
23646        // the trunk probe would be a silent, dangerous compaction bug.
23647        attach_test_mtp_width(&mut p, 11);
23648        let ids = [3, 9, 4, 12, 7, 2];
23649        let trunk = p.probe_ffn_mass(&ids);
23650        assert_eq!(trunk[0].len(), p.intermediate_size);
23651
23652        let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
23653        assert_eq!(got.len(), 1);
23654        assert_eq!(got[0].mass.len(), 11);
23655        assert_eq!(got[0].squared_mass.len(), 11);
23656        assert_eq!(got[0].observed_positions, ids.len() - 1);
23657        assert!(got[0].mass.iter().any(|&v| v > 0.0));
23658        assert!(got[0]
23659            .mass
23660            .iter()
23661            .zip(&got[0].squared_mass)
23662            .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23663        assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
23664        assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
23665
23666        // Force an error *after* the MTP attention has appended a draft KV
23667        // row. This proves the error path clears detached draft state, not
23668        // merely the easy invalid-token preflight branch.
23669        let h = p.hidden_size;
23670        let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
23671            panic!("synthetic MTP must be dense")
23672        };
23673        dense.segs.push(FfnSeg {
23674            gate: QTensor::from_f32(vec![0.0; h], 1, h),
23675            up: QTensor::from_f32(vec![0.0; h], 1, h),
23676            down: QTensor::from_f32(vec![0.0; h], h, 1),
23677            start: dense.gate_proj.rows(),
23678            width: 1,
23679        });
23680        let err = p.probe_mtp_ffn(&ids);
23681        assert!(err
23682            .unwrap_err()
23683            .contains("tube-segmented draft FFNs are not supported"));
23684        assert_eq!(p.kv_cache.seq_len(), 0);
23685        assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
23686    }
23687
23688    #[test]
23689    fn speculative_equals_vanilla_greedy() {
23690        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23691        // Exercise the experimental host pair deterministically; the native
23692        // and wgpu batch graphs have independent coverage.
23693        let _env = ScopedTestEnv::set(&[
23694            ("CMF_GPU", Some("0")),
23695            ("CMF_GPU_WGPU_GRAPH", Some("0")),
23696            ("CMF_MTP_PAIR", Some("1")),
23697        ]);
23698        let run = |spec: bool| {
23699            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23700            p.sampler_config.temperature = 0.0;
23701            attach_test_mtp(&mut p);
23702            p.speculative = spec;
23703            let r = p.generate("abcdef", 12, None, None).unwrap();
23704            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23705        };
23706        let (vanilla, d0, _) = run(false);
23707        let (spec, d1, a1) = run(true);
23708        assert_eq!(d0, 0, "vanilla path must not draft");
23709        assert!(d1 > 0, "speculative path must draft");
23710        assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
23711        assert_eq!(
23712            vanilla, spec,
23713            "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
23714        );
23715    }
23716
23717    #[test]
23718    fn speculative_accepts_constant_oracle() {
23719        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23720        let _env = ScopedTestEnv::set(&[
23721            ("CMF_GPU", Some("0")),
23722            ("CMF_GPU_WGPU_GRAPH", Some("0")),
23723            ("CMF_MTP_PAIR", Some("1")),
23724        ]);
23725        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23726        p.sampler_config.temperature = 0.0;
23727        p.sampler_config.repetition_penalty = 1.0;
23728        // Constant lm_head → every logit equal → both the main model and
23729        // the draft head argmax to token 0: acceptance must be 100%.
23730        p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
23731        attach_test_mtp(&mut p);
23732        p.speculative = true;
23733        let r = p.generate("abcd", 10, None, None).unwrap();
23734        assert!(r.mtp_drafted > 0);
23735        assert_eq!(
23736            r.mtp_accepted, r.mtp_drafted,
23737            "constant logits → every draft accepted"
23738        );
23739        // Ties resolve to the same token in both the main and draft
23740        // heads — the sequence is one repeated token.
23741        assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
23742    }
23743
23744    #[test]
23745    fn host_fused_mtp_pair_is_opt_in() {
23746        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23747        let _env = ScopedTestEnv::set(&[
23748            ("CMF_GPU", Some("0")),
23749            ("CMF_GPU_WGPU_GRAPH", Some("0")),
23750            ("CMF_MTP_PAIR", None),
23751        ]);
23752        let run = |spec: bool| {
23753            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23754            p.sampler_config.temperature = 0.0;
23755            attach_test_mtp(&mut p);
23756            p.speculative = spec;
23757            let r = p.generate("abcdef", 12, None, None).unwrap();
23758            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23759        };
23760        let (plain, d0, _) = run(false);
23761        let (guarded, d1, a1) = run(true);
23762        assert_eq!(d0, 0);
23763        assert_eq!(d1, 0, "host MTP pair must remain opt-in");
23764        assert_eq!(a1, 0);
23765        assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
23766    }
23767
23768    #[test]
23769    fn empty_prompt_is_an_error_not_a_panic() {
23770        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23771        let r = p.generate("", 4, None, None);
23772        assert!(r.is_err(), "empty prompt must be a clean error");
23773    }
23774
23775    #[test]
23776    fn every_token_enters_kv_exactly_once() {
23777        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23778        // Greedy so no RNG variance; byte tokenizer → 3 prompt tokens.
23779        p.sampler_config.temperature = 0.0;
23780        let r = p.generate("abc", 2, None, None).unwrap();
23781        assert_eq!(r.prompt_tokens, 3);
23782        // prompt(3) + first sampled token forwarded before second logits:
23783        // step0 samples from prefill hidden (no extra forward), then
23784        // forwards t1 → cache 4; step1 samples, loop ends (max_tokens).
23785        assert_eq!(
23786            p.kv_cache.seq_len(),
23787            3 + r.tokens_generated - 1,
23788            "each token must be cached exactly once (v1 cached the last prompt token twice)"
23789        );
23790    }
23791
23792    #[test]
23793    fn generation_is_reproducible_with_seed() {
23794        let run = || {
23795            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23796            p.generate("hello", 8, None, None).unwrap().token_ids
23797        };
23798        assert_eq!(run(), run());
23799    }
23800
23801    #[test]
23802    fn resetting_sampler_restarts_the_seeded_stream() {
23803        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23804        let config = SamplerConfig {
23805            seed: Some(1234),
23806            ..SamplerConfig::default()
23807        };
23808        p.set_sampler_config(config.clone());
23809        let first = p.generate("hello", 8, None, None).unwrap().token_ids;
23810        p.set_sampler_config(config);
23811        let second = p.generate("hello", 8, None, None).unwrap().token_ids;
23812        assert_eq!(first, second);
23813    }
23814
23815    #[test]
23816    fn eviction_bounds_the_cache() {
23817        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23818        p.kv_cache.max_seq_len = 6;
23819        p.sampler_config.temperature = 0.0;
23820        let _ = p.generate("abcd", 12, None, None).unwrap();
23821        assert!(
23822            p.kv_cache.seq_len() <= 6 + 1,
23823            "cache must stay bounded by max_seq_len (got {})",
23824            p.kv_cache.seq_len()
23825        );
23826    }
23827
23828    #[test]
23829    fn confidence_matches_tokens_and_is_a_probability() {
23830        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23831        p.sampler_config.temperature = 0.0;
23832        p.sampler_config.repetition_penalty = 1.0;
23833        let r = p.generate("abcd", 10, None, None).unwrap();
23834        assert_eq!(
23835            r.token_confidence.len(),
23836            r.token_ids.len(),
23837            "one confidence per emitted token"
23838        );
23839        for &c in &r.token_confidence {
23840            assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
23841        }
23842        // top1_prob is a valid softmax probability.
23843        let logits = [1.0f32, 3.0, 0.5, 3.0];
23844        let p0 = top1_prob_t(&logits, 1, 1.0);
23845        let p1 = top1_prob_t(&logits, 3, 1.0);
23846        assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
23847        assert!(p0 > 0.0 && p0 < 1.0);
23848        // Calibration temperature > 1 softens an over-confident peak.
23849        let sharp = top1_prob_t(&logits, 1, 1.0);
23850        let soft = top1_prob_t(&logits, 1, 2.0);
23851        assert!(soft < sharp, "higher temperature lowers peak confidence");
23852    }
23853
23854    #[test]
23855    fn trace_is_opt_in_and_parallels_the_output() {
23856        // Off by default: the runtime is silent unless observation asked.
23857        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23858        p.sampler_config.temperature = 0.0;
23859        p.sampler_config.repetition_penalty = 1.0;
23860        let r = p.generate("abcd", 10, None, None).unwrap();
23861        assert!(r.traces.is_empty(), "trace must be empty unless enabled");
23862
23863        // On: exactly one row per emitted token, aligned with the output.
23864        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23865        p.sampler_config.temperature = 0.0;
23866        p.sampler_config.repetition_penalty = 1.0;
23867        p.set_trace(true);
23868        let r = p.generate("abcd", 10, None, None).unwrap();
23869        assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
23870        for (i, tr) in r.traces.iter().enumerate() {
23871            assert_eq!(tr.t, i, "trace index is sequential");
23872            assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
23873            assert_eq!(
23874                tr.confidence, r.token_confidence[i],
23875                "trace confidence matches the confidence channel"
23876            );
23877            // No dynamic router in this pipeline → no skill, no coherence.
23878            assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
23879        }
23880    }
23881
23882    #[test]
23883    fn explain_prefill_logits_match_greedy_first_token() {
23884        // `cortiq explain` shows the next-token distribution from
23885        // prefill_next_logits; its argmax must equal what greedy generate
23886        // actually emits first — otherwise explain would lie.
23887        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23888        p.sampler_config.temperature = 0.0;
23889        p.sampler_config.repetition_penalty = 1.0;
23890        let ids = p.tokenizer.encode("abcd");
23891        let logits = p.prefill_next_logits(&ids, None);
23892        let argmax = logits
23893            .iter()
23894            .enumerate()
23895            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
23896            .unwrap()
23897            .0 as u32;
23898        let r = p.generate("abcd", 1, None, None).unwrap();
23899        assert_eq!(
23900            argmax, r.token_ids[0],
23901            "explain preview must match greedy emit"
23902        );
23903    }
23904
23905    #[test]
23906    fn laguna_shared_expert_is_unconditionally_added() {
23907        let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
23908        let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
23909        let zero_dense = || DenseFfn {
23910            gate_proj: matrix(vec![0.0; 4]),
23911            up_proj: matrix(vec![0.0; 4]),
23912            down_proj: matrix(vec![0.0; 4]),
23913            act: Act::Silu,
23914            down_t: None,
23915            segs: Vec::new(),
23916        };
23917        let shared = DenseFfn {
23918            gate_proj: identity(),
23919            up_proj: identity(),
23920            down_proj: identity(),
23921            act: Act::Silu,
23922            down_t: None,
23923            segs: Vec::new(),
23924        };
23925        let x = [1.0, 2.0];
23926        let expected = dense_ffn(&shared, &x, None);
23927        let moe = MoeFfn {
23928            router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
23929            experts: vec![zero_dense()],
23930            top_k: 1,
23931            norm_topk_prob: true,
23932            router_sigmoid: true,
23933            sigmoid_denom_eps: 1e-6,
23934            expert_bias: None,
23935            routed_scaling: 1.0,
23936            route_tau: None,
23937            shared: Some((shared, None)),
23938            stats: std::cell::RefCell::new(Vec::new()),
23939            act_sq: std::cell::RefCell::new(Vec::new()),
23940            act_rows: std::cell::RefCell::new(Vec::new()),
23941            mask: None,
23942            per_expert_scale: None,
23943            router_input_norm: false,
23944            resonance: None,
23945            grown: Vec::new(),
23946        };
23947        let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
23948        for (actual, expected) in actual.iter().zip(expected) {
23949            assert!((actual - expected).abs() < 1e-6);
23950        }
23951    }
23952
23953    /// A tiny MiMo-V2-shaped stack (the M3 fixture): layers [full, sliding,
23954    /// sliding, full]; 4 Q heads over 1 (full) / 2 (sliding) KV heads;
23955    /// head_dim 8 with 4-wide V heads; partial rotary 4 at θ 1e7 (full) /
23956    /// 1e4 (sliding); window 3; learned sinks on the sliding layers; layer
23957    /// 0 a dense FFN, layers 1..3 sigmoid-routed MoE with a selection bias
23958    /// (4 experts, top-2, renormalized, no shared expert). Geometry and
23959    /// sinks go through the same `set_attn_geometry` / `set_layer_sinks`
23960    /// the loader calls.
23961    fn mimo_test_pipeline() -> Pipeline {
23962        let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
23963        let kvh = [1usize, 2, 2, 1];
23964        let synth = |n: usize, salt: usize| -> Vec<f32> {
23965            (0..n)
23966                .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23967                .collect()
23968        };
23969        let qt = |rows: usize, cols: usize, salt: usize| {
23970            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23971        };
23972        let dense = |inter: usize, salt: usize| DenseFfn {
23973            gate_proj: qt(inter, hs, salt),
23974            up_proj: qt(inter, hs, salt + 1),
23975            down_proj: qt(hs, inter, salt + 2),
23976            act: Act::Silu,
23977            down_t: None,
23978            segs: Vec::new(),
23979        };
23980        let layers: Vec<LayerWeights> = (0..4)
23981            .map(|li| LayerWeights {
23982                input_norm: vec![1.0; hs],
23983                post_norm: vec![1.0; hs],
23984                attn_out_norm: None,
23985                ffn_out_norm: None,
23986                layer_scale: None,
23987                ffn: if li == 0 {
23988                    FfnKind::Dense(dense(inter, 50))
23989                } else {
23990                    FfnKind::Moe(MoeFfn {
23991                        router: qt(4, hs, 60 + li),
23992                        experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
23993                        top_k: 2,
23994                        norm_topk_prob: true,
23995                        router_sigmoid: true,
23996                        sigmoid_denom_eps: 1e-6,
23997                        expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
23998                        routed_scaling: 1.0,
23999                        route_tau: None,
24000                        shared: None,
24001                        stats: std::cell::RefCell::new(Vec::new()),
24002                        act_sq: std::cell::RefCell::new(Vec::new()),
24003                        act_rows: std::cell::RefCell::new(Vec::new()),
24004                        mask: None,
24005                        per_expert_scale: None,
24006                        router_input_norm: false,
24007                        resonance: None,
24008                        grown: Vec::new(),
24009                    })
24010                },
24011                attn: AttnKind::Full {
24012                    wq: qt(nh * hd, hs, li * 10 + 1),
24013                    wk: qt(kvh[li] * hd, hs, li * 10 + 2),
24014                    wv: qt(kvh[li] * vd, hs, li * 10 + 3),
24015                    wo: qt(hs, nh * vd, li * 10 + 4),
24016                    q_norm: None,
24017                    k_norm: None,
24018                    output_gate: false,
24019                    softplus_gate: None,
24020                    bias: None,
24021                },
24022            })
24023            .collect();
24024        let mut p = Pipeline::new(
24025            Tokenizer::byte_level(),
24026            PipelineWeights {
24027                embed_tokens: qt(vocab, hs, 100),
24028                layers,
24029                lm_head: qt(vocab, hs, 200),
24030                final_norm: vec![1.0; hs],
24031            },
24032            hs,
24033            inter,
24034            nh,
24035            1, // header num_kv_heads (the full layers')
24036            hd,
24037            4,
24038            4,
24039            false,
24040            vocab,
24041            1e-6,
24042            1e7,
24043            NormStyle::Qwen,
24044            4096,
24045            SamplerConfig {
24046                seed: Some(7),
24047                ..Default::default()
24048            },
24049        );
24050        // Diagnostics stay off whatever the test environment exports.
24051        p.layer_dump = None;
24052        p.set_rotary(4, 1e7);
24053        p.sliding_layers = Some(vec![false, true, true, false]);
24054        p.swa = Some((3, usize::MAX));
24055        p.rotary_dim_local = Some(4);
24056        p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
24057        p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
24058        p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
24059        p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
24060        p
24061    }
24062
24063    #[test]
24064    fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
24065        let mut p = mimo_test_pipeline();
24066        p.speculative = false;
24067        p.ignore_eos = true;
24068        p.sampler_config.temperature = 0.0;
24069        p.sampler_config.repetition_penalty = 1.0;
24070        let a = vec![3, 5, 7, 9, 11, 13];
24071        let b = vec![4, 8, 12, 16, 20, 24];
24072        let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
24073        let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
24074        // Same placeholder IDs as an earlier request are not a cache key
24075        // for different media. The actual rows, not a re-embedding of a,
24076        // must determine the continuation.
24077        let actual = p
24078            .generate_from_embeds(&a, &rows, 8, None, None)
24079            .unwrap()
24080            .token_ids;
24081        assert_eq!(actual, expected);
24082        assert!(p.kv_history.is_empty());
24083        let mut extended = a.clone();
24084        extended.push(17);
24085        let after_media = p
24086            .generate_from_ids(&extended, 8, None, None)
24087            .unwrap()
24088            .token_ids;
24089        p.reset_session();
24090        let fresh = p
24091            .generate_from_ids(&extended, 8, None, None)
24092            .unwrap()
24093            .token_ids;
24094        assert_eq!(after_media, fresh);
24095        assert!(
24096            p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
24097                .is_err()
24098        );
24099        // Force a real token-prefix reuse opportunity into the media call.
24100        // Those labels are unchanged, but their embeddings now describe a
24101        // different source sequence and every KV row must be rebuilt.
24102        p.reset_session();
24103        p.generate_from_ids(&a, 1, None, None).unwrap();
24104        let mut media_ids = p.kv_history.clone();
24105        assert!(!media_ids.is_empty());
24106        media_ids.extend_from_slice(&[19, 21, 23]);
24107        let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
24108        let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
24109        let mut oracle = mimo_test_pipeline();
24110        oracle.speculative = false;
24111        oracle.ignore_eos = true;
24112        oracle.sampler_config.temperature = 0.0;
24113        oracle.sampler_config.repetition_penalty = 1.0;
24114        let expected = oracle
24115            .generate_from_ids(&source_ids, 8, None, None)
24116            .unwrap()
24117            .token_ids;
24118        assert_eq!(
24119            p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
24120                .unwrap()
24121                .token_ids,
24122            expected
24123        );
24124        assert!(p.kv_history.is_empty());
24125        let mut bad = rows;
24126        bad[0] = f32::NAN;
24127        assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
24128    }
24129
24130    fn f32_bits(v: &[f32]) -> Vec<u32> {
24131        v.iter().map(|x| x.to_bits()).collect()
24132    }
24133
24134    /// M3 acceptance: on the MiMo-shaped stack the decode walk (one
24135    /// position at a time through `forward_layers`) and the batched
24136    /// prefill (`prefill_batch_span`, whole prompt and split in two
24137    /// chunks) give bit-identical logits at all 12 positions — per-layer
24138    /// KV heads, narrow V, sinks, the window and the biased sigmoid MoE all
24139    /// agree across the two walks.
24140    #[test]
24141    fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
24142        let mut p = mimo_test_pipeline();
24143        let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
24144        assert_eq!(kv, vec![1, 2, 2, 1]);
24145        assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
24146        assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
24147        let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
24148        let hs = p.hidden_size;
24149        let mut decode = Vec::new();
24150        for (pos, &id) in ids.iter().enumerate() {
24151            let e = p.embed_single(id);
24152            let h = p.forward_layers(&e, pos, None);
24153            decode.push(p.logits_from_hidden(&h));
24154        }
24155        for l in &p.kv_cache.layers {
24156            assert_eq!(l.seq_len, 12);
24157            // V rows are padded to head_dim inside the cache.
24158            assert_eq!(l.head_values(0).len(), 12 * 8);
24159        }
24160        assert!(decode.iter().flatten().all(|v| v.is_finite()));
24161
24162        p.clear_sequence_state();
24163        let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24164        for pos in 0..ids.len() {
24165            let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
24166            assert_eq!(
24167                f32_bits(&decode[pos]),
24168                f32_bits(&lg),
24169                "whole prompt, pos {pos}"
24170            );
24171        }
24172
24173        p.clear_sequence_state();
24174        let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
24175        let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
24176        for pos in 0..ids.len() {
24177            let row = if pos < 5 {
24178                &a[pos * hs..(pos + 1) * hs]
24179            } else {
24180                &b[(pos - 5) * hs..(pos - 4) * hs]
24181            };
24182            let lg = p.logits_from_hidden(row);
24183            assert_eq!(
24184                f32_bits(&decode[pos]),
24185                f32_bits(&lg),
24186                "two chunks, pos {pos}"
24187            );
24188        }
24189
24190        // The fixture is not degenerate: the sinks and the window each
24191        // change the answer.
24192        let last = |p: &mut Pipeline| {
24193            p.clear_sequence_state();
24194            let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24195            p.logits_from_hidden(&hb[11 * hs..12 * hs])
24196        };
24197        let base = last(&mut p);
24198        let mut no_sinks = mimo_test_pipeline();
24199        for l in &mut no_sinks.kv_cache.layers {
24200            l.sinks = None;
24201        }
24202        assert_ne!(
24203            f32_bits(&last(&mut no_sinks)),
24204            f32_bits(&base),
24205            "sinks are live"
24206        );
24207        let mut wide = mimo_test_pipeline();
24208        wide.swa = Some((64, usize::MAX));
24209        assert_ne!(
24210            f32_bits(&last(&mut wide)),
24211            f32_bits(&base),
24212            "window is live"
24213        );
24214
24215        // Generation runs end to end on the same stack.
24216        p.clear_sequence_state();
24217        p.ignore_eos = true;
24218        let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
24219        assert_eq!(r.token_ids.len(), 4);
24220    }
24221
24222    /// A Spark-X2.5-shaped stack: layers 0..3 slide (window 6), layer 3
24223    /// is global, every attention carries the head-wise sigmoid g_proj gate.
24224    fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
24225        let (hs, nh) = (16usize, 4usize);
24226        let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
24227        p.layer_dump = None;
24228        p.swa = Some((6, 4));
24229        p.proj_gate_sigmoid = true;
24230        for (li, lw) in p.weights.layers.iter_mut().enumerate() {
24231            if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
24232                let g: Vec<f32> = (0..nh * hs)
24233                    .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
24234                    .collect();
24235                *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
24236            }
24237        }
24238        p.swa_trim = trim;
24239        p
24240    }
24241
24242    /// Trimming the sliding tails changes nothing but memory: the same
24243    /// prompt in uneven chunks, a decode walk, and fused pairs with a
24244    /// 2-row rollback give bit-identical hiddens with and without it,
24245    /// while the trimmed sliding layers stay bounded and the global layer
24246    /// keeps every row.
24247    #[test]
24248    fn swa_trim_pipeline_matches_untrimmed_bitwise() {
24249        let mut a = spark_test_pipeline(None);
24250        let mut b = spark_test_pipeline(Some((2, 4)));
24251        assert_eq!(
24252            (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
24253            vec![Some(6), Some(6), Some(6), None]
24254        );
24255        let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
24256        let mut pos = 0usize;
24257        for &n in [5usize, 13, 7, 16].iter().cycle() {
24258            if pos >= ids.len() {
24259                break;
24260            }
24261            let end = (pos + n).min(ids.len());
24262            let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
24263            let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
24264            assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
24265            pos = end;
24266        }
24267        for _ in 0..23 {
24268            let e = a.embed_single(((pos * 7) % 64) as u32);
24269            let ha = a.forward_layers(&e, pos, None);
24270            let hb = b.forward_layers(&e, pos, None);
24271            assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
24272            for li in 0..3 {
24273                assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
24274            }
24275            pos += 1;
24276        }
24277        for round in 0..9 {
24278            let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
24279            let (a1, a2) = a.forward_pair(&e1, &e2, pos);
24280            let (b1, b2) = b.forward_pair(&e1, &e2, pos);
24281            assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
24282            assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
24283            if round % 2 == 1 {
24284                // A rejected draft: both lanes roll back.
24285                for p in [&mut a, &mut b] {
24286                    for l in &mut p.kv_cache.layers {
24287                        l.truncate_last(2);
24288                    }
24289                }
24290            } else {
24291                pos += 2;
24292            }
24293        }
24294        let e = a.embed_single(9);
24295        let (ha, hb) = (
24296            a.forward_layers(&e, pos, None),
24297            b.forward_layers(&e, pos, None),
24298        );
24299        assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
24300        for li in 0..3 {
24301            let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
24302            assert!(lb.base() > 0, "layer {li} trimmed");
24303            assert_eq!(la.base(), 0);
24304            assert_eq!(lb.pos_len(), la.seq_len);
24305            assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
24306        }
24307        let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
24308        assert_eq!(
24309            (gb.base(), gb.seq_len),
24310            (0, ga.seq_len),
24311            "the global layer keeps all"
24312        );
24313        assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
24314        assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
24315    }
24316
24317    /// The network split's KvFetch hand-off on a trimmed stack: every
24318    /// layer shipped over the wire mid-sequence (the sliding ones as
24319    /// `FullTail`) into a fresh pipeline that never ran a position, which
24320    /// then decodes on — bit for bit what the untrimmed pipeline decodes.
24321    #[test]
24322    fn swa_trim_wire_handoff_continues_bitwise() {
24323        let mut a = spark_test_pipeline(None);
24324        let mut b = spark_test_pipeline(Some((2, 4)));
24325        let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
24326        let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
24327        let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
24328        assert_eq!(f32_bits(&ha), f32_bits(&hb));
24329        let mut pos = ids.len();
24330        for _ in 0..5 {
24331            let e = a.embed_single(((pos * 5) % 64) as u32);
24332            assert_eq!(
24333                f32_bits(&a.forward_layers(&e, pos, None)),
24334                f32_bits(&b.forward_layers(&e, pos, None))
24335            );
24336            pos += 1;
24337        }
24338        let mut c = spark_test_pipeline(Some((2, 4)));
24339        for li in 0..4 {
24340            let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
24341            c.kv_cache.layers[li].import_wire(&bytes).unwrap();
24342        }
24343        assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
24344        assert_eq!(c.kv_cache.seq_len(), pos);
24345        for step in 0..17 {
24346            let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
24347            let ha = a.forward_layers(&e, pos, None);
24348            let hc = c.forward_layers(&e, pos, None);
24349            assert_eq!(
24350                f32_bits(&ha),
24351                f32_bits(&hc),
24352                "after the hand-off, step {step}"
24353            );
24354            pos += 1;
24355        }
24356        assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
24357    }
24358
24359    /// A synthetic MiMo draft stack for `mimo_test_pipeline` (the SWA
24360    /// geometry of its sliding layers: 2 KV heads, head 8 / V 4).  Widths
24361    /// are per-draft-layer so the MTP collector cannot accidentally borrow
24362    /// the trunk's `intermediate_size`.
24363    fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
24364        let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
24365        let synth = |len: usize, salt: usize| -> Vec<f32> {
24366            (0..len)
24367                .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
24368                .collect()
24369        };
24370        let qt = |rows: usize, cols: usize, salt: usize| {
24371            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
24372        };
24373        let layers = widths
24374            .iter()
24375            .copied()
24376            .enumerate()
24377            .map(|(k, inter)| {
24378                let s = 500 + k * 40;
24379                let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
24380                kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
24381                MtpModule {
24382                    enorm: vec![1.0; hs],
24383                    hnorm: vec![1.0; hs],
24384                    eh_proj: qt(hs, 2 * hs, s),
24385                    layer: LayerWeights {
24386                        input_norm: vec![1.0; hs],
24387                        post_norm: vec![1.0; hs],
24388                        attn_out_norm: None,
24389                        ffn_out_norm: None,
24390                        layer_scale: None,
24391                        attn: AttnKind::Full {
24392                            wq: qt(nh * hd, hs, s + 1),
24393                            wk: qt(nkv * hd, hs, s + 2),
24394                            wv: qt(nkv * vd, hs, s + 3),
24395                            wo: qt(hs, nh * vd, s + 4),
24396                            q_norm: None,
24397                            k_norm: None,
24398                            output_gate: false,
24399                            softplus_gate: None,
24400                            bias: None,
24401                        },
24402                        ffn: FfnKind::Dense(DenseFfn {
24403                            gate_proj: qt(inter, hs, s + 5),
24404                            up_proj: qt(inter, hs, s + 6),
24405                            down_proj: qt(hs, inter, s + 7),
24406                            act: Act::Silu,
24407                            down_t: None,
24408                            segs: Vec::new(),
24409                        }),
24410                    },
24411                    final_norm: vec![1.0; hs],
24412                    kv,
24413                }
24414            })
24415            .collect();
24416        mimo_mtp::MimoMtp::from_layers(layers)
24417    }
24418
24419    fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
24420        mimo_test_mtp_widths(&vec![24; n], gain)
24421    }
24422
24423    fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
24424        p.clear_sequence_state();
24425        p.speculative = spec;
24426        p.ignore_eos = true;
24427        p.sampler_config.temperature = 0.0;
24428        p.generate_from_ids(ids, n, None, None).unwrap()
24429    }
24430
24431    #[test]
24432    fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
24433        let mut p = mimo_test_pipeline();
24434        let widths = [7usize, 13, 29];
24435        assert_ne!(widths[0], p.intermediate_size);
24436        assert_ne!(widths[1], p.intermediate_size);
24437        assert_ne!(widths[2], p.intermediate_size);
24438        p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
24439        let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
24440
24441        let trunk = p.probe_ffn_mass(&ids);
24442        assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
24443        let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
24444        assert_eq!(got.len(), widths.len());
24445        for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
24446            assert_eq!(stat.mass.len(), width, "draft layer {k}");
24447            assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
24448            assert_eq!(stat.observed_positions, ids.len() - k - 1);
24449            assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
24450            assert!(stat
24451                .mass
24452                .iter()
24453                .zip(&stat.squared_mass)
24454                .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
24455        }
24456        assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
24457        let st = p.mimo_mtp.as_ref().unwrap();
24458        assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
24459
24460        // As above, reject only after a MiMo draft KV has been appended.
24461        // The detached multi-layer owner must still be reset on that error.
24462        let h = p.hidden_size;
24463        let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
24464            panic!("synthetic MiMo MTP must be dense")
24465        };
24466        dense.segs.push(FfnSeg {
24467            gate: QTensor::from_f32(vec![0.0; h], 1, h),
24468            up: QTensor::from_f32(vec![0.0; h], 1, h),
24469            down: QTensor::from_f32(vec![0.0; h], h, 1),
24470            start: dense.gate_proj.rows(),
24471            width: 1,
24472        });
24473        assert!(p
24474            .probe_mtp_ffn(&ids)
24475            .unwrap_err()
24476            .contains("tube-segmented draft FFNs are not supported"));
24477        assert_eq!(p.kv_cache.seq_len(), 0);
24478        assert!(p
24479            .mimo_mtp
24480            .as_ref()
24481            .unwrap()
24482            .layers
24483            .iter()
24484            .all(|m| m.kv.seq_len == 0));
24485    }
24486
24487    #[test]
24488    fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
24489        fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
24490            p.clear_sequence_state();
24491            let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
24492            let _exact = ExactMtpCalibrationGuard::enter();
24493            let got = crate::gpu::cpu_scope(|| {
24494                let trunk = p.probe_trunk_rows(ids)?;
24495                p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
24496            })
24497            .expect("teacher-forced MiMo probe");
24498            st.reset();
24499            p.mimo_mtp = Some(st);
24500            p.reset_mtp_probe_state();
24501            got
24502        }
24503
24504        for post_norm_hidden in [false, true] {
24505            let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
24506            let make = || {
24507                let mut p = mimo_test_pipeline();
24508                let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
24509                st.post_norm_hidden = post_norm_hidden;
24510                p.mimo_mtp = Some(st);
24511                p
24512            };
24513            let mut full = make();
24514            let expected = collect(&mut full, &ids, usize::MAX);
24515            for rows in [1usize, 7, 128] {
24516                let mut chunked = make();
24517                let got = collect(&mut chunked, &ids, rows);
24518                assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
24519                for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
24520                    assert_eq!(
24521                        got.observed_positions, expected.observed_positions,
24522                        "post={post_norm_hidden}, rows={rows}, layer={layer}"
24523                    );
24524                    assert_eq!(
24525                        got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24526                        expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24527                        "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
24528                    );
24529                    assert_eq!(
24530                        got.squared_mass
24531                            .iter()
24532                            .map(|v| v.to_bits())
24533                            .collect::<Vec<_>>(),
24534                        expected
24535                            .squared_mass
24536                            .iter()
24537                            .map(|v| v.to_bits())
24538                            .collect::<Vec<_>>(),
24539                        "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
24540                    );
24541                }
24542            }
24543        }
24544    }
24545
24546    /// The draft stack's incremental rounds (a few rows per layer, last
24547    /// round's provisional rows dropped) give exactly the teacher-forced
24548    /// table of one causal pass per layer over the whole sequence — the
24549    /// table `tools/mimo_ref.py mtp` computes for variant A: layer k, row
24550    /// j reads (x[j+k+1], norm(h_j)) at RoPE position j.
24551    #[test]
24552    fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
24553        // Both readings of the backbone hidden: pre-final-norm (default)
24554        // and post-final-norm (`CMF_MIMO_MTP_HIDDEN=post`).
24555        for post in [false, true] {
24556            let mut p = mimo_test_pipeline();
24557            // A non-trivial final norm, so the two readings differ.
24558            p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
24559            let mut st0 = mimo_test_mtp(3, 1.0);
24560            st0.post_norm_hidden = post;
24561            p.mimo_mtp = Some(st0);
24562            let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
24563            let hs = p.hidden_size;
24564            let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24565            p.mimo_note_rows(&hb, 0);
24566            let mut st = p.mimo_mtp.take().unwrap();
24567            // Incremental: one round per t through the decode path (later
24568            // tokens from `ids`, the probe's teacher forcing).
24569            let k = 3;
24570            let mut inc = Vec::new();
24571            for t in 0..ids.len() - k - 1 {
24572                inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
24573            }
24574            // Reference: per layer, ONE batched causal pass over all rows
24575            // with fresh caches.
24576            let s = ids.len();
24577            let mut reference = vec![vec![0u32; k]; s - k - 1];
24578            let mut fresh = mimo_test_mtp(3, 1.0);
24579            for (layer, m) in fresh.layers.iter_mut().enumerate() {
24580                let n = s - layer - 1;
24581                let mut cats = vec![0.0f32; n * 2 * hs];
24582                for j in 0..n {
24583                    let e = p.embed_single(ids[j + layer + 1]);
24584                    let raw = &hb[j * hs..(j + 1) * hs];
24585                    let g = if post {
24586                        inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
24587                    } else {
24588                        raw.to_vec()
24589                    };
24590                    let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
24591                    inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
24592                    inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
24593                }
24594                let mut x = vec![0.0f32; n * hs];
24595                m.eh_proj.matmat(&cats, n, &mut x, None);
24596                p.mimo_mtp_block(m, &mut x, n, 0);
24597                for (t, row) in reference.iter_mut().enumerate() {
24598                    let y = inference::rms_norm(
24599                        &x[t * hs..(t + 1) * hs],
24600                        &m.final_norm,
24601                        p.rms_eps,
24602                        p.norm_style,
24603                    );
24604                    row[layer] = sampler::argmax(&p.lm_head_forward(&y));
24605                }
24606            }
24607            assert_eq!(inc, reference, "post_norm_hidden = {post}");
24608            // Not a degenerate table: the drafts vary.
24609            let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
24610            assert!(distinct.len() > 3, "{inc:?}");
24611            // Each layer's cache ends holding rows up to the last round start.
24612            let last_t = ids.len() - k - 2;
24613            for m in &st.layers {
24614                assert_eq!(m.kv.seq_len, last_t + 1);
24615            }
24616        }
24617    }
24618
24619    /// Greedy with the MiMo draft stack is the plain greedy stream, token
24620    /// for token — with the real draft layers (low acceptance) and with a
24621    /// drafter that is right most of the time (exercises accepted prefixes
24622    /// of every length, the KV truncation of the rejected rows and the
24623    /// logits hand-off to the loop top), under the default repetition
24624    /// penalty.
24625    #[test]
24626    fn mimo_speculative_greedy_equals_plain_greedy() {
24627        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
24628        let _env = ScopedTestEnv::set(&[
24629            ("CMF_GPU", Some("0")),
24630            ("CMF_GPU_WGPU_GRAPH", Some("0")),
24631        ]);
24632        let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
24633        let n = 24;
24634        let mut p = mimo_test_pipeline();
24635        let plain = mimo_greedy(&mut p, &ids, n, false);
24636        assert_eq!(plain.mtp_drafted, 0);
24637        assert_eq!(plain.token_ids.len(), n);
24638        let plain_kv = p.kv_cache.layers[0].seq_len;
24639
24640        // Real draft layers.
24641        p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
24642        let spec = mimo_greedy(&mut p, &ids, n, true);
24643        assert!(spec.mtp_drafted > 0, "the round must draft");
24644        assert_eq!(spec.token_ids, plain.token_ids);
24645        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24646
24647        // A drafter reading the true continuation with every fifth token
24648        // wrong: accepted prefixes of 0..=3 all occur.
24649        let mut truth: Vec<u32> = ids.clone();
24650        truth.extend(&plain.token_ids);
24651        let mut noisy = truth.clone();
24652        for (i, t) in noisy.iter_mut().enumerate() {
24653            if i % 5 == 0 {
24654                *t = (*t + 1) % 64;
24655            }
24656        }
24657        let mut st = mimo_test_mtp(3, 1.0);
24658        st.draft_override = Some(noisy);
24659        p.mimo_mtp = Some(st);
24660        let spec = mimo_greedy(&mut p, &ids, n, true);
24661        assert_eq!(spec.token_ids, plain.token_ids);
24662        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24663        let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
24664        assert_eq!(stats.accepted as usize, spec.mtp_accepted);
24665        assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
24666        assert!(
24667            stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
24668            "{:?}",
24669            stats.accept_hist
24670        );
24671        assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
24672
24673        // A perfect drafter: every draft accepted, rounds of K+1 tokens,
24674        // and the budget is never overrun.
24675        let mut st = mimo_test_mtp(3, 1.0);
24676        st.draft_override = Some(truth);
24677        p.mimo_mtp = Some(st);
24678        let spec = mimo_greedy(&mut p, &ids, n, true);
24679        assert_eq!(spec.token_ids, plain.token_ids);
24680        assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
24681        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24682
24683        // CMF_MTP=0 path: the stack is attached but idle.
24684        let off = mimo_greedy(&mut p, &ids, n, false);
24685        assert_eq!(off.token_ids, plain.token_ids);
24686        assert_eq!(off.mtp_drafted, 0);
24687    }
24688
24689    /// The wgpu graphs carry MiMo-V2's attention per layer (KV heads,
24690    /// narrow V, sinks, windows, two RoPE tables): no attention-level
24691    /// decline for it any more, and the geometry each layer hands the
24692    /// graph is exactly what the CPU attention reads for that layer. The
24693    /// descriptive reasons stay (the Metal graphs and the q1 dropin still
24694    /// decline on them), and what the per-layer geometry cannot express
24695    /// keeps a named wgpu decline.
24696
24697    #[test]
24698    fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
24699        let p = mimo_test_pipeline();
24700        assert_eq!(
24701            p.graph_attn_decline_reason(),
24702            Some("per-layer KV head counts")
24703        );
24704        assert_eq!(p.wgpu_graph_attn_decline(), None);
24705        let g0 = p.graph_attn_geom(0).expect("full layer geometry");
24706        assert_eq!(
24707            (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
24708            (1, 4, 4, None, false)
24709        );
24710        assert_eq!(g0.invf, p.inv_freq.as_slice());
24711        let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
24712        assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
24713        assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
24714        assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
24715        assert_ne!(g0.invf, g1.invf, "two RoPE tables");
24716        let g3 = p.graph_attn_geom(3).expect("full layer geometry");
24717        assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
24718
24719        // No wgpu device in this process: the builders run and decline on
24720        // the (f32, unmapped) experts — never with an attention line.
24721        let emb = p.embed_single(3);
24722        let mut lg = Vec::new();
24723        assert!(
24724            p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
24725                .is_none()
24726        );
24727        let mut hid = emb.clone();
24728        assert_eq!(
24729            p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
24730            crate::gpu::BatchGraphOutcome::Declined
24731        );
24732        assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
24733        assert!(p.try_multi_burst(3, 0, 4).is_none());
24734        assert!(
24735            p.graph_declines().is_empty(),
24736            "no attention decline logged: {:?}",
24737            p.graph_declines()
24738        );
24739        // (No assertion on graph_prefill_preferred: with no attention
24740        // decline it follows the device — a test process that brought a
24741        // wgpu adapter up routes this resident MoE through the graph.)
24742
24743        let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
24744        assert_eq!(plain().graph_attn_decline_reason(), None);
24745        assert_eq!(plain().wgpu_graph_attn_decline(), None);
24746        assert!(
24747            plain().graph_attn_geom(0).is_none(),
24748            "uniform models keep the historical arms"
24749        );
24750        let mut q = plain();
24751        q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
24752        assert_eq!(
24753            q.graph_attn_decline_reason(),
24754            Some("learned attention sinks")
24755        );
24756        assert_eq!(
24757            q.graph_attn_geom(1).unwrap().sink,
24758            Some(&[0.25f32, -0.25][..])
24759        );
24760        let mut q = plain();
24761        q.set_attn_geometry(None, Some(2)).unwrap();
24762        assert_eq!(
24763            q.graph_attn_decline_reason(),
24764            Some("V heads narrower than Q/K heads")
24765        );
24766        assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
24767        let mut q = plain();
24768        q.sliding_layers = Some(vec![true, false]);
24769        q.swa = Some((4, usize::MAX));
24770        assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
24771        assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
24772        assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
24773
24774        // YaRN's post-rotation amplitude factor rides the per-layer graph
24775        // geometry. It is intentionally not an angular scale folded into
24776        // `invf`: the WGPU RoPE kernel receives it separately.
24777        let mut q = mimo_test_pipeline();
24778        q.rope_scale = 2.0;
24779        q.rope_scale_local = 1.25;
24780        assert_eq!(q.wgpu_graph_attn_decline(), None);
24781        assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
24782        assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
24783
24784        // A uniform YaRN model cannot yet pass the factor through the fused
24785        // RKV arm; decline safely instead of producing scale-1 output.
24786        let mut q = plain();
24787        q.rope_scale = 2.0;
24788        assert_eq!(
24789            q.wgpu_graph_attn_decline(),
24790            Some("scaled RoPE without per-layer geometry")
24791        );
24792    }
24793
24794    /// The native q1 attention path has a narrower contract than the WGPU
24795    /// per-layer geometry graph, but it must not discard the exact Flint
24796    /// combination of uniform GQA + sliding windows + learned sinks.  The
24797    /// actual Metal kernel is covered independently with a direct sink
24798    /// softmax test; this is the pure admission test, with no model run.
24799    #[cfg(target_os = "macos")]
24800    #[test]
24801    fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
24802        // rd=64 is in the native rope kernel's lane-pair contract; the
24803        // synthetic stack otherwise has the same uniform K/V geometry that
24804        // Flint needs for q1 (unlike the deliberately ragged MiMo fixture).
24805        let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
24806        p.swa = Some((16, usize::MAX));
24807        p.sliding_layers = Some(vec![true, true]);
24808        p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
24809        p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
24810
24811        assert_eq!(
24812            p.graph_attn_decline_reason(),
24813            Some("learned attention sinks")
24814        );
24815        let swa_graph = p.metal_graph_swa();
24816        assert!(swa_graph, "uniform sliding sink geometry must reach q1");
24817        let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
24818            && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
24819        assert!(
24820            native_sink_attend,
24821            "the q1 sink admission must follow the SWA door"
24822        );
24823        // The batched chunk softmax has no value-less sink column, so its
24824        // separate admission gate must keep this prompt on the host path.
24825        assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
24826    }
24827
24828    #[test]
24829    fn mimo_verify_rewind_preserves_lagging_host_caches() {
24830        let mut p = mimo_test_pipeline();
24831        for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
24832            let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
24833            for _ in 0..if li == 0 { 2 } else { 12 } {
24834                layer.append(&row, &row, &[]);
24835            }
24836        }
24837        p.mimo_verify_rewind(9).unwrap();
24838        assert_eq!(p.kv_cache.layers[0].seq_len, 2);
24839        for layer in &p.kv_cache.layers[1..] {
24840            assert_eq!(layer.seq_len, 9);
24841        }
24842    }
24843
24844    /// CMF_LAYER_DUMP: the decode walk and the batched prefill both write
24845    /// every (position, layer) hidden, the two sets agree byte for byte,
24846    /// and the last layer's file is the stack output.
24847    #[test]
24848    fn layer_dump_covers_every_position_and_layer_on_both_walks() {
24849        let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
24850        let _ = std::fs::remove_dir_all(&dir);
24851        let mut p = mimo_test_pipeline();
24852        let hs = p.hidden_size;
24853        let ids = [5u32, 9, 11, 2, 40];
24854        p.layer_dump = Some(dir.join("decode"));
24855        for (pos, &id) in ids.iter().enumerate() {
24856            let e = p.embed_single(id);
24857            let _ = p.forward_layers(&e, pos, None);
24858        }
24859        p.clear_sequence_state();
24860        p.layer_dump = Some(dir.join("prefill"));
24861        let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24862        for pos in 0..ids.len() {
24863            for li in 0..p.num_layers {
24864                let name = format!("p{pos:06}_l{li:02}.f32");
24865                let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
24866                let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
24867                assert_eq!(a.len(), hs * 4, "{name}");
24868                assert_eq!(a, b, "{name}");
24869            }
24870        }
24871        let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
24872        let vals: Vec<f32> = last
24873            .chunks(4)
24874            .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
24875            .collect();
24876        assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
24877        let _ = std::fs::remove_dir_all(&dir);
24878    }
24879
24880    #[test]
24881    fn attn_geometry_and_sinks_are_validated() {
24882        let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
24883        assert!(
24884            p.set_attn_geometry(Some(vec![2]), None).is_err(),
24885            "one entry per layer"
24886        );
24887        assert!(
24888            p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
24889            "3 does not divide 4"
24890        );
24891        assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
24892        assert!(p.set_attn_geometry(None, Some(0)).is_err());
24893        assert!(
24894            p.set_attn_geometry(None, Some(5)).is_err(),
24895            "V wider than the head"
24896        );
24897        p.set_attn_geometry(None, Some(4)).unwrap();
24898        assert_eq!(
24899            p.v_head_dim, None,
24900            "v_head_dim == head_dim is the uniform case"
24901        );
24902        p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
24903        p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
24904        assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
24905        assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
24906        assert!(
24907            p.kv_cache.layers[1].sinks.is_some(),
24908            "a reshape keeps the layer's sinks"
24909        );
24910        assert_eq!(p.layer_geom(1).0, 4);
24911        assert!(
24912            p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
24913            "one sink per Q head"
24914        );
24915        assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
24916        assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
24917    }
24918
24919    /// The O(1) Nyström state replaces a plain full-context softmax; it
24920    /// must never be armed on a sliding, sink or narrow-V layer.
24921    #[test]
24922    fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
24923        let cfg = || {
24924            Some(crate::nystrom::O1Cfg {
24925                layers: crate::nystrom::O1Layers::All,
24926                m: 4,
24927                w: 8,
24928                sink: 2,
24929                rect: crate::nystrom::O1Rect::Aggregate,
24930            })
24931        };
24932        let mut p = mimo_test_pipeline();
24933        p.set_o1(cfg());
24934        assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
24935        let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
24936        q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
24937        q.sliding_layers = Some(vec![false, false, true]);
24938        q.swa = Some((4, usize::MAX));
24939        q.set_o1(cfg());
24940        assert_eq!(q.o1_flags, vec![true, false, false]);
24941    }
24942
24943    #[test]
24944    fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
24945        const B: usize = 19;
24946        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24947        p.set_o1(Some(crate::nystrom::O1Cfg {
24948            layers: crate::nystrom::O1Layers::All,
24949            m: 4,
24950            w: 8,
24951            sink: 2,
24952            rect: crate::nystrom::O1Rect::Aggregate,
24953        }));
24954        p.o1_begin_with_prefix(Some(B));
24955        let ids: Vec<u32> = (0..B as u32).collect();
24956        let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24957
24958        assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
24959        assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
24960        let next = p.embed_single(B as u32);
24961        let _ = p.forward_layers(&next, B, None);
24962        assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
24963    }
24964
24965    #[test]
24966    fn o1_pair_transition_commits_scratch_before_epoch_publication() {
24967        const B: usize = 19;
24968        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24969        // Keep a real recurrent layer ahead of the Full O(1) layer so the
24970        // pair test observes the GDN lane-2 scratch swap at the same
24971        // boundary, rather than only exercising an artificial scratch vec.
24972        let gdn_cfg = crate::linear_core::GdnCfg {
24973            num_v_heads: 2,
24974            num_k_heads: 1,
24975            key_head_dim: 2,
24976            value_head_dim: 4,
24977            conv_kernel: 3,
24978            hidden_size: 8,
24979            rms_eps: 1e-6,
24980            output_gate_sigmoid: false,
24981        };
24982        let synth = |n: usize, salt: usize| -> Vec<f32> {
24983            (0..n)
24984                .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
24985                .collect()
24986        };
24987        let qt = |rows: usize, cols: usize, salt: usize| {
24988            crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
24989        };
24990        let c_dim = gdn_cfg.conv_dim();
24991        let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
24992        p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
24993            in_proj_qkv: qt(c_dim, 8, 1),
24994            in_proj_z: qt(vd, 8, 2),
24995            in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
24996            in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
24997            conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
24998            a_log: vec![0.2, 0.5],
24999            dt_bias: synth(gdn_cfg.num_v_heads, 6),
25000            norm: vec![1.0; gdn_cfg.value_head_dim],
25001            out_proj: qt(8, vd, 7),
25002        });
25003        p.gdn_cfg = Some(gdn_cfg);
25004        p.set_o1(Some(crate::nystrom::O1Cfg {
25005            layers: crate::nystrom::O1Layers::All,
25006            m: 4,
25007            w: 8,
25008            sink: 2,
25009            rect: crate::nystrom::O1Rect::Aggregate,
25010        }));
25011        p.o1_begin_with_prefix(Some(B));
25012        for pos in 0..B - 2 {
25013            let emb = p.embed_single(pos as u32);
25014            let _ = p.forward_layers(&emb, pos, None);
25015        }
25016        let lane1_state = p.kv_cache.layers[0].linear_state.clone();
25017
25018        let e1 = p.embed_single((B - 2) as u32);
25019        let e2 = p.embed_single((B - 1) as u32);
25020        let _ = p.forward_pair(&e1, &e2, B - 2);
25021
25022        assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
25023        assert!(
25024            p.kv_cache
25025                .layers
25026                .iter()
25027                .enumerate()
25028                .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
25029        );
25030        assert!(!p.kv_cache.layers[0].linear_state.is_empty());
25031        assert_ne!(
25032            p.kv_cache.layers[0].linear_state, lane1_state,
25033            "real pair must commit GDN lane 2 before returning"
25034        );
25035        assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
25036        let next = p.embed_single(B as u32);
25037        let _ = p.forward_layers(&next, B, None);
25038        assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
25039    }
25040
25041    #[test]
25042    fn o1_error_observation_stays_terminal_until_reset() {
25043        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25044        p.set_o1(Some(crate::nystrom::O1Cfg {
25045            layers: crate::nystrom::O1Layers::All,
25046            m: 4,
25047            w: 8,
25048            sink: 2,
25049            rect: crate::nystrom::O1Rect::Aggregate,
25050        }));
25051        p.o1_begin();
25052        p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
25053
25054        assert!(p.o1_seal_checked().is_err());
25055        assert!(
25056            p.o1_seal_checked().is_err(),
25057            "retry must see the sticky error"
25058        );
25059        let k = vec![0.2f32; 4];
25060        let v = vec![0.3f32; 4];
25061        p.kv_cache.layers[0].append(&k, &v, &[]);
25062        assert_eq!(p.kv_cache.layers[0].seq_len, 0);
25063
25064        p.reset_session();
25065        p.o1_begin();
25066        p.kv_cache.layers[0].append(&k, &v, &[]);
25067        assert_eq!(p.kv_cache.layers[0].seq_len, 1);
25068    }
25069
25070    #[test]
25071    fn nll_graph_failure_is_terminal_and_request_is_reusable() {
25072        let ids = vec![1u32, 2, 3, 4, 5, 6];
25073        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25074        p.graph_logits = Some(vec![123.0]);
25075        p.graph_want_logits = true;
25076        p.graph_failed
25077            .store(true, std::sync::atomic::Ordering::Relaxed);
25078        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
25079        let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
25080        assert!(err.contains("before NLL"));
25081        assert!(p.graph_logits.is_none());
25082        assert!(!p.graph_want_logits);
25083        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25084        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25085
25086        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25087        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
25088        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
25089        assert_eq!(actual.1, expected.1);
25090        assert!((actual.0 - expected.0).abs() < 1e-9);
25091    }
25092
25093    #[test]
25094    fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
25095        let ids = vec![1u32, 2, 3, 4, 5, 6];
25096        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25097        p.nll_test_fail_at = Some(1);
25098        let err = p
25099            .nll_ids_from(&ids, 0)
25100            .expect_err("one-shot forward failure");
25101        assert!(err.contains("forward") || err.contains("score row"));
25102        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25103        assert!(!p.graph_want_logits);
25104        assert!(p.graph_logits.is_none());
25105        assert!(p.kv_history.is_empty());
25106
25107        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25108        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
25109        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
25110        assert_eq!(actual.1, expected.1);
25111        assert!((actual.0 - expected.0).abs() < 1e-9);
25112    }
25113
25114    #[test]
25115    fn nll_serial_failure_before_first_row_is_reported() {
25116        let ids = vec![1u32, 2, 3, 4];
25117        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25118        p.nll_test_force_serial = true;
25119        p.nll_test_fail_at = Some(0);
25120        let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
25121        assert!(err.contains("serial forward"));
25122        assert!(p.kv_history.is_empty());
25123        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25124        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25125    }
25126
25127    #[test]
25128    fn ffn_probe_failure_discards_recorder_and_state() {
25129        let ids = vec![1u32, 2, 3, 4];
25130        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25131        p.nll_test_fail_at = Some(0);
25132        let err = p
25133            .probe_ffn_mass_batch(&ids)
25134            .expect_err("probe forward failure");
25135        assert!(err.contains("NLL"));
25136        assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
25137        assert!(p.kv_history.is_empty());
25138        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25139    }
25140
25141    #[test]
25142    fn nll_test_controls_are_pipeline_scoped() {
25143        let ids = vec![1u32, 2, 3, 4];
25144        let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25145        let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25146        failing.nll_test_force_serial = true;
25147        failing.nll_test_fail_at = Some(0);
25148
25149        assert!(!failing.can_prefill_batched());
25150        assert!(unaffected.can_prefill_batched());
25151        let expected = unaffected
25152            .nll_ids_from(&ids, 0)
25153            .expect("unaffected pipeline remains usable");
25154        let err = failing
25155            .nll_ids_from(&ids, 0)
25156            .expect_err("failure injection belongs to failing pipeline");
25157        assert!(err.contains("serial forward"));
25158        assert!(failing.nll_test_fail_at.is_none());
25159        assert!(unaffected.can_prefill_batched());
25160        let actual = unaffected
25161            .nll_ids_from(&ids, 0)
25162            .expect("unaffected pipeline remains reusable");
25163        assert_eq!(actual.1, expected.1);
25164        assert!((actual.0 - expected.0).abs() < 1e-9);
25165    }
25166
25167    #[test]
25168    fn forward_ids_failure_channel_is_terminal_and_reusable() {
25169        let ids = vec![1u32, 2, 3, 4, 5, 6];
25170        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25171        p.graph_logits = Some(vec![123.0]);
25172        p.graph_want_logits = true;
25173        p.graph_failed
25174            .store(true, std::sync::atomic::Ordering::Relaxed);
25175        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
25176
25177        let err = p
25178            .forward_ids(&ids, None)
25179            .expect_err("a failed forward must not become a valid head result");
25180        assert!(err.contains("forward_ids setup"));
25181        assert!(p.graph_logits.is_none());
25182        assert!(!p.graph_want_logits);
25183        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25184        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25185        assert_eq!(p.kv_cache.seq_len(), 0);
25186
25187        let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
25188            .forward_ids(&ids, None)
25189            .expect("fresh forward_ids");
25190        let actual = p
25191            .forward_ids(&ids, None)
25192            .expect("pipeline remains reusable after a failed forward");
25193        assert_eq!(actual.len(), expected.len());
25194        assert!(
25195            actual
25196                .iter()
25197                .zip(expected)
25198                .all(|(a, b)| (a - b).abs() < 1e-9)
25199        );
25200        assert_eq!(p.kv_cache.seq_len(), ids.len());
25201    }
25202
25203    #[test]
25204    fn sigmoid_router_floor_is_explicit_per_architecture() {
25205        // Flint and MiniMax M3.1 reference runtimes use +1e-20, while the
25206        // generic LFM2-compatible path uses +1e-6. At low (but representable)
25207        // sigmoid scores, silently sharing the latter changes expert weights by
25208        // orders of magnitude and can make a routed layer look coherent while
25209        // discarding its expert contribution.
25210        let zero = || DenseFfn {
25211            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25212            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25213            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25214            act: Act::Silu,
25215            down_t: None,
25216            segs: Vec::new(),
25217        };
25218        let m = MoeFfn {
25219            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25220            experts: vec![zero(), zero()],
25221            top_k: 1,
25222            norm_topk_prob: true,
25223            router_sigmoid: true,
25224            sigmoid_denom_eps: 1e-20,
25225            expert_bias: None,
25226            routed_scaling: 2.5,
25227            route_tau: None,
25228            shared: None,
25229            stats: std::cell::RefCell::new(Vec::new()),
25230            act_sq: std::cell::RefCell::new(Vec::new()),
25231            act_rows: std::cell::RefCell::new(Vec::new()),
25232            mask: None,
25233            per_expert_scale: None,
25234            router_input_norm: false,
25235            resonance: None,
25236            grown: Vec::new(),
25237        };
25238        let logits = [-20.0f32, -20.0];
25239        let (_, p, flint_wsum) = moe_route(&logits, &m, None);
25240        let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25241        let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
25242        let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
25243        assert!((flint_wsum - reference_expected).abs() < 1e-15);
25244        assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
25245        assert!(legacy_wsum > minimax_wsum * 100.0);
25246    }
25247
25248    #[test]
25249    fn resonance_scores_match_formula_and_stable_tie() {
25250        let r = Resonance {
25251            // Three descriptors, hidden=2, one projection row each.
25252            mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
25253            u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
25254            k: 1,
25255            bias: vec![1.5, 0.5, 0.0],
25256            shell: Vec::new(),
25257        };
25258        let x = [1.0f32, 1.0];
25259        let mut got = vec![0.0; 3];
25260        r.scores(&x, &mut got);
25261        // Expert 0 and 1 are an exact score tie; the CPU top-1 contract uses
25262        // the lower index.  The values also check d² - (U·d)², not just tie
25263        // ordering.
25264        assert!((got[0] - 0.5).abs() < 1e-6);
25265        assert!((got[1] - 0.5).abs() < 1e-6);
25266        assert!(got[2].abs() < 1e-6);
25267        let best = got
25268            .iter()
25269            .enumerate()
25270            .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
25271            .map(|(i, _)| i);
25272        assert_eq!(best, Some(0));
25273        assert!(got.iter().all(|v| v.is_finite()));
25274    }
25275
25276    /// The growth shell (spec §2): a grown expert whose reconstruction
25277    /// error lies outside its shell scores −∞, one inside keeps the exact
25278    /// resonance score, trunk rows (`+inf` shell) are bit-identical to the
25279    /// shell-less computation; the process-wide switch disables it.
25280    #[test]
25281    fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
25282        // hidden = 2, rank 1. Experts 0/1 = trunk (shell +inf); 2 and 3 =
25283        // grown, the same descriptor (μ = (0, 1), u = (1, 1)) with shells
25284        // 6.0 and 0.25. At x' = (3, 0): d = (3, −1), d² = 10, proj =
25285        // (3 − 1)² = 4, err = 6 exactly — on the boundary of expert 2's
25286        // shell (kept: the rule is strict `>`), outside expert 3's.
25287        let plain = Resonance {
25288            mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
25289            u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
25290            k: 1,
25291            bias: vec![1.5, 0.5, 0.0, 0.0],
25292            shell: Vec::new(),
25293        };
25294        let shelled = Resonance {
25295            mu: plain.mu.clone(),
25296            u: plain.u.clone(),
25297            k: 1,
25298            bias: plain.bias.clone(),
25299            shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
25300        };
25301        assert!(!plain.has_shell());
25302        assert!(shelled.has_shell());
25303        let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
25304        let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
25305        set_growth_shell(Some(true));
25306        assert!(growth_shell_enabled());
25307        // x = (1, 1): the grown experts reconstruct it exactly (err 0):
25308        // inside both shells, every row the shell-less bits.
25309        let x = [1.0f32, 1.0];
25310        plain.scores(&x, &mut a);
25311        shelled.scores(&x, &mut b);
25312        assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
25313        assert!(a[2] == 0.0 && a[3] == 0.0);
25314        // x' = (3, 0): expert 3 → −∞, expert 2 (err == shell) and the
25315        // trunk rows keep their exact bits.
25316        let xo = [3.0f32, 0.0];
25317        plain.scores(&xo, &mut a);
25318        shelled.scores(&xo, &mut b);
25319        assert_eq!(a[2], -6.0);
25320        assert_eq!(a[3], -6.0);
25321        assert_eq!(
25322            bits(&a[..3]),
25323            bits(&b[..3]),
25324            "trunk rows + the boundary row"
25325        );
25326        assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
25327        assert_eq!(shelled.effective_shell(4), shelled.shell);
25328        // The switch (`CMF_GROWTH_SHELL=off` / `growth-eval --shell off`):
25329        // all +inf, the shell-less bits everywhere.
25330        set_growth_shell(Some(false));
25331        assert!(!growth_shell_enabled());
25332        assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
25333        shelled.scores(&xo, &mut b);
25334        assert_eq!(bits(&a), bits(&b));
25335        set_growth_shell(None);
25336        // A shell vector shorter than the expert count masks nothing
25337        // beyond it (a legacy layer whose tail has no shell).
25338        let short = Resonance {
25339            shell: vec![f32::INFINITY, f32::INFINITY],
25340            ..shelled
25341        };
25342        set_growth_shell(Some(true));
25343        short.scores(&xo, &mut b);
25344        assert_eq!(bits(&a), bits(&b));
25345        set_growth_shell(None);
25346    }
25347
25348    /// `moe_route` with −∞ logits (a grown expert outside its shell):
25349    /// top-1 is the best finite expert with weight exactly 1.0 on both
25350    /// the softmax and the sigmoid path; all −∞ degrades to uniform.
25351    #[test]
25352    fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
25353        let zero = || DenseFfn {
25354            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25355            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25356            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25357            act: Act::Silu,
25358            down_t: None,
25359            segs: Vec::new(),
25360        };
25361        let moe = |sigmoid: bool| MoeFfn {
25362            router: QTensor::from_f32(vec![0.0; 8], 4, 2),
25363            experts: vec![zero(), zero(), zero(), zero()],
25364            top_k: 1,
25365            norm_topk_prob: true,
25366            router_sigmoid: sigmoid,
25367            sigmoid_denom_eps: 1e-6,
25368            expert_bias: None,
25369            routed_scaling: 1.0,
25370            route_tau: None,
25371            shared: None,
25372            stats: std::cell::RefCell::new(Vec::new()),
25373            act_sq: std::cell::RefCell::new(Vec::new()),
25374            act_rows: std::cell::RefCell::new(Vec::new()),
25375            mask: None,
25376            per_expert_scale: None,
25377            router_input_norm: false,
25378            resonance: None,
25379            grown: Vec::new(),
25380        };
25381        let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
25382        for sigmoid in [false, true] {
25383            let m = moe(sigmoid);
25384            let (idx, p, wsum) = moe_route(&logits, &m, None);
25385            assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
25386            assert_eq!(p[1], 0.0);
25387            assert_eq!(p[3], 0.0);
25388            assert!(p[2] > p[0] && p[0] > 0.0);
25389            assert!(p.iter().all(|v| v.is_finite()));
25390            let w = p[2] / wsum;
25391            if sigmoid {
25392                // The sigmoid renorm keeps its reference floor (+1e-6).
25393                assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
25394            } else {
25395                assert_eq!(
25396                    w, 1.0,
25397                    "softmax: the renormalized top-1 weight is exactly 1"
25398                );
25399            }
25400            // Masked experts stay masked even when they are the only ones
25401            // "admitted" by an allow-list that covers everything.
25402            let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
25403            assert_eq!(idx, vec![2]);
25404        }
25405        // A finite expert always beats −∞ whatever the bias / order.
25406        let m = moe(false);
25407        let (idx, _, _) = moe_route(
25408            &[
25409                f32::NEG_INFINITY,
25410                f32::NEG_INFINITY,
25411                f32::NEG_INFINITY,
25412                -9.0,
25413            ],
25414            &m,
25415            None,
25416        );
25417        assert_eq!(idx, vec![3]);
25418        // Every expert at −∞ (cannot happen on a grown file — trunk rows
25419        // have no shell): uniform, finite, lowest index.
25420        let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
25421        assert_eq!(idx, vec![0]);
25422        assert!(p.iter().all(|&v| v == 0.25));
25423        assert!(wsum.is_finite() && wsum > 0.0);
25424    }
25425
25426    /// MiniMax-M3.1's DEVICE route (the pooled token-graph arm's exact
25427    /// flags: sigmoid scoring via n's high bit, +1e-20 floor,
25428    /// routed_scaling 2.0, selection bias on the CHOICE only) must match
25429    /// the host reference in experts, ORDER and weights — the check that
25430    /// caught the flags/slot collision.
25431    #[cfg(feature = "gpu")]
25432    #[test]
25433    fn minimax_device_route_matches_host_sigmoid_contract() {
25434        match crate::gpu_wgpu::selected_and_up() {
25435            None => {
25436                crate::gpu_wgpu::skip_or_fail(module_path!());
25437                return;
25438            }
25439            // Sibling unit tests in THIS process select wgpu by writing
25440            // `CMF_GPU` (the gpu_wgpu test module does it ~70 times), so on a
25441            // headless runner the variable is already set by the time this
25442            // test reads it — "requested" here is test pollution, not an
25443            // operator's ask, and the adapter genuinely does not exist in
25444            // this process. Same treatment as the gpu_wgpu tests themselves:
25445            // a visible skip, never a silent pass.
25446            Some(false) => {
25447                eprintln!(
25448                    "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25449                    module_path!()
25450                );
25451                return;
25452            }
25453            Some(true) => {}
25454        }
25455        let n = 128usize;
25456        let top_k = 4usize;
25457        let logits: Vec<f32> = (0..n)
25458            .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25459            .collect();
25460        let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25461        let m = MoeFfn {
25462            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25463            experts: Vec::new(),
25464            top_k,
25465            norm_topk_prob: true,
25466            router_sigmoid: true,
25467            sigmoid_denom_eps: 1e-20,
25468            expert_bias: Some(bias.clone()),
25469            routed_scaling: 2.0,
25470            route_tau: None,
25471            shared: None,
25472            stats: std::cell::RefCell::new(Vec::new()),
25473            act_sq: std::cell::RefCell::new(Vec::new()),
25474            act_rows: std::cell::RefCell::new(Vec::new()),
25475            mask: None,
25476            per_expert_scale: None,
25477            router_input_norm: false,
25478            resonance: None,
25479            grown: Vec::new(),
25480        };
25481        let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25482        let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
25483
25484        for shared in [Some(n as u32), None] {
25485            let mut gi = Vec::new();
25486            let mut gw = Vec::new();
25487            assert!(
25488                crate::gpu_wgpu::minimax_moe_route_for_test(
25489                    &logits,
25490                    Some(&bias),
25491                    top_k,
25492                    2.0,
25493                    shared,
25494                    &mut gi,
25495                    &mut gw,
25496                ),
25497                "the device did not come up — the test checked nothing"
25498            );
25499            let routed = if shared.is_some() { top_k } else { gi.len() };
25500            assert_eq!(
25501                &gi[..routed],
25502                &ci[..],
25503                "different experts or order (shared={shared:?})"
25504            );
25505            let d = cw
25506                .iter()
25507                .zip(&gw[..routed])
25508                .map(|(a, b)| (a - b).abs())
25509                .fold(0.0f32, f32::max);
25510            println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
25511            assert!(d < 1e-6, "the weights diverged by {d:.3e}");
25512            if let Some(s) = shared {
25513                assert_eq!(gi[top_k], s as usize, "shared pin slot");
25514                assert_eq!(gw[top_k], 1.0, "shared weight");
25515            }
25516        }
25517    }
25518
25519    /// The pooled arm's COLD hand-back: `(expert, weight)` pairs for the
25520    /// picks the bank could not serve must carry the host's EXACT final
25521    /// weights — the device ships its resident sum, the host adds this on
25522    /// top of the same residual, so a weight drift compounds per layer.
25523    #[cfg(feature = "gpu")]
25524    #[test]
25525    fn minimax_device_cold_weights_match_host_contract() {
25526        match crate::gpu_wgpu::selected_and_up() {
25527            None => {
25528                crate::gpu_wgpu::skip_or_fail(module_path!());
25529                return;
25530            }
25531            // Sibling unit tests in THIS process select wgpu by writing
25532            // `CMF_GPU` (the gpu_wgpu test module does it ~70 times), so on a
25533            // headless runner the variable is already set by the time this
25534            // test reads it — "requested" here is test pollution, not an
25535            // operator's ask, and the adapter genuinely does not exist in
25536            // this process. Same treatment as the gpu_wgpu tests themselves:
25537            // a visible skip, never a silent pass.
25538            Some(false) => {
25539                eprintln!(
25540                    "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25541                    module_path!()
25542                );
25543                return;
25544            }
25545            Some(true) => {}
25546        }
25547        let n = 128usize;
25548        let top_k = 4usize;
25549        let logits: Vec<f32> = (0..n)
25550            .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25551            .collect();
25552        let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25553        let m = MoeFfn {
25554            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25555            experts: Vec::new(),
25556            top_k,
25557            norm_topk_prob: true,
25558            router_sigmoid: true,
25559            sigmoid_denom_eps: 1e-20,
25560            expert_bias: Some(bias.clone()),
25561            routed_scaling: 2.0,
25562            route_tau: None,
25563            shared: None,
25564            stats: std::cell::RefCell::new(Vec::new()),
25565            act_sq: std::cell::RefCell::new(Vec::new()),
25566            act_rows: std::cell::RefCell::new(Vec::new()),
25567            mask: None,
25568            per_expert_scale: None,
25569            router_input_norm: false,
25570            resonance: None,
25571            grown: Vec::new(),
25572        };
25573        let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25574        assert_eq!(ci.len(), top_k);
25575
25576        // Every pick resident → no cold pairs at all.
25577        let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
25578        let mut cold = Vec::new();
25579        let mut idx = Vec::new();
25580        let mut dw = Vec::new();
25581        assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25582            &logits,
25583            Some(&bias),
25584            top_k,
25585            2.0,
25586            &resident,
25587            n as u32,
25588            &mut cold,
25589            &mut idx,
25590            &mut dw,
25591        ));
25592        assert!(cold.is_empty(), "fully resident bank: {cold:?}");
25593        // With subset+pin together: routed slots carry the FLAT remap value
25594        // (here identity = the expert id), weights the host contract, and
25595        // the pinned shared keeps weight 1 — this exact combination is what
25596        // the pooled arm runs with.
25597        for (r, &e) in ci.iter().enumerate() {
25598            assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
25599            let host = p[e] / wsum;
25600            let d = (dw[r] - host).abs();
25601            assert!(
25602                d <= 1e-6 * host.abs().max(1.0),
25603                "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
25604                dw[r]
25605            );
25606        }
25607        assert_eq!(idx[top_k] as usize, n, "shared pin slot");
25608        assert_eq!(dw[top_k], 1.0, "shared weight");
25609
25610        // Make exactly two of the CHOSEN picks cold.
25611        let mut remap = resident.clone();
25612        remap[ci[0]] = u32::MAX;
25613        remap[ci[2]] = u32::MAX;
25614        let mut cold = Vec::new();
25615        let mut idx2 = Vec::new();
25616        let mut dw2 = Vec::new();
25617        assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25618            &logits,
25619            Some(&bias),
25620            top_k,
25621            2.0,
25622            &remap,
25623            n as u32,
25624            &mut cold,
25625            &mut idx2,
25626            &mut dw2,
25627        ));
25628        // The resident picks of the partial bank still carry exact weights.
25629        // A COLD rank is a placeholder the device writes as slot 0 / weight
25630        // 0.0 — its real (id, weight) pair rides in `cold` — so weight zero
25631        // is what marks it, not the slot value: reading the placeholder as
25632        // a resident expert compared 0.0 against p[0]/wsum and failed with
25633        // Δ=2.552e-1. The test logits keep every sigmoid score strictly
25634        // positive, so a resident pick can never weigh exactly zero.
25635        let mut resident_weight_checked = 0usize;
25636        for (r, &slot) in idx2.iter().enumerate().take(top_k) {
25637            if dw2[r] == 0.0 {
25638                continue; // cold rank: placeholder, not an expert id
25639            }
25640            let e = slot as usize;
25641            assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
25642            let host = p[e] / wsum;
25643            let d = (dw2[r] - host).abs();
25644            assert!(
25645                d <= 1e-6 * host.abs().max(1.0),
25646                "partial-bank resident weight expert {e}: Δ={d:.3e}"
25647            );
25648            resident_weight_checked += 1;
25649        }
25650        assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
25651        println!("partial bank: checked {resident_weight_checked} resident weights");
25652        assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
25653        let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
25654        ids.sort_unstable();
25655        let mut want = vec![ci[0], ci[2]];
25656        want.sort_unstable();
25657        assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
25658        for &(e, w) in &cold {
25659            let host = p[e] / wsum;
25660            let d = (w - host).abs();
25661            println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
25662            assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
25663        }
25664    }
25665
25666    /// The resonance router (top-1) selects by the raw score as the
25667    /// trainer and the graph do — not by softmax probabilities, where two
25668    /// scores closer than 2^-25 collapse to the same `exp(l − max) = 1.0`
25669    /// and the LOWER index wins a token whose score is strictly smaller.
25670    #[test]
25671    fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
25672        let zero = || DenseFfn {
25673            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25674            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25675            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25676            act: Act::Silu,
25677            down_t: None,
25678            segs: Vec::new(),
25679        };
25680        let moe = |resonant: bool, norm_topk: bool| MoeFfn {
25681            router: QTensor::from_f32(vec![0.0; 6], 3, 2),
25682            experts: vec![zero(), zero(), zero()],
25683            top_k: 1,
25684            norm_topk_prob: norm_topk,
25685            router_sigmoid: false,
25686            sigmoid_denom_eps: 1e-6,
25687            expert_bias: None,
25688            routed_scaling: 1.0,
25689            route_tau: None,
25690            shared: None,
25691            stats: std::cell::RefCell::new(Vec::new()),
25692            act_sq: std::cell::RefCell::new(Vec::new()),
25693            act_rows: std::cell::RefCell::new(Vec::new()),
25694            mask: None,
25695            per_expert_scale: None,
25696            router_input_norm: false,
25697            resonance: resonant.then(|| Resonance {
25698                mu: vec![0.0; 6],
25699                u: Vec::new(),
25700                k: 0,
25701                bias: vec![0.0; 3],
25702                shell: Vec::new(),
25703            }),
25704            grown: Vec::new(),
25705        };
25706        // lo = −0.1, hi = the next f32 towards zero: hi − lo = 2^-27 <
25707        // 2^-25, so exp(lo − hi) rounds to exactly 1.0 — a softmax tie.
25708        let lo = -0.1f32;
25709        let hi = f32::from_bits(lo.to_bits() - 1);
25710        assert!(hi > lo && hi - lo < 2f32.powi(-25));
25711        assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
25712        // The gated MoE (softmax) path: the tie hands the token to index 0.
25713        let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
25714        assert_eq!(
25715            idx,
25716            vec![0],
25717            "softmax tie → lower index (the gated contract)"
25718        );
25719        // The resonance path: the strictly larger raw score wins, weight
25720        // exactly 1.0 with and without norm_topk.
25721        for norm in [true, false] {
25722            let m = moe(true, norm);
25723            let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
25724            assert_eq!(idx, vec![1], "norm_topk {norm}");
25725            assert_eq!(p, vec![0.0, 1.0, 0.0]);
25726            assert_eq!(p[1] / wsum, 1.0);
25727            // An exact tie: the first maximum (as `resonance_winner` and
25728            // `embryo_core_route_pick`).
25729            let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
25730            assert_eq!(idx, vec![0]);
25731            // `−∞` never wins; the admitted set is honoured.
25732            let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
25733            assert_eq!(idx, vec![2]);
25734            let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
25735            assert_eq!(idx, vec![0]);
25736            assert_eq!(p[0] / wsum, 1.0);
25737            // Every admitted expert at −∞: the generic path's uniform
25738            // fallback (lowest index, finite weights).
25739            let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
25740            assert_eq!(idx, vec![0]);
25741            assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
25742        }
25743    }
25744}